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

    public abstract class  WXComponent<T extends View>{};
    public class WXListComponent extends WXVContainer<BounceRecyclerView>{};
    
  • Component 会保留 DomObject 的强引用,两者实例是一一对应的。
  • 通过调用 initComponentHostView 创建 Component 需要承载的 View,所有 的Component 必须重写 initComponentHostView 方法,返回需要承载的 View 的最外层容器。如下:
    public class WXBaseRefresh extends WXVContainer<WXFrameLayout> {
    
    @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
}
时间: 2024-11-08 18:06:33

Weex-初次见到你的相关文章

newcpu:专注小分类 再创新辉煌

秦国华,网名:newcpu,安徽合肥人,中国网络发展历史上不可或视的一位曾经辉煌的知名站长.他和他的朋友曾经带领3tom.com,91look.com,91look.net,wo99.com等10多个个人网站进入中国网站百强榜单,是当时个人站长合肥势力的领军人物.2004年后由于某种原因,渐渐淡出网络.因此,他的近踪也格外让人好奇,为此,通过努力,本频道笔者走访了这位曾经名噪一时的网络名人.      观点之我看生活:活的好不一定要钱多     初次见到秦国华的时候,他亲自来到公司门口来接我们,

彻底学习Java语言中的覆盖和重载

初次见到这两个单词并没有什么特别的感觉,但是时间长了,却发现书上一会儿用override,一会儿又用overload,搞得我的迷迷糊.于是就做了个总结,希望能对和我一样对这两个概念模糊不清的网友有一个帮助.override可以翻译为覆盖,从字面就可以知道,它是覆盖了一个方法并且对其重写,以求达到不同的作用.对我们来说最熟悉的覆盖就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法.除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法.

用风行网络电影,影视节目全看遍

已是隆冬季节,窗外寒风呼啸,难得有一些闲暇时光,管他外面是刮风还是下雪,打算自己利用"猫冬"的机会,放慢工作节奏,泡一杯香浓的咖啡或热茶,在家里好好欣赏那些想看却一直没时间去影院看的影视大片. 不过,如何欣赏这些影视节目呢?去购买光盘,跑来跑去的太累了,而网购需要几天才能购全,到时只怕没有时间观看了.网上的P2P网络电视,不断缓冲不说,一些画面还惨不忍睹.BT下载,能不能找到BT种子还不一定,而且找到后能不能下载完也不一定. 这也不行,那也不行,心情不禁有点烦闷,忽然想起以前朋友推荐的

增加转化率的5大劝导式网页设计技巧

眼见为实耳听为虚.人脑处理视觉信息的速度是文字信息的50倍.因此,借助视觉刺激让他人执行动作要比利用口头交流或文字更为容易,此原理也适用于电商网站设计. 根据网页设计心理学最新研究成果,要让访客执行动作,首先必须了解他们的心理.人脑做决策时大多是仓促的,未经过深思熟虑.Malcolm Gladwell说过,"成功的决策是权衡理性思维与感性思维的结果."当访客面对众多选择时,最重要的是从感性层面吸引他们,但你只有几秒时间来完成这一步.关于劝导式网页设计,以下有五个建议: 一.清晰明确 人

电商网站设计:增加页面转化率的5个设计建议

眼见为实耳听为虚.人脑处理视觉信息的速度是文字信息的50倍.因此,借助视觉刺激让他人执行动作要比利用口头交流或文字更为容易,此原理也适用于电商网站设计. 根据网页设计心理学最新研究成果,要让访客执行动作,首先必须了解他们的心理.人脑做决策时大多是仓促的,未经过深思熟虑.MalcolmGladwell说过,"成功的决策是权衡理性思维与感性思维的结果."当访客面对众多选择时,最重要的是从感性层面吸引他们,但你只有几秒时间来完成这一步.关于劝导式网页设计,以下有五个建议: 一.清晰明确 人脑

Windows 10 Redstone将允许在云端保存账户密码与菜单布局

微软即将为用户带来Windows 10 Redstone这个大更新,根据近日流出的屏幕截图,可知其将允许用户在云端保存密码.开始菜单布局等账户信息.需要指出的是,曾在build 14257和14287中短暂出现的这一特性,已在最新的编译版本中移除.但类似选项早已在Windows 8.1上用过,所以它肯定会在进一步完善后在Redstone更新中回归. Windows 10 Redstone将允许在云端保存账户密码与菜单布局的照片 Thurrott的报道称,全新的OneDrive功能被列在设置选项的

统计工具R在排查和诊断中的实战

排查和诊断需要统计工具? 排查和诊断过程,就是数据处理过程. 有时候只要处理过程探索到一个信号出现了就足以确定致因和解决问题了,至少也会得到进一步的线索.比如,客户ECS并发访问量上不去,如果发现对进程能打开多少文件描述符有限制.那么,我们差不多就确定了致因,对于如何解决问题也有了答案. 我们可以把上面排查和诊断的过程规范化为基于数据回答问题的过程.对这个过程,我们的预期是找到一个信号,这个信号可以确定致因,同时也对解决问题给出线索或者依据.比如,上面的例子,我们就是基于ECS的系统日志.业务日

《 FreeSWITCH权威指南》——第3章 初识FreeSWITCH3.1 什么是FreeSWITCH ?

第3章 初识FreeSWITCH 在前面几章,我们用了很大的篇幅介绍了电话通信的背景和基础知识,以及电信业务的知识.对于刚刚跨入通信(或电信)领域的读者来说,熟悉这些背景知识以及里面提到的各种名词术语,有助于理解后面要学到的知识.通信领域涉及的面非常广泛,可以说,里面的很多术语或知识点单独拿出来都可以写成一章或一本书.我们本书的重点是FreeSWITCH,因此从本章开始,我们正式进入FreeSWITCH的学习.学习本书的好处在于,即使你对前两章的内容不是很了解,也可以通过对FreeSWITCH的

开心网程炳皓:月平均广告收入达700万元

中介交易 SEO诊断 淘宝客 云主机 技术大厅 11月5日上午消息,开心网CEO程炳皓近日首次接受台湾媒体<商业周刊>专访时表示,开心网团队已从最初的6人增加到80人,一个月广告收入平均达人民币700万元,不过尚未实现盈利. 以下为专访部分实录: 在全球最大社交网站Facebook的"开心农场"风靡全台湾同时,中国内地也掀起一波相同的热潮,只是,他们用的不是美国人开发的洋玩意,而是中国本土开发出来的"开心网". 没打广告.没有富爸爸-- 网站用户数十八个

汪潮涌:投资家是怎样炼成的

东星航空破产案,让算不上主角的汪潮涌再度"热"了起来,这连他本人都没预料到.汪潮涌被称为国内投资界的"教父级"人物,从北京到华尔街,再从华尔街回到北京,他的人生与事业双双成功,精彩纷呈且充满故事 不管愿不愿意,汪潮涌又一次成为镁光灯下的焦点. 介入东星航空破产案近两个月来,汪潮涌成全国为各大媒体密集追踪的对象,火暴程度堪比他所投资的"中国之队"酣战第32届"美洲杯"帆船赛时的情景. 在执行本期选题过程中,记者先后两次采访了汪潮