Cómo diseñar una solución dinámica nativa pura

Por qué existe una solución dinámica nativa pura

Muchas soluciones dinámicas en la industria se implementan a través de máquinas virtuales JS. Hay muchos beneficios. La lógica puede ser dinámica. Hay JavaScriptCore (iOS) o V8 (Android) listos para usar como motor dinámico, que puede cubrir el 90% de la escenas apelación

Pero para las páginas principales, como los feeds de la página de inicio, el automóvil amarillo pequeño, los pedidos, los detalles comerciales y otras páginas, habrá problemas de estabilidad y rendimiento a través de soluciones dinámicas (después de todo, JS, como lenguaje interpretado y hilo único, tiene un cuello de botella natural, el conjunto de instrucciones basado en registros conduce a un mayor consumo de memoria. La devolución de llamada asincrónica también se realiza mediante el mecanismo de notificación de mensajes después de que el subproceso principal se envía al subproceso de trabajo para su procesamiento. Además, la capa inferior del puente también se realiza llamando al método Native, así como a JS y conversión de tipo Native.)

Hice algunos cambios con la demostración oficial de ReactNative. El modelo es iPhoneX, y la velocidad de fotogramas es la siguiente cuando se usa FlatList (componente de lista de alto rendimiento de RN) para deslizar rápidamente. La velocidad de fotogramas mínima es de 52 fotogramas cuando se desliza rápidamente.

j7t5t-mnfk5.gif

Hizo una lista nativa similar, el rendimiento deslizante es el siguiente, la velocidad de fotogramas mínima es de 58 fotogramas

kvazq-hvucr.gif

El diseño consta de dos etiquetas más una vista de imagen, y la celda muestra diferentes alturas según los datos para simular la situación de altura variable, que es una escena muy típica con una estructura de interfaz de usuario relativamente simple. En este caso, la diferencia de rendimiento entre Native y RN será más obvia, por lo que la diferencia definitivamente será más obvia cuando la estructura celular sea más compleja.

Después de comparar las soluciones comunes en la industria, como complemento de la escena ReactNative, la página tiene requisitos dinámicos y los requisitos dinámicos de la lógica no son tan altos. La solución dinámica nativa con un buen rendimiento de representación tiene valor comercial.

La solución dinámica nativa de alto rendimiento generalmente usa el formato de archivo binario acordado, usa un decodificador personalizado para convertir el archivo binario en un OriginTree en la aplicación y luego genera un árbol de vista en la canalización para finalmente representar una vista nativa.

Compare los pros y los contras de los formatos de archivo generales y binarios personalizados

comparación de habilidades Archivos comunes como JSON, XML binarios personalizados
generalidad no
Tamaño del archivo (tome la ventana emergente como ejemplo) 17 KB 2KB
Proporción que requiere mucho tiempo para analizar el mismo archivo iOS 6 1
la seguridad Diferencia Mejor, no puede obtener el contenido correspondiente sin conocer las reglas de análisis
Requiere entorno de desarrollo adicional No necesita 需要前端搭建编写环境、服务端,客户端定制编解码器
拓展性

对比以上优劣点,大型APP在资源充足的情况下往往更关注性能、安全性以及后续扩展性方面。

接下来我会大致聊聊端上相关的开发思路。

制定文件格式

我们可以参考zhuanlan.zhihu.com/p/20693043 进行二进制文件格式设计

客户端可以利用JSON来描述UI:

//ShopBannerComponent
{
    "componentName": "ViewComponent",
    "width": "375",
    "height": "70",
    "backgroundColor": "#fff",
    "onClick": "customClick(mdnGetData(data.jumpUrl))",
    "children": [
        {
            "componentName": "ListComponent",
            "width": "100%",
            "height": "50",
            "listData": "mdnGetData(data.list)",
            "orientation": "horizontal",
            "children": [
                {
                    "componentName": "TextComponent",
                    "width": "mdnGetSubData(item.width)",
                    "height": "mdnGetSubData(item.height)",
                    "maxLines": "1",
                    "textSize": "15",
                    "textColor": "#fff",
                    "text": "mdnGetSubData(item.content)"
                }
            ]
        },
        {
            "componentName": "ImageComponent",
            "width": "100%",
            "height": "20",
            "contentMode": "aspectFill",
            "imageUrl": "mdnGetData(data.backgroudPic)"
        },
        {
            "componentName": "TextComponent",
            "width": "44",
            "height": "15",
            "maxLines": "1",
            "textSize": "15",
            "textColor": "#fff",
            "text": "mdnGetData(data.desc)"
        }
    ]
}
复制代码

