帶你徹底看懂React Native和Android原生控制元件之間的對映關係

Marksss發表於2019-03-01

此文基於react natve的 September 2018 - revision 5 版本

本人學校畢業後就當了安卓爬坑專業戶,3年來總算爬習慣了,不料今年掉進了RN這個天坑,從此開始了我的悲慘人生。。。Anyway,RN的思想還是值得學習的,今天就從Android的角度開始分析一下react native的基礎元件如何載入,看看它們與原生控制元件間的對映關係。

Android端原始碼淺析

安卓老司機看頁面的實現原理,必然首先看Activity,其次看View,RN在安卓端的載入開端也是如此。

以下是截至此文釋出前最新的RN官方教程中的例子(RN官方教程和RN原始碼一樣,一日三變,習慣就好) :

public class MyReactActivity extends Activity implements DefaultHardwareBackBtnHandler {
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null);

        setContentView(mReactRootView);
    }
}
複製程式碼

從上面的程式碼中可以看出,承載RN頁面顯示的也是一個普通的Activity,但setContentView中傳入的卻是一個特定的ReactRootView,也就是說載入全部在這個ReactRootView中完成。ReactInstanceManager類似於一個代理,承接了IO,通訊,佈局及其他一些邏輯性操作,下文中還會提到。

public class ReactRootView extends SizeMonitoringFrameLayout
    implements RootView, MeasureSpecProvider {
  ...
  @Override
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // No-op since UIManagerModule handles actually laying out children.
  }
}
複製程式碼

上面的程式碼省略了大部分與本文無關的程式碼,但也可以看出ReactRootView並沒有三頭六臂,它只不過是一個很普通的繼承自SizeMonitoringFrameLayoutFrameLayout)的控制元件容器,而且它的onLayout方法是空的,從註釋中可以看出子控制元件的佈局在UIManagerModule中實現。

public class UIManagerModule extends ReactContextBaseJavaModule
    implements OnBatchCompleteListener, LifecycleEventListener, UIManager {
  private final UIImplementation mUIImplementation;
  ...
  @ReactMethod(isBlockingSynchronousMethod = true)
  public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) {
    ...
    // 根據viewManagerName獲取ViewManager的對映
    return computeConstantsForViewManager(viewManagerName);
  }

  @Override
  public <T extends SizeMonitoringFrameLayout & MeasureSpecProvider> int addRootView(
      final T rootView, WritableMap initialProps, @Nullable String initialUITemplate) {
    ...
    // 獲取ReactRootView物件的引用,以便於再裡面新增View
    mUIImplementation.registerRootView(rootView, tag, themedRootContext);
    ...
  }
  // 該註解的方法都是可以在js程式碼中呼叫的
  @ReactMethod
  public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    if (DEBUG) {
      ...
    }
    // 實現的是reactRootView.addView()
    mUIImplementation.createView(tag, className, rootViewTag, props);
  }
  ...
}
複製程式碼

同樣,UIManagerModule裡面也沒有太多東西,它主要是用於暴露方法供js呼叫的,具體實現是由UIImplementation來完成的。被@ReactMethod註解的方法都可以在js程式碼中被呼叫到,包括:removeRootViewcreateViewmeasuremeasureLayoutmanageChildren等等,可見子控制元件的add,measure,layout,remove等操作都是由js呼叫UIManagerModule相應的方法後完成。

public class UIImplementation {
  ...
  public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    //構建ReactShadowNode
    ReactShadowNode cssNode = createShadowNode(className);
    ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
    Assertions.assertNotNull(rootNode, "Root node with tag " + rootViewTag + " doesn't exist");
    cssNode.setReactTag(tag);
    cssNode.setViewClassName(className);
    cssNode.setRootTag(rootNode.getReactTag());
    cssNode.setThemedContext(rootNode.getThemedContext());

    mShadowNodeRegistry.addNode(cssNode);
    ...
  }
  ...
}
複製程式碼

以上就是createView的具體實現,它主要做的是構造了一個ReactShadowNode

再看看createShadowNode

protected ReactShadowNode createShadowNode(String className) {
  ViewManager viewManager = mViewManagers.get(className);
  return viewManager.createShadowNodeInstance(mReactContext);
}
複製程式碼

