ReactNative源碼解析-源碼編譯ReactNative

接觸RN已經一年多時間了,基礎概念和使用方法基本沒什么問題了。但是底層原理一直沒有進行深入的研究。RN啟動、通信、渲染等相關原理并不清楚,導致服務端渲染、高性能列表等優化手段看到實現方案后對其原理仍然很模糊,是時候解決這種尷尬的處境了。

本篇作為RN源碼解析的首篇,主要介紹如何搭建環境、引入相關源碼,給后續的分析做準備。

系統環境:
macOS: 10.14.6
AndroidStudio: 3.5.1
Android Emulator: 9.0 (Pie) - API 28

相關源碼版本:
React: 16.11.0
ReactNative: 0.62.2

1. 準備工程目錄

代碼目錄.jpg

2. 安裝NDK

  • 下載ndk:http://dl.google.com/android/repository/android-ndk-r17c-darwin-x86_64.zip
  • 配置環境:在本地命令行腳本配置中添加變量,根據使用的shell的不同,配置文件可能如下
    bash: .bash_profile or .bashrc
    zsh: .zprofile or .zshrc
    ksh: .profile or $ENV
    export ANDROID_SDK=/Users/your_unix_name/android-sdk-macosx
    export ANDROID_NDK=/Users/your_unix_name/android-ndk/android-ndk-r17c
    

3. 安裝ReactNative源碼
進入到sourcecode目錄下,執行如下命令

npm install --save react-native@0.62.2

之前我的理解是,從RN github倉庫克隆下來的master分支是源碼。從結果上看這個源碼是不能直接引入Android工程編譯的,還需要執行npm install 之后才能被引入(TODO這點后續看看是為什么)
4. 創建js工程
進入到RNDemoApp目錄,創建package.json文件,并填入如下內容

{
  "name": "MyReactNativeApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "yarn react-native start"
  },
  "dependencies": {
    "react": "16.11.0",
    "react-native": "0.62.2"
  }
}

執行命令,安裝工程

yarn install

添加index.js文件,作為RN頁面內容的具體實現

import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';

class HelloWorld extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>Hello, I come from native build! </Text>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

AppRegistry.registerComponent('MyReactNativeApp', () => HelloWorld);

5. 創建Android工程
進入ReactNativeDemo所在的父目錄底下,創建空的Android工程,工程名稱為ReactNativeDemo,創建后工程代碼內容在ReactNativeDemo目錄下

Android工程目錄.jpg

打開Android工程的local.properties文件,添加

ndk.dir=/Users/your_unix_name/android-ndk/android-ndk-r17c

在android/build.gradle 文件里面添加gradle-download-task依賴

dependencies {
        classpath 'com.android.tools.build:gradle:3.4.2'
        classpath 'de.undercouch:gradle-download-task:4.0.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

在android/settings.gradle文件里面添加:ReactAndroid,引入ReactAndroid子工程

include ':ReactAndroid'

project(':ReactAndroid').projectDir = new File(
       rootProject.projectDir, '../ReactNative/sourcecode/node_modules/react-native/ReactAndroid')

修改android/app/build.gradle文件,使用剛引入的ReactAndroid工程作為工程的源碼

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation project(':ReactAndroid')

    ...
}

前面完成了基本工程的配置,接著添加新的Activity,來承載要顯示的RN頁面

package com.example.reactnativedemo;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.KeyEvent;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.shell.MainReactPackage;

/**
 * Created by shihongjie on 2020-05-08
 */
public class MyReactActivity extends Activity implements DefaultHardwareBackBtnHandler {
    private final int OVERLAY_PERMISSION_REQ_CODE = 1;  // 任寫一個值

    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

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

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
            }
        }

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModulePath("index")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 注意這里的MyReactNativeApp必須對應“index.js”中的
        // “AppRegistry.registerComponent()”的第一個參數
        mReactRootView.startReactApplication(mReactInstanceManager, "MyReactNativeApp", null);

        setContentView(mReactRootView);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (!Settings.canDrawOverlays(this)) {
                    // SYSTEM_ALERT_WINDOW permission not granted
                }
            }
        }
        mReactInstanceManager.onActivityResult(this, requestCode, resultCode, data);
    }


    @Override
    protected void onPause() {
        super.onPause();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostPause(this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostResume(this, this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostDestroy(this);
        }
        if (mReactRootView != null) {
            mReactRootView.unmountReactApplication();
        }
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }
}

在AndroidManifest文件中注冊上面新添加的Activity,并添加權限

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.reactnativedemo">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".MyReactActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
        </activity>

        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>

在MainActivity 中添加一個跳轉,跳轉到MyReactActivity,很簡單,這里不展示相關的代碼了。

NetWork Security Config(API level 28+)

從Android 9 開始,cleartext traffic 默認是關閉的,這會使應用無法連接到 React Native Packager 上,需要添加域名規則以允許在 React Native 包管理器的 IP 上使用 cleartext traffic。

創建資源文件

新建文件 src/main/res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <!-- allow cleartext traffic for React Native packager ips in debug -->
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="false">localhost</domain>
    <domain includeSubdomains="false">10.0.2.2</domain>
    <domain includeSubdomains="false">10.0.3.2</domain>
  </domain-config>
</network-security-config>

在 AndroidManifest.xml 中使用上面的配置項

<!-- ... -->
<application
  android:networkSecurityConfig="@xml/network_security_config">
  <!-- ... -->
</application>
<!-- ... -->

注意
ReactNative 0.60.0版本以上,啟動MyReactActivity 后會報java.lang.UnsatisfiedLinkError: couldn't find DSO to load 的錯誤,需要在 app/build.gradle中添加如下代碼

project.ext.react = [
    entryFile: "index.js",
    enableHermes: true,  // clean and rebuild if changing
]

/**
 * The preferred build flavor of JavaScriptCore.
 *
 * For example, to use the international variant, you can use:
 * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
 *
 * The international variant includes ICU i18n library and necessary data
 * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
 * give correct results when using with locales other than en-US.  Note that
 * this variant is about 6MiB larger per architecture than default.
 */
def jscFlavor = 'org.webkit:android-jsc:+'

/**
 * Whether to enable the Hermes VM.
 *
 * This should be set on project.ext.react and mirrored here.  If it is not set
 * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
 * and the benefits of using Hermes will therefore be sharply reduced.
 */
def enableHermes = project.ext.react.get("enableHermes", false);

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation project(':ReactAndroid')
    if (enableHermes) {
        def hermesPath = "../../ReactNative/sourcecode/node_modules/hermes-engine/android/"
        debugImplementation files(hermesPath + "hermes-debug.aar")
        releaseImplementation files(hermesPath + "hermes-release.aar")
    } else {
        implementation jscFlavor
    }
}

同時在上面的dependencies中添加swiperefreshlayout依賴,防止出現java.lang.ClassNotFoundException: Didn't find class "androidx.swiperefreshlayout.widget.SwipeRefreshLayout" on path: DexPathList[[zip file "/data/app/com.app-Of8EHYbtm9-YItGtnh8O9Q==/base.apk"],nativeLibraryDirectories=[/data/app/com.app-Of8EHYbtm9-YItGtnh8O9Q==/lib/x86, /data/app/com.app-Of8EHYbtm9-YItGtnh8O9Q==/base.apk!/lib/x86, /system/lib]] 異常

implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"

6. 啟動demo
有兩種方法:

  • 本地啟動server
    進入RNDemoApp目錄,啟動本地server

    yarn start
    

    android studio 啟動模擬器安裝應用,打開RN頁面,正常情況應該能正常連接到啟動的本地server并加載出js頁面


    連接本地packager.png
  • 將js工程打成離線bundle包,放在Android工程的assets目錄下
    創建assets目錄,在Android工程的main目錄下創建assets文件夾


    創建assets文件夾.png

    進到RNDemoApp js工程目錄下,執行打包命令,生成bundle包

react-native bundle --platform android --dev true --entry-file index.js --bundle-output ../../ReactNativeDemo/app/src/main/assets/index.android.bundle --assets-dest ../../ReactNativeDemo/app/src/main/res/

然后重新安裝應用,啟動后進入到RN頁面即可正常顯示。

現在已經有了源碼方式編譯的工程了,后續進行源碼的分析

??如果覺得對您有幫助,不妨點個贊??

參考文獻:
https://github.com/facebook/react-native/wiki/Building-from-source
https://reactnative.cn/docs/integration-with-existing-apps
https://blog.csdn.net/mu_xixi/article/details/79830527

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。