经过和后端协商定制协议后,生成的二进制文件如下:

Header(固定大小区域)

  • 标志符:也叫MagicNumber,判断是否是指定文件格式
  • MainVersion:用来判断二进制文件编译的版本号,和本地解码器版本做对比,当二进制版本号大于本地时,判断文件不可用,最大值1btye,也就是版本号不能大于127
  • SubVersion:当新增feature的时候需要升级,本地解码器根据版本做逻辑判断,最大值不能大于short的最大值32767

    大的版本迭代比如1.0升级到2.0,规定必须是基于核心逻辑的升级,整个二进制文件结构可能会重新设计,这时候通过主版本号比对,假如版本号小于文件版本号,那么就直接不读取,返回为空。小的迭代比如二进制文件内新增了某个小feature,在对应SDK内部逻辑添加一个版本判断,大于指定版本就读取对应区域,使用新的feature,老版本还是能够正常使用基本功能,做到向上兼容。

  • ExtraData:预留空间,用于后续扩展,可以包含文件大小,checksum等内容,用来检验文件是否被篡改

Body

FileNameLength用于读取文件名长度,然后根据FileNameLength读取具体文件名,比如FileNameLength为19,往后读取19byte长度数据,UTF8Decode成对应文件名ShopBannerComponent

读取流程

大致流程图

参考Flutter的渲染管线机制,设置如下流程图

整个渲染流程都是在一个流水线内执行,可以保证从任意节点开始到任意节点结束

日常运用场景比如:我们在TableView里要尽快的返回Cell的高度,这时候流水线执行到MDNRenderStageCalculateFrame即可,同时会按照indexPath进行索引值Cache,后续需要返回cell的时候,取到对应indexPath的Component,后续再执行MDNRenderStageFlatten以及后面逻辑,保证每个component的各个节点只会执行一次,大致流程如下

流水线执行始终围绕在Component,只不过每道工序都会让Component更接近NativeView

就和汽车工厂里一样,最开始只有一个车架,后面通过按照引擎、零部件、喷漆等等工序最终组装成我们可以驾驶的汽车

组件解析

将本地二进制文件转化原始视图树,这个阶段不会绑定动态数据,通过全局缓存一份, 后续以Copy的形式生成对应副本,可以有效的提高性能以及降低内存,然后在副本进行数据绑定以及生成IntermediateTree

  • OriginObjectTree:直接通过二进制数据解析出来的树,全局只有一个,类似于Flutter的WidgetTree
  • IntermediateTree:通过OriginObjectTree克隆后,将数据填充进去计算布局后,然后经过层级剪枝的树,将没有点击事件以及无特殊UI效果的Node进行合并,目的是为了降低渲染树生成真实view的视图层级,减少View实例,避免了创建无用view 对象的资源消耗,CPU生成更少的bitmap,顺带降低了内存占用,GPU 避免了多张 texture 合成和渲染的消耗,降低Vsync期间的耗时
  • RenderTree:和IntermediateTree一一对应,递归生成原生View

和ReactNative类似,所有的组件都继承自基类,基类提供一些生命周期方法让子类重写

