Weex SDK Android 源码解析
Weex SDK Android 源码解析
Weex 是一套构建高性能、可扩展的原生应用跨平台开发方案。一次编写,多端运行。对于前端是个与客户端融合的最方便的路径,透过客户端的模块封装可以快速的让前端突破浏览器的限制,将客户端能力发威到极限,因此除了业务层的深入外,对于 SDK 框架本身也必须更进一步的了解。
本篇主要是从分析代码入手,探讨一下Weex在安卓平台上是如何构建一套JS的运行框架。主要讨论的范畴包括:线程模型、渲染流程、Component/Module的注册流程以及 Weex 中 JNI 的调试技巧。
本文目录结构如下:
------1. 整体架构
------------1.1 线程模型
----------------1.1.1 结构图
----------------1.1.2 线程间通信
----------------1.1.3 线程异常处理
----------------1.1.4 线程清理
----------------1.1.5 @JSMethod 的使用
------------1.2 渲染模型
----------------1.2.1 结构图
----------------1.2.2 Native 中布局方式
----------------1.2.3 FlexBox 概念说明
----------------1.2.4 CSSNode/WXDomObject 方法说明
----------------1.2.5 ViewPort的使用
------------1.3 Component/Module 注册流程
----------------1.3.1 结构图
----------------1.3.2 Component 注册及使用方式
----------------1.3.3 Module 注册及使用方式
----------------1.3.4 DomObject 注册及使用方式
------2. jni 调试技巧
整体架构
整个运行框架包括三大部分:JS Bridge、Render、Dom,这三大部分都包含在 WXSDKManager 中。WXBridgeManager、WXRenderManager、WXDomManager 都可以通过WXSDKManager 获取。
- JS Bridge:主要用来和 JS Engine(V8)进行双向通信,运行在JSBridge线程中。Weex 的初始化,Component、Module、DomObject的注册与调用,JSBridge 线程管理最终都会由JS Bridge 的管理类 WXBridgeManager 完成。所有和 Dom 相关的操作都会通知到 Dom 线程,交由 WXDomModule 处理。
- Render:主要用来操作具体的Native View,包括管理Native View的各种操作(添加/删除Component,构造Component Tree等)、Native View的布局等,运行在UI线程中。由 WXRenderManager 统一管理,具体操作由 WXRenderStatement 管理,每一个weex instance 一一对应一个 WXRenderStatement。WXRenderStatement 具体就是操作 WXComponent。
- Dom:主要用来操作Dom结构,包括生成对应的Dom Tree,添加/删除Dom 节点(WXDomObject)等操作,运行在独立的 Dom 线程中。由 WXDomManager 统一管理,具体操作由 WXDomStatement 管理,每一个weex instance 一一对应一个 WXDomStatement。WXDomStatement 具体就是操作 WXDomObject。所有的 Dom 操作(包括CSSLayout的计算)都在 Dom 线程中,完成后会通知UI线程处理对应的Native Component View。
线程模型
线程模型图
在处理复杂逻辑的情况下,使用线程是必不可少的,使UI线程不会积压太重的任务,导致界面卡顿。这时候又遇到另一个问题,线程是一次性的消耗品,使用完了线程就自动退出销毁了,假如有比较多的耗时任务,不得不重新创建线程去执行该耗时任务,就会存在性能问题:多次创建和销毁线程是很耗系统资源的。为了解这种问题,我们可以自己构建一个循环线程Looper Thread,当有耗时任务投放到该循环线程中时,线程执行耗时任务,执行完之后循环线程处于等待状态,直到下一个新的耗时任务被投放进来。这样一来就避免了多次创建Thread线程导致的性能问题了。Android SDK中其实已经有一个循环线程的框架-HandlerThread,Weex 中的线程使用的正是HandlerThread,如何使用HandlerThread可以参看官方文档。
Weex中的线程:
- JSBridgeThread:用来java jni层和v8 engine之间进行通信,包括初始化js framework、callJS、callNative等。
- DomThread:用来进行Dom操作,包括Dom解析、设置Dom样式、CSS Layout操作、生成Component Tree等操作。图中可知 DomThread 中的操作都是v8 engine调用上来的,也就是说是js runtime生成dom的各种操作,一旦js bundle过大,会是一个瓶颈。
- UIThread:用来真正的视图渲染,包括设置View Layout、设置View Padding、绑定数据、Add/Remove View等操作。
通信:
- 通信方式:三个线程之间的通信方式使用的都是正常的Android Handler通信机制,每个线程中的所有操作都是时序性的(也是Handle的机制决定),保证了操作Dom的时序性。
使用 runnable 方式
Message m = Message.obtain(mJSHandler, WXThread.secure(r)); m.obj = token; m.sendToTarget();
使用 handleMessage 方式
Message msg = Message.obtain(); WXDomTask task = new WXDomTask(); … msg.what = WXDomHandler.MsgType.WX_DOM_CREATE_BODY; msg.obj = task; WXSDKManager.getInstance().getWXDomManager().sendMessage(msg);
- 通信流程
UIThread 与 JSBridgeThread: JSBridgeThread 不会直接发送任务给 UIThread , UIThread 发送给 JSBridgeThread 的任务有初始化js framework、开始渲染页面createInstance、发送event事件等。
- UIThread 与 DomThread: UIThread 会在销毁instance的时候发送任务给 DomThread 进行清理,DomThread 发送任务给 UIThread 会分为两步,这两步会是一个task:
- 发送前会重新计算CSSLayout的耗时操作,这部分的操作是在DomThread中进行。
- 发送 runnable 到 UIThread,runnable执行的就是view的渲染流程,在UIThread中进行。
说明: 这一整个task是每隔16ms自动触发,也是说一旦dom操作过多,就会拖累帧率。
JSBridgeThread 与 DomThread:DomThread不会直接发送任务给JSBridgeThread 。js runtime会通过jni发送指令到 java 层,这一部分在JSBridgeThread中,然后JSBridgeThread会发送任务给 DomThread 进行各种 Dom 操作。
线程异常
在Weex中线程处理有一个专门的类WXThread,里面分装了HandlerThread处理任务的两种方式:SafeRunnable、SafeCallback,并且 try catch 了所有的异常,以保证在处理过程中异常crash。
线程清理
正常情况下页面退出,是不是应该把所有的线程清理(quit)呢?weex中的做法是NO,想必是为了提高创建、销毁线程消耗系统资源的效率。Weex中WXSDKManager 、WXBridgeManager是单例,WXRenderManager 、WXDomManager的获取都是通过WXSDKManager,在WXSDKInstance destroy 的时候并不会销毁单实例,因此在多次Weex页面进出的时候线程是重用的。
@JSMethod
在WXComponent、WXModule中可以使用 @JSMethod 注解来提供Native方法给JS调用,这个注解有uiThread这个方法,默认值为true 参数说明: 1. 如果uiThread = true,则在UIThread中执行 2. 否则在JSBridgeThread中执行
总结
- 虽说使用了线程,其实都是线性的在执行,只不过把繁重的任务让线程执行了,这也和js runtime中dom解析逻辑、顺序有关。
- 看完了Weex中的线程模型,是不是还是很简单的,没有那么复杂,有木有~
渲染
结构图
以添加dom节点来说明整个渲染过程,步骤1可以换成其他dom操作,比如update、remove等操作。步骤2则是通用的。
- createBody/addDom: WXDomStatement,从 dom tree 到 component tree 的映射
- WXDomObject.parse() 递归解析dom JSONObject,最终得到当前dom树结构
实例化WXDomObject
设置viewport
解析dom JSONObject,得到type、ref、style、attr、event
{ "attr":{"spmId":"spma"}, "ref":"_root", "style":{}, "type":"div" }
赋值 domObject.mDomContext = sdkInstance
- 若为 root 节点
- prepareRoot
- 若没有设置style flexDirection 与 backgroundColor,则设置默认值 column、#ffffff
- 设置style defaultWidth、defaultHeight
- prepareRoot
普通节点:parent.add(domObject) 把当前解析得到的dom树加到父节点,并且把当前节点和父节点置为 dirty。
- traverseTree 遍历当前dom节点
- 注册得到所有dom节点到 mRegistry 中,标记为 young
- 检查root节点是否为fixed节点,把fixed的节点存到root dom object 内
- apply所有的 style 到 CSSNode
递归创建Component Tree
添加 createBody 或者 addDom 任务到 renderTask中
代码如下:
private void addDomInternal(JSONObject dom,boolean isRoot, String parentRef, final int index){ …… //only non-root has parent. WXDomObject parent; WXDomObject domObject = WXDomObject.parse(dom,instance); …… if (isRoot) { WXDomObject.prepareRoot(domObject, WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexHeight(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId)), WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexWidth(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId))); } else if ((parent = mRegistry.get(parentRef)) == null) { instance.commitUTStab(IWXUserTrackAdapter.DOM_MODULE, errCode); return; } else { //non-root and parent exist parent.add(domObject, index); } domObject.traverseTree( mAddDOMConsumer, ApplyStyleConsumer.getInstance()); //Create component in dom thread WXComponent component = isRoot ? mWXRenderManager.createBodyOnDomThread(mInstanceId, domObject) : mWXRenderManager.createComponentOnDomThread(mInstanceId, domObject, parentRef, index); …… AddDomInfo addDomInfo = new AddDomInfo(); addDomInfo.component = component; mAddDom.put(domObject.getRef(), addDomInfo); IWXRenderTask task = isRoot ? new CreateBodyTask(component) : new AddDOMTask(component, parentRef, index); mNormalTasks.add(task); addAnimationForDomTree(domObject); mDirty = true; …… }
- WXDomObject.parse() 递归解析dom JSONObject,最终得到当前dom树结构
- layout:WXDomStatement,由 batch 驱动(每隔16ms执行一批任务,开始渲染)
把所有fixed的节点移到 root 节点 child 内
CSSLayout 计算整个 dom 树(calculateLayout),耗时操作,dom 树即为mRegistry中注册的所有节点
- 遍历 mRegistry 中所有节点
设置 markLayoutSeen
applyUpdate:如果当前节点已经被消费,则post message到渲染的 UIThread 更新Component hostView 的 LayoutParams(更新上一帧已经被消费过的节点的LayoutParams)。
为什么需要这个步骤?正常情况下在renderTask中对Component View setLayout就可以了,但是这要基于一个前提,那就是所有的 Dom 节点都已经被注册到 mRegistry 中了,只有这样最后 CSSLayout 计算出来的才是正确的。 由于 batch 驱动的不确定性(有可能分好几帧),非常有可能在本次 Layout 过程中 Dom 数量是不完整的,导致 CSSLayout 计算的结果肯定是不完整的。因此需要每次batch 重新计算完整 dom 树的时候把之前节点 Layout 再更新一遍,并且此时的节点必为 old(上一帧被标为old)
更新 mRegistry 中计算好的所有节点到Component Tree中的 Dom 节点,并且置为 old,下次 layout 时需要更新Component 的 LayoutParams。(如果只有一帧,就没有也不需要下次更新的机会了)
- 执行renderTasks:由一系列 js bridge 传过来的各种指令,包括Dom操作(createBody、addDom、updateStyle等)、createFinish、updateFinish事件等引起的执行dom对应的componet view操作,这些操作都会被添加到renderTasks中,并且这些任务由 batch 驱动。因为对应的是UI操作,都需在UIThread中执行。
- 示例:createBody、addDom
- createView 如果有parent则add到父View
- applyLayoutAndEvent:Component setLayout,更新Component hostView 的LayoutParams。
- bindData
- 示例:createBody、addDom
代码如下:
//WXDomStatement.java void layout(WXDomObject rootDom) { …… rebuildingFixedDomTree(rootDom); rootDom.calculateLayout(mLayoutContext); …… rootDom.traverseTree(new ApplyUpdateConsumer()); updateDomObj(); parseAnimation(); int count = mNormalTasks.size(); for (int i = 0; i < count && !mDestroy; ++i) { mWXRenderManager.runOnThread(mInstanceId, mNormalTasks.get(i)); } mNormalTasks.clear(); mAddDom.clear(); animations.clear(); mDirty = false; …… }
最终生成的root component 会被添加到 RenderContainer
布局方式:
最终进行布局都会进入函数 applyLayoutAndEvent,layout操作会有两步:
- setLayout:设置当前view的宽、高,以及 margin 值
- width:getLayoutWidth() CSSLayout 计算得到的宽
- height:getLayoutHeight() CSSLayout 计算得到的高
- left:margin left,当前节点相对于父节点的X坐标 - parent的padding值(包括bording值)
- top:margin top,同 left 计算方式
- right:margin right,直接使用CSSLayout计算得到的值
- bottom:margin bottom,同right 计算方式
- setPadding:使用 CSSLayout 计算得到的 padding 值设置当前 view 的 padding 值
其实布局就是设置当前 View LayoutParams 的padding、margin值
FlexBox 概念说明
- Flex direction:FlexDirection 控制 children 的排布方向,并且这个属性标实为主轴方向,有四个可选值:
- Column(默认):主轴方向从上到下排布,垂直的轴即为从左到右
- Row:主轴方向从左到右排布,垂直的轴即为从上到下
- ColumnReverse:和 Column 相反,在 RTL 布局下使用
- RowReverse:和 Row 相反,在 RTL 布局下使用
- Justify content:JustifyContent 控制在一个容器内主轴方向上 children 的排列方式,比如当 FlexDirection = Row 时,可以用这个属性控制 children 水平居中。有五个可选值:
JustifyContent = FlexStart (默认)
JustifyContent = FlexEnd
JustifyContent = Center
JustifyContent = SpaceBetween
JustifyContent = SpaceAround
- Flex wrap:FlexWrap 控制容器内的 children 超出容器时的排布方式。有两个可选值:
FlexWrap = Nowrap:
FlexWrap = Wrap:如果FlexDirection = Row 时则往下排放,如果FlexDirection = Column时则往右排放。
- Alignment:AlignItems 控制在一个容器内垂直轴上 children 的排列方式。和 JustifyContent 有点类似,但是方向上正好相反。有四个可选值:
- Stretch(默认):在垂直轴上拉升 children 的大小与容器匹配。
- FlexStart:排列在垂直轴上的开始位置
- FlexEnd:排列在垂直轴上的末尾位置
- Center:排列在垂直轴上的中间位置
- Flex:FlexGrow 控制在主轴上 children 在剩余的空间如何被分布。
FlexGrow = 1
- Margin、Padding、Border:Margin 与 Padding 有点类似,但是又有比较大的区别。Margin 相对于父节点或者兄弟节点的边距,而 Padding则是指父容器内 children 的边距。Border 则和 Padding 概念基本一致,主要是用来区别 border 效果的大小。
MarginStart = 50
MarginEnd = 50
MarginAll = 50
PaddingAll = 50
BorderWidth = 50
WXDomObject/CSSNode 方法说明
- young:标实当前节点是否被消费过,并且只有在第一次注册是标记为 young。消费过即此节点 CSSLayout 计算完成,并且更新到Component中的DomObject,此后此节点必为 old
- needUpdate:是否需要更新
- markHasNewLayout:每次 CSSLayout 计算完成,都会标实 LayoutState 为 LayoutState.HAS_NEW_LAYOUT
- markLayoutSeen:每次 CSSLayout 计算完成之后都要调用这个方法,用来标实当前节点的 CSSLayout 值可以被使用了。如何当前节点没有调用 markLayoutSeen 被 dirty,会抛异常,说明之前的CSSLayout计算还没被使用过。
- addChild:添加子节点,并且 置为 dirty(自己和父节点)
- getLayoutX/Y():相对于父节点的x、y坐标
- getPadding():当前节点l、t、r、b的padding距离
- getBorder():当前节点l、t、r、b的border距离
- getMargin():当前节点l、t、r、b的margin距离
ViewPort
之前框架支持的布局策略是 flexable 布局,在不同屏宽下保持相同 ViewPort(默认 750),整体缩放,可以解决大部分屏幕自适应问题。但是在某些情况下无法满足,比如想让某些元素在不同屏宽手机中都保持相同大小,画一像素的线等。目前weex 支持可配置的响应式布局,自定义设置 ViewPort 值。
使用方式
<script type="config">
{
"viewport": {
"width": "device-width"
}
}
</script>
Component/Module 注册流程
Component 和 Module 是 Weex 中主要的两种与 js 交互的载体,支持与 js 双向通信。也是 Weex 扩展的两种方式,具体的扩展方式可参考这两篇文章 Weex Android/iOS 扩展指南 ,这篇文章主要介绍 Weex Android Component Module 的注册流程,以及懒加载以提高初始化效率。
注册流程
类结构图
注册类型主要有:Component、Module、DomObject,注册入口统一在 WXSDKEngine 中
Component
注册方式 入口都是从 WXSDKEngine 开始
//WXSDKEngine.java public static boolean registerComponent(String type, Class<? extends WXComponent> clazz, boolean appendTree) throws WXException { return registerComponent(clazz, appendTree,type); }
最终的注册到 WXComponentRegistry.java
public static boolean registerComponent(final String type, final IFComponentHolder holder, final Map<String, Object> componentInfo) throws WXException { …… WXBridgeManager.getInstance() .post(new Runnable() { @Override public void run() { try { Map<String, Object> registerInfo = componentInfo; if (registerInfo == null){ registerInfo = new HashMap<>(); } registerInfo.put("type",type); registerInfo.put("methods",holder.getMethods()); registerNativeComponent(type, holder); registerJSComponent(registerInfo); sComponentInfos.add(registerInfo); } catch (WXException e) { WXLogUtils.e("register component error:", e); } } }); return true; }
可以看到注册分为两部分,这两部分都是在 bridge 线程中执行:
native 部分注册:记录component 的 type 与 holder 的 map 映射。holder 默认为 SimpleComponentHolder,主要用来:如果不为懒加载(lazyLoad),会提前解析好注解 @WXComponentProp 和 @JSMethod,否则会等到使用的时候才会去解析注解,可以提高 weex 的初始化效率。
js 部分注册:把 component 的 type 、methods 信息注册到 js runtime中,可以看到最终的注册部分在 WXBridgeManager 中
private void invokeRegisterComponents(List<Map<String, Object>> components) { …… WXJSObject[] args = {new WXJSObject(WXJSObject.JSON, WXJsonUtils.fromObjectToJSONString(components))}; try { mWXBridge.execJS("", null, METHOD_REGISTER_COMPONENTS, args); } catch (Throwable e) { …… } }
使用方式 使用 WXComponentFactory 的 newInstance 方法生成 WXComponent。
public static WXComponent newInstance(WXSDKInstance instance, WXDomObject node, WXVContainer parent) { …… IFComponentHolder holder = WXComponentRegistry.getComponent(node.getType()); …… try { return holder.createInstance(instance, node, parent); } catch (Exception e) { WXLogUtils.e("WXComponentFactory Exception type:[" + node.getType() + "] ", e); } return null; }
通过holder (默认为 SimpleComponentHolder )的 createInstance 方法生成 WXComponent 实例。
Module
Module 的注册方式与使用方式和 Component 类似,入口从 WXSDKEngine 的方法 registerModule 开始。最终通过 WXModuleManager 注册,也分为 native 与 js 两部分注册,注册的类型会区分是否是全局,如果是全局的则会提前实例化好。
使用方式 通过 WXModuleManager 的 callModuleMethod 方法使用 WXModule,调用者都是从 WXBridgeManager 发起(js端发起)。
static Object callModuleMethod(final String instanceId, String moduleStr, String methodStr, JSONArray args) { ModuleFactory factory = sModuleFactoryMap.get(moduleStr); …… final WXModule wxModule = findModule(instanceId, moduleStr,factory); if (wxModule == null) { return null; } WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId); wxModule.mWXSDKInstance = instance; final Invoker invoker = factory.getMethodInvoker(methodStr); try { return instance .getNativeInvokeHelper() .invoke(wxModule,invoker,args); } catch (Exception e) { WXLogUtils.e("callModuleMethod >>> invoke module:" + moduleStr + ", method:" + methodStr + " failed. ", e); return null; } finally { …… } }
步骤:
- 实例化 WXModule,会先从全局的Module中找,未找到则通过 ModuleFactory 的 buildInstance 方法实例化。
- 调用 WXModule 的方法
DomObject
注册 DomObject主要是为了自定义 WXDomObject 类,默认的 Component 对应的domObject 都为 WXDomObject。
- 注册方式:从 WXSDKEngine 的方法 registerDomObject 开始,最终会从 WXDomRegistry 中注册。
- 获取方式:使用场景是在native端生成dom树的时候
- 从 WXDomRegistry 中获取 DomObject 的 class。
- 从 WXDomObjectFactory 的方法 newInstance 实例化WXDomObject
jni 调试技巧
Android Weex 中使用的 Javascript 引擎为 google 的 V8 引擎,V8 由 C++ 写,在 C++ 与 Java 之间需要JNI进行桥接,普通的JNI的调试方法只能打log输出,重新 ndk build so 动态库文件,重新跑APK程序,比较繁琐,不能直观的动态debug。有一个好消息是最新的Android Studio支持了Android Native 调试,下面就来介绍下如何在Android Studio中动态调试、断点Weex Native 代码。
在 SDK Manager 中的 SDK Tools 安装CMake、LLDB、NDK。
在 Weex SDK 项目中的 build.gradle 增加如下配置
android { externalNativeBuild { ndkBuild { path '../../../weex_v8core/jni/Android.mk' // v8core的具体路径 } } }
把 v8core 目录下build出来的v8core/obj/local/armeabi/libweexv8.so 拷贝到weex sdk目录libs下对应的文件夹中,这一步尤为关键,这个 so 文件是静态文件,不是动态库文件,里面包含了所有符号,因此可以调试debug。