Weex SDK Android 源碼解析

Weex SDK Android 源碼解析

Weex 是一套構建高性能、可擴展的原生應用跨平台開發方案。一次編寫,多端運行。對於前端是個與客戶端融合的最方便的路徑,透過客戶端的模塊封裝可以快速的讓前端突破瀏覽器的限制,將客戶端能力發威到極限,因此除了業務層的深入外,對於 SDK 框架本身也必須更進一步的了解。

本篇主要是從分析代碼入手,探討一下Weex在安卓平台上是如何構建一套JS的運行框架。主要討論的範疇包括:線程模型、渲染流程、Component/Module的註冊流程以及 Weex 中 JNI 的調試技巧。

本文目錄結構如下:

------1. 整體架構

------------1.1 線程模型

----------------1.1.1 結構圖

----------------1.1.2 線程間通信

----------------1.1.3 線程異常處理

----------------1.1.4 線程清理

----------------1.1.5 @JSMethod 的使用

------------1.2 渲染模型

----------------1.2.1 結構圖

----------------1.2.2 Native 中布局方式

----------------1.2.3 FlexBox 概念說明

----------------1.2.4 CSSNode/WXDomObject 方法說明

----------------1.2.5 ViewPort的使用

------------1.3 Component/Module 註冊流程

----------------1.3.1 結構圖

----------------1.3.2 Component 註冊及使用方式

----------------1.3.3 Module 註冊及使用方式

----------------1.3.4 DomObject 註冊及使用方式

------2. jni 調試技巧

整體架構

整個運行框架包括三大部分:JS Bridge、Render、Dom,這三大部分都包含在 WXSDKManager 中。WXBridgeManager、WXRenderManager、WXDomManager 都可以通過WXSDKManager 獲取。

  1. JS Bridge:主要用來和 JS Engine(V8)進行雙向通信,運行在JSBridge線程中。Weex 的初始化,Component、Module、DomObject的註冊與調用,JSBridge 線程管理最終都會由JS Bridge 的管理類 WXBridgeManager 完成。所有和 Dom 相關的操作都會通知到 Dom 線程,交由 WXDomModule 處理。
  2. Render:主要用來操作具體的Native View,包括管理Native View的各種操作(添加/刪除Component,構造Component Tree等)、Native View的布局等,運行在UI線程中。由 WXRenderManager 統一管理,具體操作由 WXRenderStatement 管理,每一個weex instance 一一對應一個 WXRenderStatement。WXRenderStatement 具體就是操作 WXComponent。
  3. Dom:主要用來操作Dom結構,包括生成對應的Dom Tree,添加/刪除Dom 節點(WXDomObject)等操作,運行在獨立的 Dom 線程中。由 WXDomManager 統一管理,具體操作由 WXDomStatement 管理,每一個weex instance 一一對應一個 WXDomStatement。WXDomStatement 具體就是操作 WXDomObject。所有的 Dom 操作(包括CSSLayout的計算)都在 Dom 線程中,完成後會通知UI線程處理對應的Native Component View。

線程模型

線程模型圖

在處理複雜邏輯的情況下,使用線程是必不可少的,使UI線程不會積壓太重的任務,導致界面卡頓。這時候又遇到另一個問題,線程是一次性的消耗品,使用完了線程就自動退出銷毀了,假如有比較多的耗時任務,不得不重新創建線程去執行該耗時任務,就會存在性能問題:多次創建和銷毀線程是很耗系統資源的。為了解這種問題,我們可以自己構建一個循環線程Looper Thread,當有耗時任務投放到該循環線程中時,線程執行耗時任務,執行完之後循環線程處於等待狀態,直到下一個新的耗時任務被投放進來。這樣一來就避免了多次創建Thread線程導致的性能問題了。Android SDK中其實已經有一個循環線程的框架-HandlerThread,Weex 中的線程使用的正是HandlerThread,如何使用HandlerThread可以參看官方文檔。

