接触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页面
-
将js工程打成离线bundle包,放在Android工程的assets目录下
创建assets目录,在Android工程的main目录下创建assets文件夹
进到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