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 anywhere
- RN 可以认为是一个全新的跨平台移动开发框架,比较重,适合一个完整应用的开发;而 Weex 是为了增强移动端动态性而生的轻量级框架,具有极强的可扩展性,能够比较容易的融入成熟的Native项目中。
- 跟 QZone 的前辈聊到过 RN,QZone 接入 RN 后,总体性能还不如原来腾讯的 Webview 方案,而 Weex 在加载性能上是要占优势的。
二、Weex的重要器官
1、WXDomObject
- DomObject 包括了
<template>
在 Dom 树中的所有信息,如 style、attr、event、ref(结点的唯一标识符)、parent、children
2、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 RootView: Weex 最外层容器, Native 接入方可以设置
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 的实现@WXModuleAnno public 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() { @Override public 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、实现回调
@Override
public void onViewCreated(WXSDKInstance instance, View view) {
// 这里返回的view就是weex将我们指定path的js文件渲染出来的view
}