This article is based on react NatveSeptember 2018 – revision 5version

After graduation, I became a professional android pit climber. I finally got used to it for 3 years. Unexpectedly, I fell into RN sinkhole this year, and my miserable life began. Anyway, RN’s idea is still worth learning. Today, we’ll analyze how react Native’s basic components load from the perspective of Android and see how they map to native controls.

Android end source analysis

Android old driver to see the realization principle of the page, must first look at the Activity, then look at the View, RN in android terminal loading beginning is the same.

Here are some examples from RN’s official tutorials as of this post (like RN’s source code, you get used to them) :

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); }}Copy the code

As you can see from the code above, the page hosting RN displays a normal Activity, but the setContentView is passed in a specific ReactRootView, meaning that all loading is done in this ReactRootView. The ReactInstanceManager is similar to an agent that undertakes IO, communication, layout, and other logical operations, as described below.

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.
  }
}
Copy the code

The above code omitted most of the code that has nothing to do with this article, but also can see ReactRootView no superhuman powers, it is just a very ordinary inherited from SizeMonitoringFrameLayout (FrameLayout) control container, And its onLayout method is empty, and you can see from the comments that the layout of the child controls is implemented in the UIManagerModule.

public class UIManagerModule extends ReactContextBaseJavaModule implements OnBatchCompleteListener, LifecycleEventListener, UIManager { private final UIImplementation mUIImplementation; . @ReactMethod(isBlockingSynchronousMethod =true) public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) { ... // Get the ViewManager mapping according to viewManagerNamereturncomputeConstantsForViewManager(viewManagerName); } @Override public <T extends SizeMonitoringFrameLayout & MeasureSpecProvider> int addRootView( final T rootView, WritableMap initialProps, @Nullable String initialUITemplate) { ... / / get ReactRootView object reference, in order to add the View again mUIImplementation. RegisterRootView (rootView, tag, themedRootContext); . } @reactMethod public void createView(int className, String className, int rootViewTag, ReadableMap props) {if(DEBUG) { ... } / / implementation is reactRootView addView () mUIImplementation. CreateView (tag, the className, rootViewTag, props); }... }Copy the code

Similarly, there’s not much in the UIManagerModule, which is mainly used to expose methods for JS calls, implemented by UIImplementation. Methods annotated by @reactMethod can be called in JS code, including: RemoveRootView, createView, Measure, measureLayout, manageChildren, etc. Remove and other operations are completed after JS calls corresponding methods of UIManagerModule.

public class UIImplementation { ... public void createView(int tag, String className, int rootViewTag, ReadableMap props) {// Build 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); . }... }Copy the code

CreateView creates a ReactShadowNode.

Now look at createShadowNode:

protected ReactShadowNode createShadowNode(String className) {
  ViewManager viewManager = mViewManagers.get(className);
  return viewManager.createShadowNodeInstance(mReactContext);
}
Copy the code

It gets the ViewManager by className. So the question is, what is a ViewManager? See its source code know it is an abstract class, from its source code it is difficult to see what it is used for, but a look at the inheritance of its subclasses are suddenly clear, its subclasses include ReactTextInputManager, ReactTextViewManager, ReactImageManager, SwipeRefreshLayoutManager ReactCheckBoxManager, ReactProgressBarViewManager, ReactScrollViewManager etc etc. From the class name, this is not Android’s various controls? After viewing the source code, so it is.

Take ReactTextViewManager as an example:

public class ReactTextViewManager
    extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode> {
    ...
}
Copy the code
public class ReactTextView extends TextView implements ReactCompoundView {
  ...
}
Copy the code

It’s just a wrapper around the TextView. This shows that the JS code eventually maps to the native controls.

I wrote a very simple RN page with only one Text and one Image. The Layout Inspector on the AS clearly shows the wrapped TextView and ImageView.

Going back to the @reactMethod annotation, which is retrieved in JavaModuleWrapper and placed into a mapping table via NativeModuleRegistry:

public class JavaModuleWrapper {
  ...
  private void findMethods() {...for(Method targetMethod : TargetMethods) {/ / get the @ ReactMethod annotation ReactMethod annotation = targetMethod. GetAnnotation (ReactMethod. Class); . }}}Copy the code
public class NativeModuleRegistry { /* package */ Collection<JavaModuleWrapper> getJavaModules(JSInstance jsInstance) { ArrayList<JavaModuleWrapper> javaModules = new ArrayList<>(); // Create a mapping tablefor (Map.Entry<String, ModuleHolder> entry : mModules.entrySet()) {
      if (!entry.getValue().isCxxModule()) {
        javaModules.add(new JavaModuleWrapper(jsInstance, entry.getValue()));
      }
    }
    returnjavaModules; }}Copy the code
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(); // Pass the native method mapping table to jsBridge jniExtendNativeModules(javaModules, cxxModules); } // private native void jniExtendNativeModules(Collection<JavaModuleWrapper> javaModules, Collection<ModuleHolder> cxxModules); . }Copy the code

Finally, the CatalystInstanceImpl initializes the ReactBridge (jsBridge) internally, meaning that @reactMethod annotated methods are placed in a registry for jsBridge to call at any time.

The CatalystInstanceImpl is instantiated inside the ActInstancemanager, which goes back to the original ActInstancemanager, meaning that jsBridge mapping to native controls is implemented inside it.

summary

The loading process for Android is as follows:

  1. JsBridge mapped toUIManagerModuleThere are@ReactMethodOn the method of;
  2. UIManagerModuleOperations on the control are performed byUIImplementationAgent, complete control add, measure, layout, remove and other operations;
  3. All controls are finally added toReactRootViewIn, it finally completes the overall load and display.

Now that the logic on the Android side is pretty much there, let’s see how it maps to the JS side.

Js end source analysis

Let’s start with the code for the RN page mentioned above:

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;
Copy the code

The CSS code is beside the point, so I’ve omitted it. It’s just JS and JSX, the js syntactic sugar, and all the base components are put in the JSX form of the Component’s Render method.

Let’s see how Component is implemented:

const Component = class extends RealComponent {
    render() {
      const name = RealComponent.displayName || RealComponent.name;
      return React.createElement(
        name.replace(/^(RCT|RK)/,' '), this.props, this.props.children, ); }};Copy the code

JSX will eventually be translated into JS code in the React. CreateElement method. If you are interested, check the React framework.

Now go back to the basic component in the sample code, using Text as an example to see its source code:

. 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>>);
Copy the code

Text source code many, for non-professional front-end, looks more difficult, but there are shortcuts, from the external exposure point to find, that is, from module.exports to TextToExport, then to Text, and then to RCTVirtualText, Finally locate the UIManager. GetViewManagerConfig.

UIManager.getViewManagerConfig = function(viewManagerName: string) {
  if( viewManagerConfigs[viewManagerName] === undefined && UIManager.getConstantsForViewManager ) { try { viewManagerConfigs[ viewManagerName ] = UIManager.getConstantsForViewManager(viewManagerName); } catch (e) { viewManagerConfigs[viewManagerName] = null; }}... };Copy the code

See getConstantsForViewManager, feel a little familiar? Yes, it is the UIManagerModule method mentioned in the previous Android source. Let’s review the Java source code again:

  @ReactMethod(isBlockingSynchronousMethod = true)
  public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) {
    ...
    returncomputeConstantsForViewManager(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);
      }
      returnnull; } finally { SystraceMessage.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE).flush(); }}Copy the code

This method takes the ViewManager object from the cache, loads the WritableMap, and passes it back to JS, where the WritableMap exists as an object.

Back to UIManager, besides it can call the getConstantsForViewManager, mentioned on the tectonic plates are @ ReactMethod annotation methods such as removeRootView createView, measure, MeasureLayout and other mappings in JS are called by it, which means UIManager does all the mapping for javascript calls to native controls.

Take a look at the UIManager source code again:

