本文主要分析在debug环境下Android是怎么加载到bundle文件的主要加载流程,不涉及太底层的代码均是Java代码分析。
开始
首先我们也在AndroidStudio中多多少少看过RN的源码,也知道它其实就是一个ReactRootView,而且是通过下面这段代码进行加载相对应的视图呈现我们要的UI效果:
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
mainComponentName,
getLaunchOptions());
可以知道mainComponentName这个是我们重写了ReactActivity中相对应的
public String getMainComponentName() {
return "RN_Demo";
}
但是ReactInstanceManager这个类了解的还是比较,根据代码的追踪还是比较容易的就找到了相对应的初始化代码:
protected ReactInstanceManager createReactInstanceManager() {
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(mApplication)
.setJSMainModulePath(getJSMainModuleName())
.setUseDeveloperSupport(getUseDeveloperSupport())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
.setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
for (ReactPackage reactPackage : getPackages()) {
builder.addPackage(reactPackage);
}
String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
builder.setJSBundleFile(jsBundleFile);
} else {
builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}
return builder.build();
}
为什么要看这些代码,主要是为了在debug环境的时候能明白这些参数是什么从哪里来,所以这里主要关系下面两个函数:
// 返回用于拼接bundle的名字
protected String getJSMainModuleName() {
return "index.android";
}
// debug的时候是返回true
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
记住这两个方法之后,我们继续看ReactRootView的加载代码:createReactContextInBackground -> recreateReactContextInBackgroundInner
然后这边涉及到了一个全新的类DevSupportManager这个是一个接口他的具体实现类:
DisabledDevSupportManager: 用于线上的版本
DevSupportManagerImpl: 用于debug的版本
// DevSupportManager的初始化方式:
mDevSupportManager =
DevSupportManagerFactory.create(
applicationContext,
createDevHelperInterface(),
mJSMainModulePath,
useDeveloperSupport,
redBoxHandler,
devBundleDownloadListener,
minNumShakes);
根据ReactInstanceManager的初始化我们可以知道 mJSMainModulePath = getJSMainModuleName() = “index.android” .. 然后我们很自然的跟进入看看是怎么初始化的原来是通过反射大法:
String className =
new StringBuilder(DEVSUPPORT_IMPL_PACKAGE)
.append(".")
.append(DEVSUPPORT_IMPL_CLASS)
.toString();
Class<?> devSupportManagerClass =
Class.forName(className);
Constructor constructor =
devSupportManagerClass.getConstructor(
Context.class,
ReactInstanceManagerDevHelper.class,
String.class,
boolean.class,
RedBoxHandler.class,
DevBundleDownloadListener.class,
int.class);
return (DevSupportManager) constructor.newInstance(
applicationContext,
reactInstanceManagerHelper,
packagerPathForJSBundleName,
true,
redBoxHandler,
devBundleDownloadListener,
minNumShakes);
当然如果是非debug的时候会返回:
if (!enableOnCreate) {
return new DisabledDevSupportManager();
}
不容易呀,终于是找到debug相关的类,既然初始化完成了那么我们就进入看看里面做了什么?
加载
我们在DevSupportManagerImpl的构造函数中重点关注一些重要类的初始化:
// DevServerHelper初始化
public DevServerHelper(DevInternalSettings settings, String packageName) {
mSettings = settings;
mClient = new OkHttpClient.Builder()
.connectTimeout(HTTP_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.build();
mBundleDownloader = new BundleDownloader(mClient);
mRestartOnChangePollingHandler = new Handler();
mPackageName = packageName;
}
很显然这里主要是初始化了一个OkHttpClient对象,自然这里肯定是用于请求地址用的。
mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);
初始化了一个ReactNativeDevBundle.js文件 很好一切都就绪之后开始执行mDevSupportManager.handleReloadJS这个方法:
public void handleReloadJS() {
// 其他的代码都忽略只看这部分的代码即可
PrinterHolder.getPrinter()
.logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
String bundleURL =
mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
reloadJSFromServer(bundleURL);
}
重要部分的来了mDevServerHelper.getDevServerBundleURL:
public String getDevServerBundleURL(final String jsModulePath) {
return createBundleURL(
mSettings.getPackagerConnectionSettings().getDebugServerHost(),
jsModulePath,
getDevMode(),
getJSMinifyMode(),
mSettings.isBundleDeltasEnabled());
}
跟着流程继续看是怎么拼接host地址的getDebugServerHost进入这个方法最后会发现:
public static final String EMULATOR_LOCALHOST = "10.0.2.2";
public static final String GENYMOTION_LOCALHOST = "10.0.3.2";
public static final String DEVICE_LOCALHOST = "localhost";
private static String getServerIpAddress(int port) {
// Since genymotion runs in vbox it use different hostname to refer to adb host.
// We detect whether app runs on genymotion and replace js bundle server hostname accordingly
String ipAddress;
if (isRunningOnGenymotion()) {
ipAddress = GENYMOTION_LOCALHOST;
} else if (isRunningOnStockEmulator()) {
ipAddress = EMULATOR_LOCALHOST;
} else {
ipAddress = DEVICE_LOCALHOST;
}
return String.format(Locale.US, "%s:%d", ipAddress, port);
}
这边系统还会判断是否是Genymotion或者自带的Emulator模拟器,当然这些我们都没有设置,所以这里直接返回的是DEVICE_LOCALHOST这个本地的地址,所以最后的拼接出来的的地址是:localHost:8081 .. 然后我们回到createBundleURL这个方法中得到最最最最最终的拼接地址是:
http://localHost:8081/index.android.bundle?platform=android&dev=true&jsMinify=false
然后我们前面已经初始化好了OKHttpClient对象,接下来就是执行这个地址文件的下载,并下载到mJSBundleTempFile也就是ReactNativeDevBundle.js这个文件并保存到我们Context.getFilesDir路径下面。
最后我们下载成功之后会回调到ReactInstanceManager类的
private void onJSBundleLoadedFromServer() {
Log.d(ReactConstants.TAG, "ReactInstanceManager.onJSBundleLoadedFromServer()");
recreateReactContextInBackground(
mJavaScriptExecutorFactory,
JSBundleLoader.createCachedBundleFromNetworkLoader(
mDevSupportManager.getSourceUrl(), mDevSupportManager.getDownloadedJSBundleFile()));
}
然后继续recreateReactContextInBackground这个方法下去,这个方法那就做太多事情了,里面涉及到jni相关的能力有限分析不下去了,所以就此结束了。
总结
在我们写完RN代码的时候 react-native run-android 命令启动后你能看到: 我们会启动一个Http服务并监听8081端口,然后我们把我们写的效果经过Android一系列的构造流程打包成apk并安装。在我们敲入命令的时候我们会发现:
request:/index.android.bundle?platform=android&dev=true
大致原理:RN会在我们本地帮我们把相关的数据打包完成并上传到这个地址去,所以我们在debug的时候可以通过下载得到相对应的bundle文件。
额外发现
我们在生成这个下载地址的时候是否发现这么一行代码:
public String getDebugServerHost() {
String hostFromSettings = mPreferences.getString(PREFS_DEBUG_SERVER_HOST_KEY, null);
if (!TextUtils.isEmpty(hostFromSettings)) {
return Assertions.assertNotNull(hostFromSettings);
}
return host;
}
也就是说如果PREFS_DEBUG_SERVER_HOST_KEY这个对应的Preferences不为空那么我们就从这个地址上加载相对应的bundle文件,所以我们可以根据这个原理弄个输入框只要写入ip以及端口我们就能读取别人写好的RN并实现调试了,如:
PreferenceManager.getDefaultSharedPreferences(applicationContext)
.put("192.168.1.1:8081");
是吧,这样子我们就能直接读取这个ip下的bundle文件了,当然前提是要存在相对应的bundle文件。
稍微走了一遍流程清楚多了只怎么个加载原理,当然Android跟RN交互的底层代码还是很大的一部分代码,有待分析理解。