深入Weex系列(九)Weex SDK可借鑑細節總結

頭條祁同偉發表於2019-02-19

1、前言

經過前面五篇文章的原始碼分析及總結,我們對Weex的整體架構及核心原始碼都有了清晰的認識。本篇文章主要總結我在Weex SDK原始碼閱讀時覺得可以借鑑的細節。

備註:本文側重講Weex SDK原始碼級別的可借鑑細節,對大方向上的可借鑑點比如動態化+Native思路、一項技術完整的生態等方面可以參考上一篇文章《深入Weex系列(八)之Weex SDK架構分析》

2、建造者模式

在使用Weex之前我們都會進行Weex SDK的初始化,對於Weex SDK它的輔助配置類就使用到了建造者模式。

建造者模式主要解決:一個模組各個部分子物件的構建演算法可能變化,但是各個部分子物件相互結合在一起的演算法確實穩定的。一句話總結就是:模組整體構建過程穩定,但是構建的每一步可能有出入。

我們結合Weex的場景來具體分析下:Weex配置模組的構建過程是穩定的(都需要提供同樣的能力),但是構建的每一步則可能有出入(每個配置的能力提供卻可以多樣)。

舉例說明:例如Weex需要提供網路請求的基礎能力(這個構建過程穩定),但是網路請求可以有不同的實現方式(具體的構建演算法可能變化)。

InitConfig config = new InitConfig.Builder().
        setImgAdapter(new WeexImageAdapter()).
        setHttpAdapter(new WeexHttpAdapter).
        setSoLoader(new WeexSoLoaderAdapter).
        build();
複製程式碼

好處:呼叫者無需知道構建模組如何組裝,也不會忘記組裝某一部分,同時也提供給了開發者定製的能力。

3、So的載入

So的成功載入對Weex的執行至關重要,畢竟Weex需要V8引擎執行Js與Native的互動,原始碼中也可以看出So沒有載入成功則Weex的各個模組不會執行。

而線上上Bug收集中我們會遇到UnsatisfiedLinkError錯誤,雖然不是頻發性Bug,但是對於Weex而言一旦出現那麼Weex就不可能再執行。於是Weex SDK對So載入這塊做了優化,我們看下So載入的程式碼邏輯:

public static boolean initSo(String libName, int version, IWXUserTrackAdapter utAdapter) {
    String cpuType = _cpuType();
    if (cpuType.equalsIgnoreCase(MIPS) ) {
      return false; // mips架構不支援,直接返回
    }

    boolean InitSuc = false;
    if (checkSoIsValid(libName, BuildConfig.ARMEABI_Size) ||checkSoIsValid(libName, BuildConfig.X86_Size)) {  // 校驗So大小是否正常
      /**
       * Load library with {@link System#loadLibrary(String)}
       */
      try {
        // If a library loader adapter exists, use this adapter to load library
        // instead of System.loadLibrary.
        if (mSoLoader != null) {
          mSoLoader.doLoadLibrary(libName);// 自定義SoLoader載入的話自己去載入
        } else {
          System.loadLibrary(libName);// 預設載入的方式
        }
        commit(utAdapter, null, null);

        InitSuc = true;
      } catch (Exception | Error e2) {// So載入失敗
        if (cpuType.contains(ARMEABI) || cpuType.contains(X86)) {
          commit(utAdapter, WXErrorCode.WX_ERR_LOAD_SO.getErrorCode(), WXErrorCode.WX_ERR_LOAD_SO.getErrorMsg() + ":" + e2.getMessage());
        }
        InitSuc = false;
      }

      try {
        if (!InitSuc) {
          // 沒有載入成功的話則從檔案中載入
          //File extracted from apk already exists.
          if (isExist(libName, version)) {
            boolean res = _loadUnzipSo(libName, version, utAdapter);// 從解壓包中載入So
            if (res) {
              return res;
            } else {
              //Delete the corrupt so library, and extract it again.
              removeSoIfExit(libName, version);// 解壓包也載入失敗,刪除;
            }
          }

          //Fail for loading file from libs, extract so library from so and load it.
          if (cpuType.equalsIgnoreCase(MIPS)) {
            return false;
          } else {
            try {
              InitSuc = unZipSelectedFiles(libName, version, utAdapter);// 從apk中解壓出來So,然後載入;
            } catch (IOException e2) {
              e2.printStackTrace();
            }
          }
        }
      } catch (Exception | Error e) {
        InitSuc = false;
        e.printStackTrace();
      }
    }
    return InitSuc;
}
複製程式碼