Weex中的線程:

  1. JSBridgeThread:用來java jni層和v8 engine之間進行通信,包括初始化js framework、callJS、callNative等。
  2. DomThread:用來進行Dom操作,包括Dom解析、設置Dom樣式、CSS Layout操作、生成Component Tree等操作。圖中可知 DomThread 中的操作都是v8 engine調用上來的,也就是說是js runtime生成dom的各種操作,一旦js bundle過大,會是一個瓶頸。
  3. UIThread:用來真正的視圖渲染,包括設置View Layout、設置View Padding、綁定數據、Add/Remove View等操作。

通信:

  1. 通信方式:三個線程之間的通信方式使用的都是正常的Android Handler通信機制,每個線程中的所有操作都是時序性的(也是Handle的機制決定),保證了操作Dom的時序性。
    • 使用 runnable 方式

      Message m = Message.obtain(mJSHandler, WXThread.secure(r));nm.obj = token;nm.sendToTarget();n

    • 使用 handleMessage 方式

      Message msg = Message.obtain();nWXDomTask task = new WXDomTask();n…nmsg.what = WXDomHandler.MsgType.WX_DOM_CREATE_BODY;nmsg.obj = task;nWXSDKManager.getInstance().getWXDomManager().sendMessage(msg);n

  2. 通信流程
    • UIThread 與 JSBridgeThread: JSBridgeThread 不會直接發送任務給 UIThread , UIThread 發送給 JSBridgeThread 的任務有初始化js framework、開始渲染頁面createInstance、發送event事件等。

    • UIThread 與 DomThread: UIThread 會在銷毀instance的時候發送任務給 DomThread 進行清理,DomThread 發送任務給 UIThread 會分為兩步,這兩步會是一個task:
      1. 發送前會重新計算CSSLayout的耗時操作,這部分的操作是在DomThread中進行。
      2. 發送 runnable 到 UIThread,runnable執行的就是view的渲染流程,在UIThread中進行。

      說明: 這一整個task是每隔16ms自動觸發,也是說一旦dom操作過多,就會拖累幀率。

    • JSBridgeThread 與 DomThread:DomThread不會直接發送任務給JSBridgeThread 。js runtime會通過jni發送指令到 java 層,這一部分在JSBridgeThread中,然後JSBridgeThread會發送任務給 DomThread 進行各種 Dom 操作。

線程異常

在Weex中線程處理有一個專門的類WXThread,裡面分裝了HandlerThread處理任務的兩種方式:SafeRunnable、SafeCallback,並且 try catch 了所有的異常,以保證在處理過程中異常crash。

線程清理

正常情況下頁面退出,是不是應該把所有的線程清理(quit)呢?weex中的做法是NO,想必是為了提高創建、銷毀線程消耗系統資源的效率。Weex中WXSDKManager 、WXBridgeManager是單例,WXRenderManager 、WXDomManager的獲取都是通過WXSDKManager,在WXSDKInstance destroy 的時候並不會銷毀單實例,因此在多次Weex頁面進出的時候線程是重用的。

@JSMethod

在WXComponent、WXModule中可以使用 @JSMethod 註解來提供Native方法給JS調用,這個註解有uiThread這個方法,默認值為true 參數說明: 1. 如果uiThread = true,則在UIThread中執行 2. 否則在JSBridgeThread中執行

總結

  1. 雖說使用了線程,其實都是線性的在執行,只不過把繁重的任務讓線程執行了,這也和js runtime中dom解析邏輯、順序有關。
  2. 看完了Weex中的線程模型,是不是還是很簡單的,沒有那麼複雜,有木有~

渲染

結構圖

