React-Native系列Android——Native與Javascript通訊原理(三)
前面兩篇部落格,詳細分析了Native與Javascript通訊的過程,可以滿足絕大部分情境下Native和Javascript的相互調用,但是仍然有不健全的情況。
比如Javascript層要即時擷取Native的一些狀態,就需要Native被動地向Javascript層通訊了。這個過程區別於通訊第一篇中Native主動向Javascript層通訊,本篇部落格就來研究下這樣一個被動回調的過程!
首先,從一個常用的情境開始分析。
假設前端開發人員在Javascript的代碼中想要擷取APP的狀態,比如APP是否是處於前台(active),還是後台(background)。大概有兩種實現方式:
1、Native 在APP每次狀態切換的時候,調用callFunction將最新的狀態傳給Javascript層,然後由Javascript緩衝起來,這樣開發人員想要擷取狀態可以能直接使用這個緩衝的值。
2、前端開發人員在Javascript中想要擷取狀態時,先向Native端發起通訊請求,表示想擷取狀態,然後由Native端把這個狀態作為通訊應答返給Javascript層。
這兩種方案都有各自的使用性情境,並且在React-Native都有相應實現。第一種實現對開發人員來說相對簡單,直接取緩衝值,是一個完全同步的過程。第二種實現向Native發起通訊請求,需要等待Native的應答,是一個非同步過程。
第一種方案實現原理在React-Native系列Android——Native與Javascript通訊原理(一)中已經詳細分析過了,不再贅述,本篇博文重點來分析下第二種方案的實現原理。
1、JavaScript的請求
Native與JavaScript的通訊,都是由Native主動發起,然後由JavaScript應答,但是JavaScript是無法向Native主動發起通訊的。那麼,JavaScript如何才能向Native發起通訊請求呢?
上一篇博文中講過,JavaScript應答Native是通過將應答資料封裝成JSON格式,然後在flushedQueue() 返給Bridge再返給Native的。如果JavaScript在這個應答資訊加入通訊請求的標識,那麼Native在解析應答資訊時發現了其中包含JavaScript的通訊標識,然後Native來應答這個請求,這樣不就完成了一次JavaScript請求Native的過程嗎?
第一步:在返給Native的應答資訊中加入JavaScript的通訊請求
同樣以在Javascript中擷取APP目前狀態為例,示範代碼如下:
var NativeModules = require('NativeModules');var RCTAppState = NativeModules.AppState;var logError = require('logError');RCTAppState.getCurrentAppState( (appStateData) => { console.log('dev', 'current state: ' + appStateData.app_state); }, logError);
前一篇博文中分析過NativeModules的前世今生,它是一個動態初始化的類(具體請看前篇Native與Javascript通訊原理(二),這裡略過),RCTAppState.getCurrentAppState實際上是調用的是MessageQueue.js的下面這段代碼:
function(...args) { let lastArg = args.length > 0 ? args[args.length - 1] : null; let secondLastArg = args.length > 1 ? args[args.length - 2] : null; let hasSuccCB = typeof lastArg === 'function'; let hasErrorCB = typeof secondLastArg === 'function'; hasErrorCB && invariant(hasSuccCB, 'Cannot have a non-function arg after a function arg.'); let numCBs = hasSuccCB + hasErrorCB; let onSucc = hasSuccCB ? lastArg : null; let onFail = hasErrorCB ? secondLastArg : null; args = args.slice(0, args.length - numCBs); return self.__nativeCall(module, method, args, onFail, onSucc);};
這裡面參數args具體化有兩個,一個是lambda運算式回呼函數,一個是logError,都是function類型。解析的時候lastArg變數指logError,secondLastArg變數指回呼函數。
所以調用__nativeCall函數時候傳遞的兩個參數onFail和onSucc,就分別指回呼函數和logError。這裡明顯是React-Native的命名bug了,差點以為是兩個變數解析顛倒了,不過不影響整個流程(原因是Native代碼中解析參數時預設是onSucc在前面,又顛倒回來了,後面會分析到)。
接下來看__nativeCall
__nativeCall(module, method, params, onFail, onSucc) { if (onFail || onSucc) { ... onFail && params.push(this._callbackID); this._callbacks[this._callbackID++] = onFail; onSucc && params.push(this._callbackID); this._callbacks[this._callbackID++] = onSucc; } ... this._queue[MODULE_IDS].push(module); this._queue[METHOD_IDS].push(method); this._queue[PARAMS].push(params); ... }
this._queue的作用上篇分析過,是用來儲存應答Native的資料的,這裡主要來看if裡面的判斷邏輯。
this._callbackID是作為this._callbacks集合的索引來標識回呼函數的,同時這個索引會放到params裡面傳遞給Native端,Native端應答的時候會將這個索引傳回到Javascript端,這樣Javascript端就能通過索引找到事先存放在this._callbacks集合裡的回呼函數了。所以,this._callbackID就是Javascript請求Native的標識了。
第二步:Native如何應答Javascript端
中間還有一步flushedQueue() 向Bridge層的傳遞過程,參考前文即可,這裡跳過。
前篇博文中分析過Native處理來自Javascript應答資訊,都是通過moduleID+methodID映射到具體NativeModule組件的方法,然後解析參數,最後通過invoke反射方式完成調用的。
例子中,擷取APP目前狀態的組件在Native端對應的NativeModule類是AppStateModule。被映射到的方法是getCurrentAppState,它有兩個Callback類型的參數。
來看看NativeModule解析Callback型別參數時的代碼,位於其父類com.facebook.react.bridge.BaseJavaModule.java中
static final private ArgumentExtractor ARGUMENT_EXTRACTOR_CALLBACK = new ArgumentExtractor() { @Override public @Nullable Callback extractArgument( CatalystInstance catalystInstance, ReadableNativeArray jsArguments, int atIndex) { if (jsArguments.isNull(atIndex)) { return null; } else { int id = (int) jsArguments.getDouble(atIndex); return new CallbackImpl(catalystInstance, id); } } };
private ArgumentExtractor[] buildArgumentExtractors(Class[] paramTypes) { ArgumentExtractor[] argumentExtractors = new ArgumentExtractor[paramTypes.length]; for (int i = 0; i < paramTypes.length; i += argumentExtractors[i].getJSArgumentsNeeded()) { Class argumentClass = paramTypes[i]; ... if (argumentClass == Callback.class) { argumentExtractors[i] = ARGUMENT_EXTRACTOR_CALLBACK; } ... } return argumentExtractors; }
對於Callback型別參數,使用的參數提取器是ARGUMENT_EXTRACTOR_CALLBACK,在其extractArgument方法裡面提取出由Javascript端傳來的callbackID,構造進CallbackImpl對象裡面。而這個構造出來的CallbackImpl對象,就是invoke反射getCurrentAppState方法裡的參數了。
下面來看一下被反射的getCurrentAppState方法,位於com.facebook.react.modules.appstate.AppStateModule.java
public class AppStateModule extends ReactContextBaseJavaModule implements LifecycleEventListener { public static final String APP_STATE_ACTIVE = "active"; public static final String APP_STATE_BACKGROUND = "background"; private String mAppState = "uninitialized"; public AppStateModule(ReactApplicationContext reactContext) { super(reactContext); } @Override public String getName() { return "AppState"; } @Override public void initialize() { getReactApplicationContext().addLifecycleEventListener(this); } @ReactMethod public void getCurrentAppState(Callback success, Callback error) { success.invoke(createAppStateEventMap()); } @Override public void onHostResume() { mAppState = APP_STATE_ACTIVE; sendAppStateChangeEvent(); } @Override public void onHostPause() { mAppState = APP_STATE_BACKGROUND; sendAppStateChangeEvent(); } ... private WritableMap createAppStateEventMap() { WritableMap appState = Arguments.createMap(); appState.putString("app_state", mAppState); return appState; } ...}
當Activity生命週期變化的時候,會更新狀態到mAppState,createAppStateEventMap()將mAppState封裝在用於Native-Bridge間傳遞的WritableMap對象中。然後調用了success.invoke(),而這個Callback類型的 success參數就是前面ARGUMENT_EXTRACTOR_CALLBACK構造出來的CallbackImpl對象了,它記憶體儲存著用於回調的標識callbackID。
所以,來看CallbackImpl的invoke方法,代碼在com.facebook.react.bridge.CallbackImpl.java
public final class CallbackImpl implements Callback { private final CatalystInstance mCatalystInstance; private final int mCallbackId; public CallbackImpl(CatalystInstance bridge, int callbackId) { mCatalystInstance = bridge; mCallbackId = callbackId; } @Override public void invoke(Object... args) { mCatalystInstance.invokeCallback(mCallbackId, Arguments.fromJavaArgs(args)); }}
其invoke方法裡面又調用了CatalystInstance.invokeCallback,通過前面兩篇博文我們知道CatalystInstance是Native向Javascript通訊的入口,那麼這裡很明顯其CatalystInstance.invokeCallback就是Native對Javascript的應答了。裡麵包含了標識callbackID和內容資料mAppState。
在CatalystInstance的實作類別CatalystInstanceImpl內部,又是通過ReactBridge調用JNI的,這一點同
React-Native系列Android——Native與Javascript通訊原理(一)中的callFunction原理完全一樣。
public class CatalystInstanceImpl implements CatalystInstance { ... public void invokeCallback(final int callbackID, final NativeArray arguments) { ... Assertions.assertNotNull(mBridge).invokeCallback(callbackID, arguments); ... } ...}
第三步:Bridge的中轉
上一步中通過JNI調用了invokeCallback方法,裡面有兩個參數:callbackID和arguments。callbackID是來自Javascript端的通訊回調標識,arguments是Native應答Javascript請求的內容。Bridge的作用就是將這兩個參數中轉到Javascript端。
Bridge層的調用入口是react\jni\OnLoad.cpp,先來瞧瞧invokeCallback方法
static void invokeCallback(JNIEnv* env, jobject obj, JExecutorToken::jhybridobject jExecutorToken, jint callbackId, NativeArray::jhybridobject args) { auto bridge = extractRefPtr(env, obj); auto arguments = cthis(wrap_alias(args)); try { bridge->invokeCallback( cthis(wrap_alias(jExecutorToken))->getExecutorToken(wrap_alias(jExecutorToken)), (double) callbackId, std::move(arguments->array) ); } catch (...) { translatePendingCppExceptionToJavaException(); }}
調用的又是CountableBridge即Bridge對象的invokeCallback方法,代碼在react\Bridge.cpp中
void Bridge::invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& arguments) { ... auto executorMessageQueueThread = getMessageQueueThread(executorToken); if (executorMessageQueueThread == nullptr) { ... return; } std::shared_ptr isDestroyed = m_destroyed; executorMessageQueueThread->runOnQueue([=] () { ... JSExecutor *executor = getExecutor(executorToken); if (executor == nullptr) { ... return; } ... executor->invokeCallback(callbackId, arguments); });}
在executorMessageQueueThread隊列線程裡面,執行的是JSExecutor的invokeCallback方法。
繼續來看react\JSCExecutor.cpp
void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { std::vector call{ (double) callbackId, std::move(arguments) }; std::string calls = executeJSCallWithJSC(m_context, "invokeCallbackAndReturnFlushedQueue", std::move(call)); m_bridge->callNativeModules(*this, calls, true);}static std::string executeJSCallWithJSC( JSGlobalContextRef ctx, const std::string& methodName, const std::vector& arguments) { ... // Evaluate script with JSC folly::dynamic jsonArgs(arguments.begin(), arguments.end()); auto js = folly::to( "__fbBatchedBridge.", methodName, ".apply(null, ", folly::toJson(jsonArgs), ")"); auto result = evaluateScript(ctx, String(js.c_str()), nullptr); return Value(ctx, result).toJSONString();}
這段代碼和callFunction非常相似,只不過executeJSCallWithJSC裡面第二個參數換成了invokeCallbackAndReturnFlushedQueue。
這一段產生的Javascript執行語句是
__fbBatchedBridge.invokeCallbackAndReturnFlushedQueue.apply(null, jsonArgs);
jsonArgs中包含callbackID和arguments,Webkit執行這段Javascript語句達到串連到Javascript端的目的。
當然,執行完Javascript語句後也有一個result返回,用來調用callNativeModules,作為後續的通訊請求,流程和前篇完全一致!
第四步:Javascript接收Native的應答
參考React-Native系列Android——Native與Javascript通訊原理(一),上一步中Bridge建立的Javascript執行語句
__fbBatchedBridge.invokeCallbackAndReturnFlushedQueue.apply(null, jsonArgs);
其實等同於
MessageQueue.invokeCallbackAndReturnFlushedQueue.apply(null, callbackID, args);
所以執行的是MessageQueue.js的invokeCallbackAndReturnFlushedQueue方法。
invokeCallbackAndReturnFlushedQueue(cbID, args) { guard(() => { this.__invokeCallback(cbID, args); this.__callImmediates(); }); return this.flushedQueue(); }
這裡的cbID其實就是callbackID了,也就是第一步裡面的this._callbackID。這個值是由Javascript傳給Native的,現在又從Native傳回來了,完璧歸趙啊!
下面調用的是this.__invokeCallback
__invokeCallback(cbID, args) { ... let callback = this._callbacks[cbID]; ... this._callbacks[cbID & ~1] = null; this._callbacks[cbID | 1] = null; callback.apply(null, args); ... }
this._callbacks集合裡面以callbackID為索引儲存著回呼函數callback,這裡就可以通過cbID這個索引為key取出來了。這樣執行callback.apply(null, args)就等於執行回呼函數了。
同時,還要清除this._callbacks集合裡面儲存的回呼函數。由於__nativeCall中封裝回呼函數時,先後儲存了兩個回呼函數onFail(索引為偶數)和onSucc(索引為奇數,比前者+1),而取出來的callback並不確定是onFail還是onSucc。所以,cbID & ~1最低位置0,cbID | 1最低位置1,這樣無論cbID標識是onFail還是onSucc的索引,都能保證兩者完全清除。
擷取APP狀態例子中的回呼函數是
function(appStateData){ console.log('dev', 'current state: ' + appStateData.app_state);}
app_state變數的值就是當前APP的狀態了,與AppStateModule中的值的封裝恰好呼應
private WritableMap createAppStateEventMap() { WritableMap appState = Arguments.createMap(); appState.putString("app_state", mAppState); return appState;}
這樣,整個通訊流程差不多就到此完整了。
總結
Javascript請求Native再回調到Javascript中,一共經曆了如下流程:
一共Javascript->Bridge->Native->Bridge->Javascript五個步驟,callbackID是整個流程的關鍵點。
Javascript請求Native,需要先產生callbackID,並以callbackID為唯一鍵儲存回呼函數。callbackID作為上一次通訊請求的應答內容傳到Native端,Native接收到後通過反射NativeModule的處理方法,然後將callbackID及處理結果返給Javascript端,Javascript使用callbackID擷取到儲存的回調方法,然後執行。
流程圖表示如下: