Weex 刚刚开源,非常幸运能在 Weex 团队实习,也见证了 Weex 正式开源的夜晚,作为一名 Androider,当然是万分激动。近三个月的实习过程,Weex Android 的源码也捣腾了不少,写篇文章纪念一下~ 写的不对的地方,还望各大神指点迷津!
PS:这里只对 Weex Android 源码进行分析。
一、Weex第一眼
A framework for building Mobile cross-platform UI (一款轻量级的移动端跨平台动态性技术解决方案)通过 Html 搭建组件结构,flexbox 负责界面布局, Js 控制数据和逻辑相比 RN,Weex 真正做到了 write once run anywhereRN 可以认为是一个全新的跨平台移动开发框架,比较重,适合一个完整应用的开发;而 Weex 是为了增强移动端动态性而生的轻量级框架,具有极强的可扩展性,能够比较容易的融入成熟的Native项目中。跟 QZone 的前辈聊到过 RN,QZone 接入 RN 后,总体性能还不如原来腾讯的 Webview 方案,而 Weex 在加载性能上是要占优势的。二、Weex的重要器官
1、WXDomObject
DomObject 包括了<template>
在 Dom 树中的所有信息,如 style、attr、event、ref(结点的唯一标识符)、parent、children2、WXComponent
Component 负责承载 Native View,可以通过泛型指定承载 View的类型:
abstract class WXComponent{};
class WXListComponent extends WXVContainer{};
Component 会保留 DomObject 的强引用,两者实例是一一对应的。
通过调用initComponentHostView
创建 Component 需要承载的 View,所有 的Component 必须重写initComponentHostView
方法,返回需要承载的 View 的最外层容器。如下:
class WXBaseRefresh extends WXVContainer {
@Override
protected WXFrameLayout initComponentHostView(Context context) {
return new WXFrameLayout(context);
}
}
3、WXModule:
通过 Module 可以将 Native Api 暴露给Js。4、WXSDKInstance
Weex 的渲染单位。明确两个容器的概念:
Instance RootView: Weex 最外层容器, Native 接入方可以设置Instance RootView
大小, 最终通过onViewCreated
返回给用户的View也就是Instance RootView
。JS Root: JS 可以描述的最外层容器, 为RootView
的唯一子节点, 受 JS 样式的控制。
Instance 宽高设置遵循以下几个原则
Instance RootView 的宽高优先遵循 instance 设置的宽高,如果开发者没有设置,则与 JS Root 节点的宽高保持一致。JS Root 节点的宽高优先遵循 CSS 样式设置的宽高,如果没有设置,则使用 instance 上设置的宽高,如果 instance 也没有设置,则使用 layout 出来的宽高特殊情况,当scroller
和list
作为 JS Root 时,如果不设置高度, 会给scroller
和list
设置flex:1
综上所述,Instance RootView 和 JS Root 的宽高可以不一致,应该根据需求正确的设置 Instance 的宽高,也可以在运行时动态的改变 Instance 的宽高。
三、Weex工作原理
1、渲染原理
.we文件由<template>
、<style>
、<script>
三部分组成;首先,transformer 会将 .we 文件转换成 Js Bundle,JSFramework 根据 Js Bundle 生成 Virtual Dom,根据Virtual Dom 控制 Native 的视图层;首次渲染时,会将所有结点都交给 Native Render 渲染,在 UI 更新时,计算出最小 dif,让 Native 仅渲染发生改变的结点。2、派发渲染指令的枢纽:WXDomModule
上面提到,JSFramework 根据 Virtual Dom 计算出来的 dif,将渲染指令(Json)通过 Js Engine 发送给 Native Render 进行渲染。而WXDomModule
会接收到所有渲染指令,然后将指令post 给DomHandler
,最后由 DomHandler 来派发渲染任务。DomStatement
在 Dom 线程中创建 DomObject 和 Component,RenderStatement
负责在 UI 线程中渲染 View;每个 WXSDKInstance 会持有一个 DomStatement 和RenderStatement
实例。RenderStatement
会从DomStatementclone
一份 DomObject,是为了避免两个线程同时操作 Dom 造成的同步问题。主要有如下指令:createBody:DomStatement
首先在 Dom 线程中创建 JS Root 对应的 Component,然后会将 JS Root 添加到 WXSDKInstance 作为GodCom
的子节点,从而生成 Component 树的最顶端。生成 Component 树后,将 createBody 任务 post 到 UI 线程,由RenderStatement
创建 WXSDKInstance 的 Rootview,并通过onViewCreated
回调给 WXSDKInstance 的上下文。addElement:首先,DomStatement
在 Dom 线程中创建 DomObject 和对应的 Component 实例,加入 Dom 树和 Component 树;然后将 addElement 任务 post 到 UI 线程,RenderStatement
会触发 Component 完成以下任务: createView(初始化 Component 承载的 View)、applyLayoutAndEvent(触发 setLayout 和 setPadding、绑定 Event)、bindData(给 View 设置 style、attr)、addChild(将 View 加入 View 树)removeElement:是 addElement 的逆向操作,将 View、Component、DomObject 分别从各自的树中删除,并销毁数据回收资源。moveElement:将 View、Component、DomObject 在树中移动位置,move 操作最终被拆分成一次 remove 操作和一次 add 操作。addEvent:绑定事件。removeEvent:撤销事件绑定。updateAttrs:当结点 attr 被改变时,会触发 updateAttrs,最终会触发 WXComponent 中的 updateProperties 刷新 UI。updateStyle:与 updateAttrs 类似。createFinish:JsFramework 将所有渲染指令都发出后,会触发 createFinish,最后会触发 onRenderSuccess 回调。updateFinish:JsFramework 将所有 update 指令发出后,会触发 updateFinish,最后会触发 onUpdateFinish 回调。3、Js与Native的通信方式
(1)Js调用Native
Js 调用 Native 必须以 Module 的方式实现,@WXModuleAnno
注解会将 Module 方法暴露给Js。Js 调用 Module 方法:
this.$call('modal', 'toast', {'message': 'naviBar.rightItem.click','duration': duration});
modal
是 Module 名字,toast
是 Module 的方法名。
Js 调用 Native 方法,均通过如下方式完成:
WXModuleManager.callModuleMethod(instanceId, (String) task.get(WXDomModule.MODULE),(String) task.get(WXDomModule.METHOD), (JSONArray) task.get(WXDomModule.ARGS));
static boolean callModuleMethod(String instanceId, String moduleStr, String methodStr, JSONArray args) {//通过 ModuleFactory 拿到 module 的实例ModuleFactory factory = sModuleFactoryMap.get(moduleStr);final WXModule wxModule = findModule(instanceId, moduleStr, factory);wxModule.mWXSDKInstance = WXSDKManager.getInstance().getSDKInstance(instanceId);/**** 通过反射拿到方法和参数,构造 Invoker 对象*/Map<String, Invoker> methodsMap = factory.getMethodMap();final Invoker invoker = methodsMap.get(methodStr);invoker.invoke(wxModule, params);}
(2)Native 调用 Js 之 fireEvent
fireEvent
一般在 Component 中使用,多用于事件监听
WXSDKManager.getInstance().fireEvent(mInstanceId, getRef(), WXEventType.ONCLICK);
第一个参数表示其所在 WXSDKInstance 的 id,第二个参数是 Component 的ref
(唯一标识),第三个参数是事件名称。
<text onclick="onclick">weex</text>
<script>module.exports = {methods: {onclick: function(param) {param.x;param.y;},}}</script>
(3)Native调用Js之JSCallback
JSCallback
一般在 Module 中使用,可以参考 WXStreamModule 的实现
@WXModuleAnnopublic void fetch(String optionsStr, final JSCallback callback) {Options.Builder builder = new Options.Builder().setMethod("GET").setUrl(url);extractHeaders(headers, builder);final Options options = builder.createOptions();sendRequest(options, new ResponseCallback() {@Overridepublic void onResponse(WXResponse response, Map<String, String> headers) {if (callback != null) {Map<String, Object> resp = new HashMap<>();resp.put(STATUS, response.statusCode);resp.put("ok", (code >= 200 && code <= 299));......resp.put(STATUS_TEXT, Status.getStatusText(response.statusCode));resp.put("headers", headers);callback.invoke(resp);}}});}
在callModuleMethod
中已经将JSCallback
与 instanceIs、callbackId 封装成SimpleJSCallback
,只需调用 invoke,传入参数即可。
那么 JS 这边又该如何接收回调呢?
stream.fetch({method: 'GET',url: GET_URL,}, function(ret) {if(!ret.ok){me.getResult = "request failed";}else{console.log('get:'+ret);me.getResult = ret.data;}});
四、存在的问题
List 是业务中较为常用的容器,而 Weex Android 的 List 是通过 原生的RcyclerView
实现的,由于 RecyclerView 的复用机制,给 List 组件带来了不少坑,这里主要说下我在开发中碰到的几个例子。1、RecyclerView原理
首先简单介绍一下 RecyclerView 的复用原理~Recycler
是 RecyclerView 中管理组件复用的核心内部类,而 Recycler 中有几个重要的成员变量:
mCachedViews
:刚刚从屏幕中滑出的ViewHolder
,且 bind 的数据未修改,会缓存在 mCachedViews 中 , mCachedViews 中的 ViewHolder 随时可以重新显示在屏幕上而不需要重新 bindData;针对每一种ViewType
的 ViewHolder,缓存的数量默认为2mAttachedScrap
、mChangedScrap
:mCachedViews 中被修改过的脏块,会转移到scrap
中(Attached 和 Changed 的区别仅仅在于对 itemAnimation 的支持,这里不展开说了,统称为 scrap);scrap 中的 ViewHolder,如要重新显示在屏幕上,需要重新 bindData。RecycledViewPool
:RecycledViewPool 是可以支持多个 RecyclerView 共享的 ViewHolder 缓存池,当 mCachedViews 或 scrap 满了之后,会将末尾的元素移动到 RecycledViewPool 中,这里可以理解为分级缓存。RecyclerViewPool 里有两个成员变量,SparseArray> mScrap
和SparseIntArray mMaxScrap
,mScrap 根据 ViewType 对 ViewHolder 进行二级索引存储;mMaxScrap
表示每一类 ViewType 的允许缓存 ViewHolder 的最大数量;但是 RecycledViewPool 并没有对不同 ViewType 的数量进行限制,所以这里会涉及到一些坑,下面展开说明。
2、Cell复用存在的问题
在 Android 中,RecyclerView 提供了复用机制来减少内存开销、提升滑动效率,Weex 中 List 也暴露出相应的 API 支持 Cell 复用:设置相同scopeValue
的 Cell 支持 ViewHolder 复用。但是,List 在对scopeValue
的支持上,还存在一些问题:(1)Cell 使用 if 控制子元素发生 crash
Cell 复用的前提条件是 view 层级和布局完全一致,如果使用 if 控制 Cell 子元素的可见性,可能导致复用时旧 Cell 和新 Cell 结构不一致,在重新 bindData 时产生 crash。
(2)Cell 复用后产生文字截断
为了提升滑动效率,Cell 被复用时不会触发setLayout
,如果在 Cell 子元素中含有不定宽度的text
组件,复用后不会重新计算 text 宽度,所以导致文字截断。建议:目前 List 对 scopeValue 的支持还不够完善,使用时要多加注意,前端应该遵循“Cell 可复用的前提是 view 层级和布局完全一致”的规则,对于内部结构不确定、样式可能发生变化的 Cell 不要进行复用;如果使用得当,scopeValue 还是很强大的~
3、Cell 内存泄露
由于 Weex 重写了Recycler.ViewHolder
,使得 ViewHolder 持有 Cell 的强引用,由于 RecyclerView 会将 ViewHolder 缓存在 RecycledViewPool 中,导致 Cell 被 remove 后,无法被 JVM 回收,导致 Cell 树内存泄漏;这个问题在 0.7.0 中已经修复,ViewHolder 持有 Cell 的软引用,List 持有 Cell 的强引用,可以保证 Cell 在正确的时机被回收。4、无用的 ViewHolder 缓存
如果不指定 Cell 的 scopeValue,会使得每一个 Cell 都有不同的 ViewType,之前提到 RecycledViewPool 不限制不同类型的 ViewHolder,所以这里会导致 ViewHolder 不限数量的缓存堆积在 RecycledViewPool 中,而且根本不会参与复用,所以理解为“无用的缓存”。在 v0.7.0 中解决了这个问题,限制了不同 ViewType 在 RecycledViewPool 中缓存的数量;由于 Cell 和 Cell 中承载的 View 都会保存在内存中,可以省去 Component 创建,所以 ViewHolder 的创建耗时不多。v0.6.1:Cell 被 RecyclerView 缓存池引用v0.6.1:内存情况
v0.7.0:内存情况(可以明显看到内存被释放的过程)
5、Weex 中 ViewHolder 创建过程存在的问题
当 RecyclerView 在缓存中找不到合适的 ViewHolder 复用时,会调用onCreateViewHolder
创建新的 ViewHolder;Weex 继承RecyclerView.ViewHolder
实现了自己的ListBaseViewHolder
,将 Cell 作为 ViewHolder 的成员变量,所以在 onCreateViewHolder 创建 ViewHolder 的时候,需要找到一个“ViewType一致,且没有被 ViewHolder 绑定过”的 Cell 与其关联。在 v0.6.1 中,查找方式是遍历所有的 Cell,直到找到合适的那个,这样存在一个问题,当 List 元素特别多的时候,查找过程的性能消耗会有一些劣势:for (int i = 0; i < childCount(); i++) {WXComponent component = getChild(i);if (component == null || component.isUsing() || getItemViewType(i) != viewType)continue;.......}
在 v0.7.0 中,做了相应优化,与 RecyclerView 的缓存池使用同一种查找方式,也就是根据 ViewType 对 Cell 做分类的二级索引,这样就避免了多余的循环。
ArrayList<WXComponent> mTypes = mViewTypes.get(viewType);.......for (int i = 0; i < mTypes.size(); i++) {WXComponent component = mTypes.get(i);if (component == null || component.isUsing()) {continue;}.......}private int generateViewType(WXComponent component) {long id;try {id = Integer.parseInt(component.getDomObject().ref);String type = component.getDomObject().attr.getScope();if (!TextUtils.isEmpty(type)) {if (mRefToViewType == null) {mRefToViewType = new ArrayMap<>();}if (!mRefToViewType.containsKey(type)) {mRefToViewType.put(type, id);}id = mRefToViewType.get(type);}} catch (RuntimeException e) {id = RecyclerView.NO_ID;}return (int) id;}
五、快速接入Weex
sdk的接入就不赘述了,主要说下Native这边需要的代码配置。1、首先创建 WXSDKInstance 实例
mInstance = new WXSDKInstance(this);mInstance.setImgLoaderAdapter(new ImageAdapter(this));mInstance.registerRenderListener(this);
2、实现渲染的监听接口
public class WXBaseActivity implements IWXRenderListener {}mInstance.registerRenderListener(this);
3、指定要渲染的Page
mInstance.render(TAG,// path表示需要渲染的js文件的路径WXFileUtils.loadFileContent(path, WXPageActivity.this),null,null,ScreenUtil.getDisplayWidth(WXPageActivity.this),ScreenUtil.getDisplayHeight(WXPageActivity.this),// 传入WXRenderStrategy.APPEND_ASYNC表示异步渲染WXRenderStrategy.APPEND_ASYNC);
这里需要说明一下,第5、6个参数是用来指定 Instance 的宽高
4、实现回调
@Overridepublic void onViewCreated(WXSDKInstance instance, View view) {// 这里返回的view就是weex将我们指定path的js文件渲染出来的view}
如果觉得《Weex-初次见到你》对你有帮助,请点赞、收藏,并留下你的观点哦!