以添加dom節點來說明整個渲染過程,步驟1可以換成其他dom操作,比如update、remove等操作。步驟2則是通用的。

  1. createBody/addDom: WXDomStatement,從 dom tree 到 component tree 的映射
    • WXDomObject.parse() 遞歸解析dom JSONObject,最終得到當前dom樹結構
      1. 實例化WXDomObject

      2. 設置viewport

      3. 解析dom JSONObject,得到type、ref、style、attr、event

        {n "attr":{"spmId":"spma"},n "ref":"_root",n "style":{},n "type":"div"n}n

      4. 賦值 domObject.mDomContext = sdkInstance

    • 若為 root 節點
      • prepareRoot
        • 若沒有設置style flexDirection 與 backgroundColor,則設置默認值 column、#ffffff
        • 設置style defaultWidth、defaultHeight
    • 普通節點:parent.add(domObject) 把當前解析得到的dom樹加到父節點,並且把當前節點和父節點置為 dirty。

    • traverseTree 遍歷當前dom節點
      • 註冊得到所有dom節點到 mRegistry 中,標記為 young
      • 檢查root節點是否為fixed節點,把fixed的節點存到root dom object 內
      • apply所有的 style 到 CSSNode
    • 遞歸創建Component Tree

    • 添加 createBody 或者 addDom 任務到 renderTask中

    • 代碼如下:

      private void addDomInternal(JSONObject dom,boolean isRoot, String parentRef, final int index){n ……n //only non-root has parent.n WXDomObject parent;n WXDomObject domObject = WXDomObject.parse(dom,instance);n ……n if (isRoot) {n WXDomObject.prepareRoot(domObject, WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexHeight(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId)), WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexWidth(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId)));n } else if ((parent = mRegistry.get(parentRef)) == null) {n instance.commitUTStab(IWXUserTrackAdapter.DOM_MODULE, errCode);n return;n } else {n //non-root and parent existn parent.add(domObject, index);n }n domObject.traverseTree( mAddDOMConsumer, ApplyStyleConsumer.getInstance());n //Create component in dom threadn WXComponent component = isRoot ?n mWXRenderManager.createBodyOnDomThread(mInstanceId, domObject) :n mWXRenderManager.createComponentOnDomThread(mInstanceId, domObject, parentRef, index);n ……n AddDomInfo addDomInfo = new AddDomInfo();n addDomInfo.component = component;n mAddDom.put(domObject.getRef(), addDomInfo);n IWXRenderTask task = isRoot ? new CreateBodyTask(component) : new AddDOMTask(component, parentRef, index);n mNormalTasks.add(task);n addAnimationForDomTree(domObject);n mDirty = true;n ……n }n

  2. layout:WXDomStatement,由 batch 驅動(每隔16ms執行一批任務,開始渲染)
    • 把所有fixed的節點移到 root 節點 child 內

    • CSSLayout 計算整個 dom 樹(calculateLayout),耗時操作,dom 樹即為mRegistry中註冊的所有節點

    • 遍歷 mRegistry 中所有節點
      1. 設置 markLayoutSeen

      2. applyUpdate:如果當前節點已經被消費,則post message到渲染的 UIThread 更新Component hostView 的 LayoutParams(更新上一幀已經被消費過的節點的LayoutParams)。

        為什麼需要這個步驟?正常情況下在renderTask中對Component View setLayout就可以了,但是這要基於一個前提,那就是所有的 Dom 節點都已經被註冊到 mRegistry 中了,只有這樣最後 CSSLayout 計算出來的才是正確的。 由於 batch 驅動的不確定性(有可能分好幾幀),非常有可能在本次 Layout 過程中 Dom 數量是不完整的,導致 CSSLayout 計算的結果肯定是不完整的。因此需要每次batch 重新計算完整 dom 樹的時候把之前節點 Layout 再更新一遍,並且此時的節點必為 old(上一幀被標為old)

    • 更新 mRegistry 中計算好的所有節點到Component Tree中的 Dom 節點,並且置為 old,下次 layout 時需要更新Component 的 LayoutParams。(如果只有一幀,就沒有也不需要下次更新的機會了)

    • 執行renderTasks:由一系列 js bridge 傳過來的各種指令,包括Dom操作(createBody、addDom、updateStyle等)、createFinish、updateFinish事件等引起的執行dom對應的componet view操作,這些操作都會被添加到renderTasks中,並且這些任務由 batch 驅動。因為對應的是UI操作,都需在UIThread中執行。
      • 示例:createBody、addDom
        • createView 如果有parent則add到父View
        • applyLayoutAndEvent:Component setLayout,更新Component hostView 的LayoutParams。
        • bindData
    • 代碼如下:

      //WXDomStatement.javanvoid layout(WXDomObject rootDom) {n ……n rebuildingFixedDomTree(rootDom);n rootDom.calculateLayout(mLayoutContext);n ……n rootDom.traverseTree(new ApplyUpdateConsumer());n updateDomObj();n parseAnimation();n int count = mNormalTasks.size();n for (int i = 0; i < count && !mDestroy; ++i) {n mWXRenderManager.runOnThread(mInstanceId, mNormalTasks.get(i));n }n mNormalTasks.clear();n mAddDom.clear();n animations.clear();n mDirty = false;n ……n }n

  3. 最終生成的root component 會被添加到 RenderContainer

布局方式:

最終進行布局都會進入函數 applyLayoutAndEvent,layout操作會有兩步:

  1. setLayout:設置當前view的寬、高,以及 margin 值
    • width:getLayoutWidth() CSSLayout 計算得到的寬
    • height:getLayoutHeight() CSSLayout 計算得到的高
    • left:margin left,當前節點相對於父節點的X坐標 - parent的padding值(包括bording值)
    • top:margin top,同 left 計算方式
    • right:margin right,直接使用CSSLayout計算得到的值
    • bottom:margin bottom,同right 計算方式
  2. setPadding:使用 CSSLayout 計算得到的 padding 值設置當前 view 的 padding 值

其實布局就是設置當前 View LayoutParams 的padding、margin值

FlexBox 概念說明

  • Flex direction:FlexDirection 控制 children 的排布方向,並且這個屬性標實為主軸方向,有四個可選值:
    • Column(默認):主軸方向從上到下排布,垂直的軸即為從左到右
    • Row:主軸方向從左到右排布,垂直的軸即為從上到下
    • ColumnReverse:和 Column 相反,在 RTL 布局下使用
    • RowReverse:和 Row 相反,在 RTL 布局下使用
  • Justify content:JustifyContent 控制在一個容器內主軸方向上 children 的排列方式,比如當 FlexDirection = Row 時,可以用這個屬性控制 children 水平居中。有五個可選值:
    • JustifyContent = FlexStart (默認)

    • JustifyContent = FlexEnd

    • JustifyContent = Center

    • JustifyContent = SpaceBetween

    • JustifyContent = SpaceAround

  • Flex wrap:FlexWrap 控制容器內的 children 超出容器時的排布方式。有兩個可選值:
    • FlexWrap = Nowrap:

    • FlexWrap = Wrap:如果FlexDirection = Row 時則往下排放,如果FlexDirection = Column時則往右排放。

  • Alignment:AlignItems 控制在一個容器內垂直軸上 children 的排列方式。和 JustifyContent 有點類似,但是方向上正好相反。有四個可選值:
    • Stretch(默認):在垂直軸上拉升 children 的大小與容器匹配。
    • FlexStart:排列在垂直軸上的開始位置
    • FlexEnd:排列在垂直軸上的末尾位置
    • Center:排列在垂直軸上的中間位置
  • Flex:FlexGrow 控制在主軸上 children 在剩餘的空間如何被分布。
    • FlexGrow = 1

  • Margin、Padding、Border:Margin 與 Padding 有點類似,但是又有比較大的區別。Margin 相對於父節點或者兄弟節點的邊距,而 Padding則是指父容器內 children 的邊距。Border 則和 Padding 概念基本一致,主要是用來區別 border 效果的大小。
    • MarginStart = 50

    • MarginEnd = 50

    • MarginAll = 50

    • PaddingAll = 50

    • BorderWidth = 50

WXDomObject/CSSNode 方法說明

  1. young:標實當前節點是否被消費過,並且只有在第一次註冊是標記為 young。消費過即此節點 CSSLayout 計算完成,並且更新到Component中的DomObject,此後此節點必為 old
  2. needUpdate:是否需要更新
  3. markHasNewLayout:每次 CSSLayout 計算完成,都會標實 LayoutState 為 LayoutState.HAS_NEW_LAYOUT
  4. markLayoutSeen:每次 CSSLayout 計算完成之後都要調用這個方法,用來標實當前節點的 CSSLayout 值可以被使用了。如何當前節點沒有調用 markLayoutSeen 被 dirty,會拋異常,說明之前的CSSLayout計算還沒被使用過。
  5. addChild:添加子節點,並且 置為 dirty(自己和父節點)
  6. getLayoutX/Y():相對於父節點的x、y坐標
  7. getPadding():當前節點l、t、r、b的padding距離
  8. getBorder():當前節點l、t、r、b的border距離
  9. getMargin():當前節點l、t、r、b的margin距離

ViewPort

之前框架支持的布局策略是 flexable 布局,在不同屏寬下保持相同 ViewPort(默認 750),整體縮放,可以解決大部分屏幕自適應問題。但是在某些情況下無法滿足,比如想讓某些元素在不同屏寬手機中都保持相同大小,畫一像素的線等。目前weex 支持可配置的響應式布局,自定義設置 ViewPort 值。

使用方式

<script type="config">n {n "viewport": {n "width": "device-width"n }n }n</script>n

Component/Module 註冊流程

Component 和 Module 是 Weex 中主要的兩種與 js 交互的載體,支持與 js 雙向通信。也是 Weex 擴展的兩種方式,具體的擴展方式可參考這兩篇文章 Weex Android/iOS 擴展指南 ,這篇文章主要介紹 Weex Android Component Module 的註冊流程,以及懶載入以提高初始化效率。

註冊流程

類結構圖

註冊類型主要有:Component、Module、DomObject,註冊入口統一在 WXSDKEngine 中

Component

  • 註冊方式 入口都是從 WXSDKEngine 開始

    //WXSDKEngine.javanpublic static boolean registerComponent(String type, Class<? extends WXComponent> clazz, boolean appendTree) throws WXException {n return registerComponent(clazz, appendTree,type);n }n

    最終的註冊到 WXComponentRegistry.java

    public static boolean registerComponent(final String type, final IFComponentHolder holder, final Map<String, Object> componentInfo) throws WXException {n……n WXBridgeManager.getInstance()n .post(new Runnable() {n @Overriden public void run() {n try {n Map<String, Object> registerInfo = componentInfo;n if (registerInfo == null){n registerInfo = new HashMap<>();n }n registerInfo.put("type",type);n registerInfo.put("methods",holder.getMethods());n registerNativeComponent(type, holder);n registerJSComponent(registerInfo);n sComponentInfos.add(registerInfo);n } catch (WXException e) {n WXLogUtils.e("register component error:", e);n }n }n });n return true;n }n

    • 可以看到註冊分為兩部分,這兩部分都是在 bridge 線程中執行:

      1. native 部分註冊:記錄component 的 type 與 holder 的 map 映射。holder 默認為 SimpleComponentHolder,主要用來:如果不為懶載入(lazyLoad),會提前解析好註解 @WXComponentProp 和 @JSMethod,否則會等到使用的時候才會去解析註解,可以提高 weex 的初始化效率。

      2. js 部分註冊:把 component 的 type 、methods 信息註冊到 js runtime中,可以看到最終的註冊部分在 WXBridgeManager 中

        private void invokeRegisterComponents(List<Map<String, Object>> components) {n……n WXJSObject[] args = {new WXJSObject(WXJSObject.JSON,n WXJsonUtils.fromObjectToJSONString(components))};n try {n mWXBridge.execJS("", null, METHOD_REGISTER_COMPONENTS, args);n } catch (Throwable e) {n ……n }n }n

  • 使用方式 使用 WXComponentFactory 的 newInstance 方法生成 WXComponent。

    public static WXComponent newInstance(WXSDKInstance instance, WXDomObject node, WXVContainer parent) {n……n IFComponentHolder holder = WXComponentRegistry.getComponent(node.getType());n……n try {n return holder.createInstance(instance, node, parent);n } catch (Exception e) {n WXLogUtils.e("WXComponentFactory Exception type:[" + node.getType() + "] ", e);n }n n return null;n }n

    通過holder (默認為 SimpleComponentHolder )的 createInstance 方法生成 WXComponent 實例。

Module

Module 的註冊方式與使用方式和 Component 類似,入口從 WXSDKEngine 的方法 registerModule 開始。最終通過 WXModuleManager 註冊,也分為 native 與 js 兩部分註冊,註冊的類型會區分是否是全局,如果是全局的則會提前實例化好。

  • 使用方式 通過 WXModuleManager 的 callModuleMethod 方法使用 WXModule,調用者都是從 WXBridgeManager 發起(js端發起)。

    static Object callModuleMethod(final String instanceId, String moduleStr, String methodStr, JSONArray args) {n ModuleFactory factory = sModuleFactoryMap.get(moduleStr);n……n final WXModule wxModule = findModule(instanceId, moduleStr,factory);n if (wxModule == null) {n return null;n }n WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);n wxModule.mWXSDKInstance = instance;n final Invoker invoker = factory.getMethodInvoker(methodStr);n try {n return instancen .getNativeInvokeHelper()n .invoke(wxModule,invoker,args);n } catch (Exception e) {n WXLogUtils.e("callModuleMethod >>> invoke module:" + moduleStr + ", method:" + methodStr + " failed. ", e);n return null;n } finally {n……n }n }n

    步驟:

    1. 實例化 WXModule,會先從全局的Module中找,未找到則通過 ModuleFactory 的 buildInstance 方法實例化。
    2. 調用 WXModule 的方法

DomObject

註冊 DomObject主要是為了自定義 WXDomObject 類,默認的 Component 對應的domObject 都為 WXDomObject。

  • 註冊方式:從 WXSDKEngine 的方法 registerDomObject 開始,最終會從 WXDomRegistry 中註冊。
  • 獲取方式:使用場景是在native端生成dom樹的時候
    1. 從 WXDomRegistry 中獲取 DomObject 的 class。
    2. 從 WXDomObjectFactory 的方法 newInstance 實例化WXDomObject

jni 調試技巧

Android Weex 中使用的 Javascript 引擎為 google 的 V8 引擎,V8 由 C++ 寫,在 C++ 與 Java 之間需要JNI進行橋接,普通的JNI的調試方法只能打log輸出,重新 ndk build so 動態庫文件,重新跑APK程序,比較繁瑣,不能直觀的動態debug。有一個好消息是最新的Android Studio支持了Android Native 調試,下面就來介紹下如何在Android Studio中動態調試、斷點Weex Native 代碼。

  1. 在 SDK Manager 中的 SDK Tools 安裝CMake、LLDB、NDK。

  2. 在 Weex SDK 項目中的 build.gradle 增加如下配置

    android {n externalNativeBuild {n ndkBuild {n path ../../../weex_v8core/jni/Android.mk // v8core的具體路徑n }n }n}n

  3. 把 v8core 目錄下build出來的v8core/obj/local/armeabi/libweexv8.so 拷貝到weex sdk目錄libs下對應的文件夾中,這一步尤為關鍵,這個 so 文件是靜態文件,不是動態庫文件,裡面包含了所有符號,因此可以調試debug。

推薦閱讀:

安卓手機的照片被誤刪能恢復嗎?
Android 如何簡單集成 Emoji 鍵盤
無需Root也能使用Xposed!
實戰kotlin@android(三): 擴展變數與其它技巧

TAG:Weex | Android | Android开发 |