Rn與Android原生控件那些事

最近公司一直在搞關于Rn平臺搭建相關的東西,以前自學都是斷斷續(xù)續(xù)的入坑撞牢,但好在基礎都還在祸穷!

從Android的角度開始分析一下反應native的基礎組件如何加載,聊下它們與原生控件間的映射關系读恃。(ps:希望能幫助一些朋友)

安卓端源碼淺析

賣個關子,安卓老司機看頁面的實現(xiàn)原理,怎么看的崎苗?RN在安卓端的加載開端也是如此!

基于這個(https://github.com/facebook/react-native/releases/tag/v0.57.5):

public class DXYReactActivity 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, “DXYReactNativeApp", 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中實現(xiàn)。

public class UIManagerModule extends ReactContextBaseJavaModule
    implements OnBatchCompleteListener, LifecycleEventListener, UIManager {
  private final UIImplementation mUIImplementation;
  ...
  @ReactMethod(isBlockingSynchronousMethod = true)
  public @Nullable WritableMap getConstantsForViewManager(final String viewManagerName) {
    ...
    // 根據(jù)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) {
      ...
    }
    // 實現(xiàn)的是reactRootView.addView()
    mUIImplementation.createView(tag, className, rootViewTag, props);
  }
  ...
}

同樣锰镀,UIManagerModule里面也沒有太多東西娘侍,它主要是用于暴露方法供JS調用的,實現(xiàn)具體的英文由UIImplementation來完成的被泳炉。@ReactMethod注解的方法都可以在JS代碼中被調用到憾筏,包括:removeRootViewcreateView花鹅,measure氧腰,measureLayoutmanageChildren等等刨肃,可見子控件的添加古拴,測量,布局真友,刪除等操作都是由JS調用UIManagerModule相應的方法后完成斤富。

public class UIImplementation {
  ...
  public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
    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的具體實現(xiàn),它主要做的是構造了一個ReactShadowNode锻狗。

再看看createShadowNode

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

的英文它通過classNameViewManager满力。問題來了,ViewManager是什么轻纪?看它的源碼可知它是一個抽象類油额,從它的源碼很難看出它是干什么用的,但一看繼承自它的子類就豁然開朗了刻帚,它的子類包括ReactTextInputManager潦嘶,ReactTextViewManagerReactImageManager崇众,SwipeRefreshLayoutManager掂僵,ReactCheckBoxManagerReactProgressBarViewManager顷歌,ReactScrollViewManager等等等锰蓬。從類名上看,這不就是安卓的各種控件嗎眯漩?查看源碼后果然如此芹扭。

ReactTextViewManager為例:

public class ReactTextViewManager
    extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode> {
    ...
}

public class ReactTextView extends TextView implements ReactCompoundView {
  ...
}

就是它對TextView的封裝。由此可見JS代碼最終都映射到了原生的控件上赦抖。

大家可以嘗試下舱卡,一個很簡單的RN頁面(此處無圖),只有一個Text和一個Image队萤,通過AS上的布局檢查員可以清晰地看到轮锥,最終顯示的是封裝過的TextViewImageView

回到再@ReactMethod注解要尔,在它JavaModuleWrapper中舍杜,通過再NativeModuleRegistry被放到了一個映射表里面:

public class JavaModuleWrapper {
  ...
  private void findMethods() {
    ...
    for (Method targetMethod : targetMethods) {
      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();
    jniExtendNativeModules(javaModules, cxxModules);
  }

  private native void jniExtendNativeModules(
    Collection<JavaModuleWrapper> javaModules,
    Collection<ModuleHolder> cxxModules);
  ...
}

最后定位到CatalystInstanceImpl新娜,它內部初始化了ReactBridge(jsBridge),也就是說@ReactMethod注解的方法都放到了一個注冊表A里面供jsBridge隨時調用蝴簇。

CatalystInstanceImpl也是在ReactInstanceManager內部實例化的杯活,兜兜轉轉又回到了開頭的ReactInstanceManager,也就是說jsBridge映射到原生控件的邏輯都在它內部實現(xiàn)熬词。

小結

安卓端的加載過程大致如下:

