Weex-初次见到你

简介: Weex 刚刚开源,非常幸运能在 Weex 团队实习,也见证了 Weex 正式开源的夜晚,作为一名 Androider,当然是万分激动。近三个月的实习过程,Weex Android 的源码也捣腾了不少,写篇文章纪念一下~ 写的不对的地方,还望各大神指点迷津! PS:这里只对 Weex Android 源码进行分析。 ---------- ## 一、Weex第一眼 - A

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的类型:

  1. abstract class WXComponent{};
  2. class WXListComponent extends WXVContainer{};

  • Component 会保留 DomObject 的强引用,两者实例是一一对应的。
  • 通过调用 initComponentHostView 创建 Component 需要承载的 View,所有 的Component 必须重写 initComponentHostView 方法,返回需要承载的 View 的最外层容器。如下:

  1. 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 的实现

    @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,缓存的数量默认为2
    • mAttachedScrapmChangedScrap: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、实现回调

@Override
public void onViewCreated(WXSDKInstance instance, View view) {
  // 这里返回的view就是weex将我们指定path的js文件渲染出来的view
}
目录
相关文章
|
4月前
|
Web App开发 JavaScript 前端开发
不光好上手,功能还特强的 Vue 3组件!且开源免费!
不光好上手,功能还特强的 Vue 3组件!且开源免费!
|
资源调度 前端开发 JavaScript
三分钟搭建React开发环境
三分钟搭建React开发环境
430 0
|
JavaScript 编译器 API
Vue 3 源码开放,赶紧拉下来尝尝鲜
Vue 3 源码开放,赶紧拉下来尝尝鲜
62 0
|
JavaScript 前端开发 网络架构
React开发实践(9)实战部分 详情页和登陆功能开发
React开发实践(9)实战部分 详情页和登陆功能开发
523 0
|
存储 前端开发 Ubuntu
初次使用阿里云完成一个小项目的体验
作为一个非编程专业的学生,使用阿里云服务器对我的帮助很大,以下是我使用阿里云过程中的一些体验
初次使用阿里云完成一个小项目的体验
|
移动开发 小程序 前端开发
Taro小程序采坑记
Taro小程序采坑记
149 0
Taro小程序采坑记
|
Web App开发 JavaScript 前端开发
从零到一搭建 react 项目系列之(九)
这篇文章,我们先介绍如何观察 store 的变化,上文我说了可以通过 redux-logger 或者 Redux DevTools 来解决这个我们的需求。
179 0
从零到一搭建 react 项目系列之(九)
|
存储 JavaScript 前端开发
从零到一搭建 react 项目系列之(十)
本文将介绍 Reducer 的拆分。
159 0
从零到一搭建 react 项目系列之(十)
|
移动开发 资源调度 前端开发
从零到一搭建 react 项目系列之(六)
本篇主要介绍 react-router-v4 搭建。
181 0
从零到一搭建 react 项目系列之(六)
|
资源调度 前端开发
从零到一搭建 react 项目系列之(二)
从零到一搭建 react 项目系列之(二)
128 0
从零到一搭建 react 项目系列之(二)