失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Weex-初次见到你

Weex-初次见到你

时间:2019-11-21 15:20:36

相关推荐

Weex-初次见到你

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、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 宽高设置遵循以下几个原则

Instance RootView 的宽高优先遵循 instance 设置的宽高,如果开发者没有设置,则与 JS Root 节点的宽高保持一致。JS Root 节点的宽高优先遵循 CSS 样式设置的宽高,如果没有设置,则使用 instance 上设置的宽高,如果 instance 也没有设置,则使用 layout 出来的宽高特殊情况,当scrollerlist作为 JS Root 时,如果不设置高度, 会给scrollerlist设置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,缓存的数量默认为2mAttachedScrapmChangedScrap:mCachedViews 中被修改过的脏块,会转移到scrap中(Attached 和 Changed 的区别仅仅在于对 itemAnimation 的支持,这里不展开说了,统称为 scrap);scrap 中的 ViewHolder,如要重新显示在屏幕上,需要重新 bindData。RecycledViewPool:RecycledViewPool 是可以支持多个 RecyclerView 共享的 ViewHolder 缓存池,当 mCachedViews 或 scrap 满了之后,会将末尾的元素移动到 RecycledViewPool 中,这里可以理解为分级缓存。RecyclerViewPool 里有两个成员变量,SparseArray> mScrapSparseIntArray 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-初次见到你》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。