接觸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. 準備工程目錄
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 $ENVexport 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工程的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目錄,啟動本地serveryarn 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