const NativeModules = require('NativeModules'); const {UIManager} = NativeModules; . module.exports = UIManager;Copy the code

It seems that UIManager is nothing more than a secondary encapsulation of NativeModules. Those who have written RN are certainly familiar with this. NativeModules are certainly used in the code related to JS and native communication, which is the bridge of COMMUNICATION between JS and native code.

Within NativeModules, there is a BatchedBridge(MessageQueue) object:

_lazyCallableModules: {[key: string]: (void) => Object}; _lazyCallableModules: {[key: string]: (void) => Object}; _queue: [number[], number[], any[], number]; EnqueueNativeCall (moduleID: number, methodID: number, params: any[], onFail:? Function, onSucc: ? Function, ) { ... // Package the request as a Message and place it in the cache list 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; / / if it is a synchronous request, the request message immediately, or wait for flushedQueue () to perform / / this is a c + + function. Global nativeFlushQueueImmediate (queue); }} // Columns the cached request listflushedQueue() {
    this.__guard(() => {
      this.__callImmediates();
    });

    const queue = this._queue;
    this._queue = [[], [], [], this._callID];
    returnqueue[0].length ? queue : null; } // registerCallableModule(name: string, module: Object) {this._lazyCallableModules[name] = () => module; }... }Copy the code

It internally stores the mapping table of methods and modules exposed in JS for jsBridge to call. If the method in the native code needs to be called, MessageQueue will encapsulate the request into a Message and put it into a request queue, and then trigger the native method. Why does it look so much like the Handler mechanism in Android? The reason for this is simple: the js thread is independent of the UI thread where the native code resides, and the easiest way to communicate between threads is through something like a Handler.

summary

Mapping RN base components to native js components looks like this:

  1. RN base components in JSX form are first translated into JS code;
  2. Components are called in JS codeUIManagerCorresponding methods;
  3. byUIManagerMapping to native methods via jsBridgeUIManagerModule;

C++ source code analysis

Android end and JS end have been introduced, just like the goods at both ends of the pole are ready, except the pole, jsBridge is this pole.

First take a look at and CatalystInstanceImpl. Java corresponding CatalystInstanceImpl. CPP:

void CatalystInstanceImpl::registerNatives() {registerHybrid ({/ / jniExtendNativeModules is CatalystInstanceImpl. Java native method of the method was introduced into the native mapping table in / / It has been pointed to the extendNativeModules method 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) {// Register the mapping table moduleRegistry_->registerModules(buildNativeModuleList( std::weak_ptr<Instance>(instance_), javaModules, cxxModules, moduleMessageQueue_)); }Copy the code

You can see that this part of the CatalystInstanceImpl code is used to register the mapping table of the native method.

Take a look at the js nativeFlushQueueImmediate call c + + method, the following code in JSIExecutor. CPP in:

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"); } // Call the registered native module callNativeModules(args[0],false);
            return Value::undefined();
          }));
Copy the code

The following code, located in jstonativeBridge.cpp, exists as a delegate to perform the callNativeModules in the above code:

void callNativeModules(
      JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
    ...
    for(auto& call : ParseMethodCalls (STD ::move(calls)) {// Execute the method m_registry->callNativeMethod(call.moduleid, call.methodid, std::move(call.arguments), call.callId); }... }Copy the code

Moduleregistry.cpp:

/ / registered native modules void ModuleRegistry: : registerModules (STD: : vector < STD: : unique_ptr < NativeModule > > modules) {... } / / execution method of primary module void ModuleRegistry: : callNativeMethod (unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) { ... modules_[moduleId]->invoke(methodId, std::move(params), callId); }Copy the code

At this point, a complete chain of mappings is complete.

conclusion

In this paper, the general order to see the source code to expand, read the Android side, js side and C++ source code, analysis of RN basic components is how to map step by step into the whole process of native controls, showing a complete mapping chain.

Finally, rearrange the entire chain of mappings:

Here are some common RN components that correspond to Android native controls:

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