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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352