它是通過className獲取到ViewManager。問題來了,ViewManager是什麼?看它的原始碼可知它是一個抽象類,從它的原始碼很難看出它是幹什麼用的,但一看繼承自它的子類就豁然開朗了,它的子類包括ReactTextInputManagerReactTextViewManagerReactImageManagerSwipeRefreshLayoutManagerReactCheckBoxManagerReactProgressBarViewManagerReactScrollViewManager等等等。從類名上看,這不就是Android的各種控制元件嗎?檢視原始碼後果然如此。

ReactTextViewManager為例:

public class ReactTextViewManager
    extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode> {
    ...
}
複製程式碼
public class ReactTextView extends TextView implements ReactCompoundView {
  ...
}
複製程式碼

它就是對TextView的封裝。由此可見js程式碼最終都對映到了原生的控制元件上。

我寫了一個很簡單的RN頁面,只有一個Text和一個Image,通過AS上的Layout Inspector可以清晰地看到,最終顯示的是封裝過的TextViewImageView

Layout Inspector

再回到@ReactMethod註解,它在JavaModuleWrapper中被獲取,再通過NativeModuleRegistry被放到了一個對映表裡面:

public class JavaModuleWrapper {
  ...
  private void findMethods() {
    ...
    for (Method targetMethod : targetMethods) {
      // 獲取@ReactMethod註解
      ReactMethod annotation = targetMethod.getAnnotation(ReactMethod.class);
    ...
    }
  }
}
複製程式碼
public class NativeModuleRegistry {
  /* package */ Collection<JavaModuleWrapper> getJavaModules(JSInstance jsInstance) {
    ArrayList<JavaModuleWrapper> javaModules = new ArrayList<>();
    // 生成對映表
    for (Map.Entry<String, ModuleHolder> entry : mModules.entrySet()) {
      if (!entry.getValue().isCxxModule()) {
        javaModules.add(new JavaModuleWrapper(jsInstance, entry.getValue()));
      }
    }
    return javaModules;
  }
}
複製程式碼

public class CatalystInstanceImpl implements CatalystInstance {
  static {
    // jni
    ReactBridge.staticInit();
  }

  @Override
  public void extendNativeModules(NativeModuleRegistry modules) {
    mNativeModuleRegistry.registerModules(modules);
    Collection<JavaModuleWrapper> javaModules = modules.getJavaModules(this);
    Collection<ModuleHolder> cxxModules = modules.getCxxModules();
    // 將原生方法的對映表傳給jsBridge
    jniExtendNativeModules(javaModules, cxxModules);
  }

  // C++的方法
  private native void jniExtendNativeModules(
    Collection<JavaModuleWrapper> javaModules,
    Collection<ModuleHolder> cxxModules);
  ...
}
複製程式碼

最後定位到CatalystInstanceImpl,它內部初始化了ReactBridge(jsBridge),也就是說@ReactMethod註解的方法都放到了一個登錄檔裡面供jsBridge隨時呼叫。

CatalystInstanceImpl也是在ReactInstanceManager內部例項化的,兜兜轉轉又回到了開頭的ReactInstanceManager,也就是說jsBridge對映到原生控制元件的邏輯都在它內部實現。

小結

時序圖

Android端的載入過程大致如下:

  1. jsBridge對映到UIManagerModule中有@ReactMethod的方法上;
  2. UIManagerModule中針對控制元件的操作由UIImplementation代理,完成控制元件的add,measure,layout,remove等操作;
  3. 所有控制元件最終新增到ReactRootView中,最終由它完成總體的載入並顯示。

至此,Android端相關的邏輯已經差不多了,接下來看看在js端又是怎麼對映的。

js端原始碼淺析

先來一段上文中提到過的RN頁面的程式碼:

type Props = {};
class App extends Component<Props> {
    render() {
        return (
            <View style={styles.container}>
                <Image
                    style={styles.image}
                    source={require('./img.png')}>
                </Image>
                <Text style={styles.welcome}>Welcome to React Native!</Text>
            </View>
        );
    }
}

export default App;
複製程式碼

css程式碼不是重點,所以被我省略了,上面只有js和,JSX,一種js的語法糖,所有基礎元件都會以JSX的形式置於Componentrender方法中。

接下來看看Component是怎麼實現的:

const Component = class extends RealComponent {
    render() {
      const name = RealComponent.displayName || RealComponent.name;
      return React.createElement(
        name.replace(/^(RCT|RK)/,''),
        this.props,
        this.props.children,
      );
    }
  };
複製程式碼

最終JSX會在React.createElement方法中被翻譯成js程式碼,有興趣的童鞋可以查查React框架,這裡就不多展開了。