  1. jsBridge到映射UIManagerModule中有@ReactMethod的方法上;
  2. UIManagerModule針對中的控件操作由UIImplementation代理旁钧,完成控件的添加,測量互拾,布局歪今,刪除等操作;
  3. 控件所有名單最終添加到ReactRootView中,最終由它完成總體的加載并顯示颜矿。

至此寄猩,安卓端相關的邏輯已經(jīng)差不多了,接下來看看在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的英文怎么實現(xiàn)的:

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分類中翻譯代碼兽赁,有興趣的童鞋可以查查框架,這里就不多展開了冷守。

現(xiàn)在回到例子代碼中的基礎組件刀崖,以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的源碼不少拍摇,對于非專業(yè)前端亮钦,看起來比較吃力,但也有捷徑授翻,從對外暴露點開始找或悲,就是也。從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注解的方法諸如removeRootViewcreateView襟齿,measure姻锁,measureLayout等等在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()的執(zhí)行
      // 這是一個C++的函數(shù)
      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會將請求封裝成一個消息放入一個請求隊列信认,然后觸發(fā)原生的方法【椋看著怎么這么像的Android中的處理程序機制嫁赏?原因很簡單,JS執(zhí)行的線程是獨立于原生代碼所在的UI線程的油挥,線程間通信最簡單的還是類似處理器這樣的方式潦蝇。

小結

RN基礎組件映射到原生在JS端的表現(xiàn)大致如下:

  1. JSX形式的RN基礎組件首先會被翻譯成JS代碼;
  2. 組件會在JS中代碼調用UIManager相應的方法;
  3. UIManager通過jsBridge到映射原生方法UIManagerModule中;

C ++源碼淺析

安卓端和JS端都已經(jīng)介紹完畢了,就像扁擔兩頭的貨物都準備完畢了深寥,就差橋了攘乒,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中,它以委托的形式存在闰集,執(zhí)行上述代碼中的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) {
  ...
}

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

總算一條完整的映射鏈over沽讹。

總結

解讀了的Android端般卑,JS端和C ++的源碼,分析了RN基礎組件是如何一步步地映射成為原生控件的整個過程爽雄,展示了一條完整地映射蝠检。

最后整理一下整個映射的:


不喜勿噴.png
  • 文字 - > TextView
  • 圖像 - > ImageView
  • TextInput - > EditText
  • CheckBox - > AppCompatCheckBox
  • RefreshControl - > SwipeRefreshLayout
  • ScrollView - > ScrollView
  • 滑塊 - > SeekBar
  • Switch - > SwitchCompat
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挚瘟,隨后出現(xiàn)的幾起案子叹谁,更是在濱河造成了極大的恐慌,老刑警劉巖刽沾,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件本慕,死亡現(xiàn)場離奇詭異,居然都是意外死亡侧漓,警方通過查閱死者的電腦和手機锅尘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來布蔗,“玉大人藤违,你說我怎么就攤上這事∽葑幔” “怎么了顿乒?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長泽谨。 經(jīng)常有香客問我璧榄,道長,這世上最難降的妖魔是什么吧雹? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任骨杂,我火速辦了婚禮,結果婚禮上雄卷,老公的妹妹穿的比我還像新娘搓蚪。我一直安慰自己,他們只是感情好丁鹉,可當我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布妒潭。 她就那樣靜靜地躺著,像睡著了一般揣钦。 火紅的嫁衣襯著肌膚如雪雳灾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天冯凹,我揣著相機與錄音佑女,去河邊找鬼。 笑死谈竿,一個胖子當著我的面吹牛团驱,可吹牛的內容都是我干的。 我是一名探鬼主播空凸,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼嚎花,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呀洲?” 一聲冷哼從身側響起紊选,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎道逗,沒想到半個月后兵罢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡滓窍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年卖词,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吏夯。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡此蜈,死狀恐怖,靈堂內的尸體忽然破棺而出噪生,到底是詐尸還是另有隱情裆赵,我是刑警寧澤,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布跺嗽,位于F島的核電站战授,受9級特大地震影響,放射性物質發(fā)生泄漏桨嫁。R本人自食惡果不足惜植兰,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞧甩。 院中可真熱鬧钉跷,春花似錦、人聲如沸肚逸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朦促。三九已至膝晾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間务冕,已是汗流浹背血当。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臊旭。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓落恼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親离熏。 傳聞我的和親對象是個殘疾皇子佳谦,可洞房花燭夜當晚...
    茶點故事閱讀 43,587評論 2 350