@interface MDNBaseComponent : NSObject {
//子类重写测量方法
- (void)onMeasureSizeWidth:(MDNMeasureValue)widthValue height:(MDNMeasureValue)heightValue;
//子类重写布局方法
- (void)onLayout:(CGRect)rect;
//子类重写渲染对应的NativeView方法
- (void)onRender:(UIView *)view;
//子类重写事件相关方法
- ((BOOL)onEvent:(MDNEvent *)event;
//子类被加载的方法
- (void)componentDidLoad;
//子类被卸载的方法
- (void)componentDidUnload;
复制代码
  • 字符串存储区域存的是对应的常量、枚举、事件、方法、表达式,比如代码中宽度375 ,枚举值aspectFill,表达式mdnGetData(data.backgroudPic),这些值都会有对应的key,用于组件解析的时候进行绑定对应属性
{
    "componentName": "ImageComponent",
    "width": "100%",
    "height": "20",
    "contentMode": "aspectFill",
    "imageUrl": "mdnGetData(data.backgroudPic)"
}
复制代码
  • 表达式区域存储的是全部用到的表达式字段,每个表达式都有对应的key,与component的属性进行关联,因为表达式可以互相嵌套,因此我们可以考虑设置成树型结构。startToken以及endToken代表表达式的开始和结束,通过遍历将表达式exprNode入栈,同时将入栈的exprNode添加到之前栈顶的exprNodechildren,形成一个单节点树,方便表达式组合使用
  • 组件区域是按照DSL代码顺序,从上往下遍历,因为Component也是可以互相嵌套,也是树形结构,通过startToken以及endToken代表一个component的开始和结束,客户端层面也是按照区域顺序读取,遇到startToken创建一个component,期间会绑定属性、事件、方法,以及动态表达式,然后入栈,遇到endToken出栈,同时设置栈顶的Component为父组件,最终得到一个ComponentOriginTree

组件动态绑定

当ViewComponent需要进行动态绑定,将表达式进行遍历扫描,以customClick(mdnGetData(data.jumpUrl))为例,在二进制文件中,会通过对应的key解析成事件表达式Node,然后mdnGetData(data.jumpUrl)在二进制文件中,解析成方法表达式Node,最后在方法表达式里data.jumpUrl会进行以下操作,大致流程如下:

这个解析流程参考了SQL的解析原理

注意:合法判断里面有很多状态切换的情况需要考虑,比如如何从上个扫描的字符串到当前扫描字符串的状态切换是合法的

  • 前一个是a-z,A-Z相关的字母,那么后面的扫描结果也只能是a-z,A-Z、[、.,假如扫描到了],就是非法的
  • El anterior es [, luego el escaneo posterior solo puede ser 0-9
  • El primero es 0-9, el último solo puede ser 0-9,]

Dado que debe haber una gran cantidad de lógica de expresión en un componente, es normal realizar miles o incluso decenas de miles de recorridos. La pérdida de rendimiento acumulada por este juicio de estado también es muy grande. Por lo tanto, este tipo de lógica de juicio de estado es mejor hecho a través de la matriz Para hacer de a a procesamiento, para lograr el efecto de optimizar el rendimiento, después de la prueba, el estado aleatorio se ejecuta 10,000 veces y el tiempo de ejecución se acorta en un 20%

Cálculo y diseño de ancho y alto de componentes

Después de vincular las propiedades finales, puede calcular el ancho y la altura del componente y los componentes secundarios.Tomando como ejemplo el contenedor principal más simple de ancho y alto fijo, el contenedor principal atraviesa las vistas secundarias para pasar sus propias restricciones, como el máximo ancho del contenedor principal Alto, el contenedor secundario calcula su propio tamaño de acuerdo con las restricciones del contenedor principal, y luego realiza la recursión de restricciones de acuerdo con el algoritmo DFS para finalmente determinar el diseño de cada vista secundaria

Tome el diseño de la Figura 1 como ejemplo

Después de calcular el diseño de todos los componentes, es necesario eliminar los componentes jerárquicos inútiles para evitar que el nivel del árbol de representación sea demasiado alto y optimizar el rendimiento de las estructuras de vista complejas.

Representación de componentes

Cuando obtengamos el árbol plano completo, podemos generar recursivamente la Vista correspondiente a la Nativa. Antes de renderizar, debemos diferenciar para reducir la creación y destrucción de UIView tanto como sea posible, lo que ayuda a mejorar el rendimiento, especialmente en el extremo inferior máquinas y vistas En componentes complejos, la reutilización puede reducir mucho el tiempo de renderizado

Al mismo tiempo, debido a que la operación de la Vista en Android e iOS debe realizarse en el hilo principal, si crea una Vista por adelantado y modifica los datos o el diseño, generará muchos envíos de transacciones inútiles. Al calcular los datos y el marco, solo puede configurarlo una vez para garantizar el rendimiento.

El algoritmo diff puede hacer referencia a la diferencia de flutter y determinar si cada nodo secundario se puede reutilizar a través del recorrido O(n)

Una vez que se completa la diferencia, el marco correspondiente al Componente y el evento se vinculan a la vista correspondiente, como

ViewComponent对应MDNView
ListComponent对应MDNCollectionView
ImageComponent对应MDNImageView
TextComponent对应MDNLabelView
复制代码

Finalmente, obtenemos una vista dinámica que admite gestos de clic con lógica pura al final.\

Documentación de referencia:

ParseSQLToken

AleteoDentro

Interfaz dinámica: DSL y motor de diseño

Supongo que te gusta

Origin juejin.im/post/7119781637440667678
Recomendado
Clasificación