現在回到例子程式碼中的基礎元件,以Text為例,看看它的原始碼:

...
const RCTVirtualText =
  UIManager.getViewManagerConfig('RCTVirtualText') == null
    ? RCTText
    : createReactNativeComponentClass('RCTVirtualText', () => ({
        validAttributes: {
          ...ReactNativeViewAttributes.UIView,
          isHighlighted: true,
          maxFontSizeMultiplier: true,
        },
        uiViewClassName: 'RCTVirtualText',
      }));

const Text = (
  props: TextProps,
  forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
) => {
  return <TouchableText {...props} forwardedRef={forwardedRef} />;
};
const TextToExport = React.forwardRef(Text);
TextToExport.displayName = 'Text';
TextToExport.propTypes = DeprecatedTextPropTypes;
module.exports = (TextToExport: Class<NativeComponent<TextProps>>);
複製程式碼

Text的原始碼不少,對於非專業前端,看起來比較吃力,但也有捷徑,從對外暴露點開始找,也就是從module.exports開始,到TextToExport,再到Text,再到RCTVirtualText,最後定位到了UIManager.getViewManagerConfig

UIManager.getViewManagerConfig = function(viewManagerName: string) {
  if (
    viewManagerConfigs[viewManagerName] === undefined &&
    UIManager.getConstantsForViewManager
  ) {
    try {
      viewManagerConfigs[
        viewManagerName
      ] = UIManager.getConstantsForViewManager(viewManagerName);
    } catch (e) {
      viewManagerConfigs[viewManagerName] = null;
    }
  }
  ...
};
複製程式碼

看到getConstantsForViewManager,是不是覺得很眼熟?沒錯,它就是上一板塊Android原始碼中提到的UIManagerModule中的方法,讓我們再來回顧一下java原始碼:

  @ReactMethod(isBlockingSynchronousMethod = true)
  public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) {
    ...
    return computeConstantsForViewManager(viewManagerName);
  }

  private @Nullable WritableMap computeConstantsForViewManager(final String viewManagerName) {
    ViewManager targetView =
        viewManagerName != null ? mUIImplementation.resolveViewManager(viewManagerName) : null;
    if (targetView == null) {
      return null;
    }

    SystraceMessage.beginSection(
            Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "UIManagerModule.getConstantsForViewManager")
        .arg("ViewManager", targetView.getName())
        .arg("Lazy", true)
        .flush();
    try {
      Map<String, Object> viewManagerConstants =
          UIManagerModuleConstantsHelper.createConstantsForViewManager(
              targetView, null, null, null, mCustomDirectEvents);
      if (viewManagerConstants != null) {
        return Arguments.makeNativeMap(viewManagerConstants);
      }
      return null;
    } finally {
      SystraceMessage.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE).flush();
    }
  }
複製程式碼

這個方法的作用就是從快取中獲取ViewManager物件,裝入WritableMap後回傳給了js,而WritableMap在js中以物件的形式存在。

再回到UIManager,它除了可以呼叫getConstantsForViewManager,上個板塊提到的被@ReactMethod註解的方法諸如removeRootViewcreateViewmeasuremeasureLayout等等在js中的對映都是由它來呼叫,也就是說js呼叫原生控制元件的對映都由UIManager來完成。

再看一眼UIManager的原始碼:

const NativeModules = require('NativeModules');
const {UIManager} = NativeModules;
...
module.exports = UIManager;
複製程式碼

看來UIManager只不過是對NativeModules的二次封裝。寫過RN的童鞋對此肯定不陌生,寫js和原生通訊的相關程式碼中肯定會用到NativeModules,它是js和原生程式碼通訊的橋樑。

至於NativeModules和C++的互動過程,這裡就簡單講一下,NativeModules內部的有一個BatchedBridge(即MessageQueue)的物件:

class MessageQueue {
  // js註冊的回撥,供原生程式碼呼叫
  _lazyCallableModules: {[key: string]: (void) => Object};
  // js呼叫原生程式碼請求的快取列表
  _queue: [number[], number[], any[], number];

