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的成功載入,總結下流程圖:
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乾貨!