可以看到Weex中有多項保障去保證So的成功載入,總結下流程圖:

Weex中So載入流程圖

4、Weex的執行緒模型

各位老司機都知道多執行緒的好處也知道Android只有主執行緒才能更新UI,對於Weex來說它有自己完整的一套工作機制,如果所有任務都在主執行緒那勢必會積壓太多工,導致任務得不到及時執行同時也有卡頓的風險。

Weex SDK也考慮到了這些,分析Weex的機制可以知道任務主要花費在三方面:JSBridge相關、Dom相關、UI相關。於是對這三方面進行了細分,JSBridge相關的操作挪到JSBridge執行緒執行,Dom相關操作在Dom執行緒執行,避免了主執行緒積壓太多工。此處我們可以想到使用非同步執行緒。同時對於單項的任務例如Dom操作,需要是序列的。如果使用執行緒池,實際上也發揮不出執行緒池的威力。

分析到了這裡。我們的需求其實就很明確了:避免非同步執行緒的建立及銷燬過程消耗資源,同時支援序列執行。我們可以設想一種執行緒能力:有任務的時候則執行,沒有任務的時候則等待,是不是完美的符合我們的需求。

幸運的是Android其實已經為我們提供了這樣的一個類:HandlerThread。大家可以參考我之前的一篇文章《Android效能優化(十一)之正確的非同步姿勢》

    // 貼出Weex中使用的HandlerThread例項
    // JSBridge工作的Thread
    mJSThread = new WXThread("WeexJSBridgeThread", this);
    mJSHandler = mJSThread.getHandler();
    
    // Dom工作的Thread
    mDomThread = new WXThread("WeeXDomThread", new WXDomHandler(this));
    mDomHandler = mDomThread.getHandler();
複製程式碼

總結下Weex的執行緒模型:

  • JSBridge在WeexJSBridgeThread負責JS與Native的通訊;
  • 切換具體的Dom指令到WeeXDomThread負責關於Dom的各項如:解析、Rebuild Dom Tree、Layout等操作;
  • 切換到UI執行緒,負責原生View的建立、佈局、事件新增、資料繫結等;

優勢:

  • 避免主執行緒的卡頓風險;
  • 避免了執行緒的建立與銷燬等資源消耗;
  • 同時支援序列操作;

5、互動函式引數型別的處理

對於Weex的RunTime,再怎麼強大也少不了與Native的互動(方法呼叫,使用Native的能力),前面的系列文章也詳細分析了Module的互動原理。但是有一個細節問題前面沒有說到,就是JS與Native互動的方法簽名,引數型別只能是String嗎?

回到WXBridge這個通訊的橋樑,呼叫Native的方法都會走到callNative方法,然後走到WxBridgeManager.callNative方法,會發現函式體內有一行:

    JSONArray array = JSON.parseArray(tasks);
複製程式碼

由此可以斷定JS傳遞給Native的引數首先不僅僅是普通String字串,而是Json格式。實際上不管是斷點檢視或者翻閱WXStreamModule的程式碼,都可以發現Json的蹤影。

  @JSMethod(uiThread = false)
  public void fetch(String optionsStr, final JSCallback callback, JSCallback progressCallback){
        JSONObject optionsObj = null;
        try {
          optionsObj = JSON.parseObject(optionsStr);
        }catch (JSONException e){
          WXLogUtils.e("", e);
        }
        ......
    }
複製程式碼

不過以上發現還不足以解決我們的疑惑:引數型別只能是String嗎?那必須不是!

首先回顧下在Module的註冊過程中會有一步是獲取Module中被打上註解的方法然後存在mMethodMap中;而在真正呼叫方法的地方是NativeInvokeHelper的invoke方法:

  public Object invoke(final Object target,final Invoker invoker,JSONArray args) throws Exception {
    final Object[] params = prepareArguments(invoker.getParameterTypes(),args);// 解析引數
    if (invoker.isRunOnUIThread()) {// 要求在主執行緒執行則拋到主執行緒執行;
      WXSDKManager.getInstance().postOnUiThread(new Runnable() {
        @Override
        public void run() {
          try {
            invoker.invoke(target, params);// 反射呼叫方法執行
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        }
      }, 0);
    } else {
      return invoker.invoke(target, params);
    }
    return null;
  }
複製程式碼

我們再來詳細跟蹤下解析引數這步:

  private Object[] prepareArguments(Type[] paramClazzs, JSONArray args) throws Exception {
    Object[] params = new Object[paramClazzs.length];
    Object value;
    Type paramClazz;
    for (int i = 0; i < paramClazzs.length; i++) {
      paramClazz = paramClazzs[i];
      if(i>=args.size()){
        if(!paramClazz.getClass().isPrimitive()) {
          params[i] = null;
          continue;
        }else {
          throw new Exception("[prepareArguments] method argument list not match.");
        }
      }
      value = args.get(i);
      // JSONObject與JSCallback型別單獨處理
      if (paramClazz == JSONObject.class) {
        params[i] = value;
      } else if(JSCallback.class == paramClazz){
        if(value instanceof String){
          params[i] = new SimpleJSCallback(mInstanceId,(String)value);
        }else{
          throw new Exception("Parameter type not match.");
        }
      } else {
        // 其它型別的引數
        params[i] = WXReflectionUtils.parseArgument(paramClazz,value);
      }
    }
    return params;
  }
複製程式碼

看下其它引數型別的解析:

  public static Object parseArgument(Type paramClazz, Object value) {
    if (paramClazz == String.class) {
      return value instanceof String ? value : JSON.toJSONString(value);
    } else if (paramClazz == int.class) {
      return value.getClass().isAssignableFrom(int.class) ? value : WXUtils.getInt(value);
    } else if (paramClazz == long.class) {
      return value.getClass().isAssignableFrom(long.class) ? value : WXUtils.getLong(value);
    } else if (paramClazz == double.class) {
      return value.getClass().isAssignableFrom(double.class) ? value : WXUtils.getDouble(value);
    } else if (paramClazz == float.class) {
      return value.getClass().isAssignableFrom(float.class) ? value : WXUtils.getFloat(value);
    } else {
      return JSON.parseObject(value instanceof String ? (String) value : JSON.toJSONString(value), paramClazz);
    }
  }
複製程式碼

跟蹤到此處就顯而易見:JS與Native的互動引數不僅僅支援String。

我們再來總結下Weex是如何實現不同方法簽名的互動的:

  • Module註冊階段儲存下來Method;
  • JS傳送指令呼叫Module方法傳遞的原始引數是Json格式;
  • 真正反射呼叫方法的時候從Method中拿到引數的具體型別,然後從Json中讀到相應的值,再進行轉換。

6、後記

本文主要記錄了我在Weex原始碼閱讀過程中覺得不錯可以借鑑的細節,限於文章篇幅不能面面俱到。實際上不僅Weex的整體思路,Weex SDK的程式碼也非常優秀,非常建議大家仔細閱讀,學習優秀的原始碼對自己的編碼能力會有一定程度的提升!

歡迎持續關注Weex原始碼分析專案:Weex-Analysis-Project

歡迎關注微信公眾號:定期分享Java、Android乾貨!

歡迎關注

相關文章