  // js呼叫原生方法的請求
  enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: any[],
    onFail: ?Function,
    onSucc: ?Function,
  ) {
    ...
    // 把請求打包成一個Message,放入快取列表
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);
    if (
      global.nativeFlushQueueImmediate &&
      (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
        this._inCall === 0)
    ) {
      var queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      // 如果是同步請求,則請求的message立即入列,否則等待flushedQueue()的執行
      // 這是一個C++的函式
      global.nativeFlushQueueImmediate(queue);
    }
  }

  // 將快取的請求列表全部入列
  flushedQueue() {
    this.__guard(() => {
      this.__callImmediates();
    });

    const queue = this._queue;
    this._queue = [[], [], [], this._callID];
    return queue[0].length ? queue : null;
  }

  // 註冊回撥介面
  registerCallableModule(name: string, module: Object) {
    this._lazyCallableModules[name] = () => module;
  }
  ...
}
複製程式碼

它內部儲存了js中對外暴露的方法和模組的對映表供jsBridge呼叫,如果需要呼叫原生程式碼中的方法,MessageQueue會將請求封裝成一個Message放入一個請求佇列,然後觸發原生的方法。看著怎麼這麼像Android中的Handler機制?原因很簡單,js執行的執行緒是獨立於原生程式碼所在的UI執行緒的,執行緒間通訊最簡單的還是類似Handler這樣的方式。

小結

時序圖

RN基礎元件對映到原生在js端的表現大致如下:

  1. JSX形式的RN基礎元件首先會被翻譯成js程式碼;
  2. 元件會在js程式碼中呼叫UIManager相應的方法;
  3. UIManager通過jsBridge對映到原生方法UIManagerModule中;

C++原始碼淺析

Android端和js端都已經介紹完畢了,就像扁擔兩頭的貨物都準備完畢了,就差根扁擔了,jsBridge就是這根扁擔。

先來看一下與CatalystInstanceImpl.java對應的CatalystInstanceImpl.cpp

void CatalystInstanceImpl::registerNatives() {
  registerHybrid({
     // jniExtendNativeModules就是CatalystInstanceImpl.java中那個傳入原生方法對映表的native方法
     // 它被指向了extendNativeModules方法
     makeNativeMethod("jniExtendNativeModules", CatalystInstanceImpl::extendNativeModules),
     ...
   });

   JNativeRunnable::registerNatives();
}

void CatalystInstanceImpl::extendNativeModules(
    jni::alias_ref<jni::JCollection<JavaModuleWrapper::javaobject>::javaobject> javaModules,
    jni::alias_ref<jni::JCollection<ModuleHolder::javaobject>::javaobject> cxxModules) {
      // 註冊對映表
      moduleRegistry_->registerModules(buildNativeModuleList(
        std::weak_ptr<Instance>(instance_),
        javaModules,
        cxxModules,
        moduleMessageQueue_));
 }
複製程式碼

可見CatalystInstanceImpl的這部分程式碼就是用來註冊原生方法的對映表的。

再來看看js中呼叫C++的方法nativeFlushQueueImmediate,以下程式碼位於JSIExecutor.cpp中:

runtime_->global().setProperty(
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"),
          1,
          [this](
              jsi::Runtime&,
              const jsi::Value&,
              const jsi::Value* args,
              size_t count) {
            if (count != 1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            // 呼叫已註冊的原生模組
            callNativeModules(args[0], false);
            return Value::undefined();
          }));
複製程式碼

以下程式碼位於JsToNativeBridge.cpp中,它以委託的形式存在,執行上述程式碼中的callNativeModules

void callNativeModules(
      JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
    ...
    for (auto& call : parseMethodCalls(std::move(calls))) {
      // 執行已註冊的原生模組中的方法
      m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId);
    }
    ...
  }
複製程式碼

最後殊途同歸都到了ModuleRegistry.cpp

// 註冊原生模組
void ModuleRegistry::registerModules(std::vector<std::unique_ptr<NativeModule>> modules) {
  ...
}

// 執行原生模組的方法
void ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) {
  ...
  modules_[moduleId]->invoke(methodId, std::move(params), callId);
}
複製程式碼

至此,一條完整的對映鏈已經全部講完。

總結

本文以一般看原始碼的順序來展開,依次解讀了Android端,js端和C++的原始碼,分析了RN基礎元件是如何一步步地對映成為原生控制元件的整個過程,展示了一條完整地對映鏈條。

最後整理一下整個對映的鏈條:

對映鏈條

以下是一些常用的rn元件與Android原生控制元件之間的對應關係:

  • Text -> TextView
  • Image -> ImageView
  • TextInput -> EditText
  • CheckBox -> AppCompatCheckBox
  • RefreshControl -> SwipeRefreshLayout
  • ScrollView -> ScrollView
  • Slider -> SeekBar
  • Switch -> SwitchCompat

相關文章