//首先判断外部指定路径下是否存在新下载的bundle文件

String bundleFile = FileUpdateManager.getExtraJSBundleFile(context);

if (FileUtils.exists(bundleFile)) {

//存在更新文件则直接将外部路径设置给ReactInstanceManager,也即RN使用热更新文件加载启动

return bundleFile;

}

//不存在更新文件则使用原来打包的assets路径

bundleFile = FileUpdateManager.getInnerJSBundleFile();

return bundleFile;

}

}

再看下FileUpdateManager.java的实现,如下:

public class FileUpdateManager {

public static final String BUNDLE_FILE_NAME = “index.android.bundle”;

public static final String BUNDLE_EXTRA_DIR = “RNHotUpdate”;

public static final String ASSETS_BUNDLE_PREFIX = “assets://”;

public static String getExtraHotUpdatePath(Context context) {

return context.getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + BUNDLE_EXTRA_DIR;

}

public static String getExtraJSBundleFile(Context context) {

return getExtraHotUpdatePath(context)+ File.separator + BUNDLE_FILE_NAME;

}

public static String getInnerJSBundleFile() {

return ASSETS_BUNDLE_PREFIX + BUNDLE_FILE_NAME;

}

}

到此具备 JS 和 res 图片资源的热更新超级基础版可以算 OK 了,就是判断有没有更新文件存在,有就在启动时使用更新文件的路径,没有就使用原来 assets 的路径,简单吧,至于为毛这么设置就能热更新了后面文章我会详细介绍,现在先记得就行,饥渴的话可以自己去翻下源码就明白了。

2、本地随便搭建一个服务器,各种集成环境也可以,方便接下来的测试。

3、准备更新包,记得不要和打入assets的一样,免得看不出明显效果,随便改个字体大小、颜色啥的,然后进行官方打包命令操作:

//$OUTPUT_PATH为你指定的一个输出路径

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output $OUTPUT_PATH/index.android.bundle --assets-dest $OUTPUT_PATH

此时会在 $OUTPUT_PATH 路径下看到如下输出:

将这些文件选中压缩成 update.zip 的压缩包,如下:

如上两步除过 index.android.bundle.meta 文件可以不要以外,剩下无论是文件夹还是文件名都不要修改,千万不要修改,压缩到根目录,至此一个更新包就做好了(差分包那些自己实现,这里是最简单的热更新实现)。

4、上面热更新超级简单版机制和更新包zip文件都已经有了,接下来就得找个合适时机去向服务端请求查询是否有更新、获取更新链接进行下载解压了;这里就是你需要依据自己项目情况实现的细节了,譬如渠道控制、版本控制等等一堆匹配校验,我们都略掉吧,重点看下怎么更新成功,那就暴力点,假设有更新(直接从指定路径拉取zip包吧),所以局部代码如下:

public class RequestManager {

//第二步让你搭建的服务器,把第三步做好的更新包扔到如下路径即可(是不是很暴力很直接!!!)。

public static final String HOT_UPDATE_URL = “http://10.20.185.22/rn_hot_update/test_cdn/update.zip”;

private Context mContext;

public RequestManager(Context context) {

this.mContext = context.getApplicationContext();

}

public void start() {

//开启线程后台下载更新包解压等操作(仅仅为Demo,不具备实用价值!!!!!)

new Thread(new Runnable() {

@Override

public void run() {

OutputStream output = null;

File zipDownloadFile = null;

try {

Log.i(“YYYY”, “----------bundle download start”);

URL url = new URL(HOT_UPDATE_URL);

HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();

InputStream input = urlConn.getInputStream();

File dir = new File(FileUpdateManager.getExtraHotUpdatePath(mContext));

if(!dir.exists()) {

dir.mkdirs();

}

//创建一个临时zip文件

zipDownloadFile = new File(FileUpdateManager.getExtraHotUpdatePath(mContext) + File.separator + “template”);

if(!zipDownloadFile.exists()){

zipDownloadFile.createNewFile();

}

output = new FileOutputStream(zipDownloadFile);

byte buffer [] = new byte[1024];

int inputSize = -1;

long totalSize = 0;

byte[] header = new byte[4];

while((inputSize = input.read(buffer)) != -1) {

if (totalSize < 4) {

for (int index=0; index

int headerOffset = (int)(totalSize) + index;

if (headerOffset >= 4) {

break;

}

header[headerOffset] = buffer[index];

}

}

totalSize += inputSize;

output.write(buffer, 0, inputSize);

}

output.flush();

//判断下的是不是一个zip,其实应该还有md5等各种校验的,这只是个Demo!!!!

boolean isRealZipFile = ByteBuffer.wrap(header).getInt() == 0x504B0304;

if (isRealZipFile) {

//解压文件到热更新目录供使用(仅仅是Demo,实际要考虑版本回退问题)

FileUtils.unzipFile(zipDownloadFile, FileUpdateManager.getExtraHotUpdatePath(mContext));

Log.i(“YYYY”, “----------bundle download and unzip OK”);

} else {

Log.i(“YYYY”, “----------bundle download, but not a zip file!”);

}

} catch (Exception e) {

Log.i(“YYYY”, “----------bundle download error”);

e.printStackTrace();

} finally{

//删掉下载解压后的zip文件

try{

if (zipDownloadFile != null) zipDownloadFile.delete();

if (output != null) output.close();

}

catch(Exception e){

e.printStackTrace();

}

}

}

}).start();

}

}

好了,在你合适的地方(譬如 RN Activity 启动后 onCreate 最后)进行如下调用:

new RequestManager(this).start();

至此一个从服务器下载更新包解压和再次进入界面展示热更新资源的超级简单热更新 JS 与 image 资源的方案就落地了。TT,其实远远不止这些,这只是给大家提供个思路,里面很多细节需要考虑的,版本回退、校验、查询、兼容性、容错、差分包等等,商业原因就不细细说明了,相信有了这个主要的核心思路,那些拓展完善大家都能做好的,而且是量身定做的。

怎么样,看似很神奇的 RN 热更新 JS 和 资源本质核心就这么回事。但是一直不明白为啥很多群里和论坛到处求助如何更新 RN 的 image 资源,我想说的是,如何更新自己去看代码啊,从代码找突破口啊!

3-6 RN 集成后 release 版本中可能存在的一个小概率崩溃

有了上面那些经历, React Native 基本就算 OK 了,但是无意间和同事讨论发现一个崩溃,追踪了一把才发现是个天坑(其实这个坑在 RN 低版本是不存在,后来的新版本不清楚为毛要这么干,不知道是不是失误);上面 release 版本的更新拽回来的 bundle 文件如果是一个人为搞错的文件打包成 zip 发布的话就会复现(这种 bundle 文件搞错,譬如有个 txt 文件,只是名字取成了 bundle 等,md5 校验也无力回天),虽然这种傻逼的做法不多,但是容错机制不能没有啊,不能让它在可预见的情况下崩溃啊,万一运营发版本傻逼了咋搞。

下面来看下比较新的 RN 版本 XReactInstanceManagerImpl.java 中 ReactContextInitAsyncTask 内部类的这个坑,具体先看下 RN 里 ReactContextInitAsyncTask 内部类的 doInBackground 方法,如下:

@Override

protected Result doInBackground(ReactContextInitParams… params) {

try {

JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();

return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));

} catch (Exception e) {

// Pass exception to onPostExecute() so it can be handled on the main thread

return Result.of(e);

}

}

看看 createReactContext 方法吧,如下:

/**

@return instance of {@link ReactContext} configured a {@link CatalystInstance} set

*/

private ReactApplicationContext createReactContext(

JavaScriptExecutor jsExecutor,

JSBundleLoader jsBundleLoader) {

final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);

//release 版本时 mUseDeveloperSupport 为 false,故忽略这一步逻辑

if (mUseDeveloperSupport) {

reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);

}

//依据我们外面是否设置了mNativeModuleCallExceptionHandler决定exceptionHandler的值,外部设置是通过ReactInstanceManager的builder时set的,默认没有设置

//坑就从这里慢慢开始了,如果我们设置了mNativeModuleCallExceptionHandler来处理全局 native 的异常,则exceptionHandler就被赋值为我们设置的啦,那我们继续往下看盯紧会发现它设置给了 CatalystInstanceImpl。

NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null

? mNativeModuleCallExceptionHandler

mDevSupportManager;

CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()

.setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())

.setJSExecutor(jsExecutor)

.setRegistry(nativeModuleRegistry)

.setJSModuleRegistry(jsModulesBuilder.build())

.setJSBundleLoader(jsBundleLoader)

.setNativeModuleCallExceptionHandler(exceptionHandler);

try {

catalystInstance.getReactQueueConfiguration().getJSQueueThread().callOnQueue(

new Callable() {

@Override

public Void call() throws Exception {

try {

//文件有问题,这里就会抛出异常到MessageQueueThreadImpl的callOnQueue方法中被捕获,然后通过SimpleSettableFuture的setException方法设置了该异常

catalystInstance.runJSBundle();

} finally {

Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);

ReactMarker.logMarker(RUN_JS_BUNDLE_END);

}

return null;

}

}).get();

//上面get SimpleSettableFuture时就会得到上面设置的setException,然后被下面的ExecutionException捕获!!!!!

} catch (InterruptedException e) {

throw new RuntimeException(e);

} catch (ExecutionException e) {

//异常抛出以后被包装以后传递给了AsyncTask的onPostExecute方法

if (e.getCause() instanceof RuntimeException) {

throw (RuntimeException) e.getCause();

} else {

throw new RuntimeException(e);

}

}

return reactContext;

}

接着看下抛到AsyncTask的onPostExecute方法,如下:

@Override

protected void onPostExecute(Result result) {

try {

setupReactContext(result.get());

} catch (Exception e) {

//来自上面包装的result.get()异常在此被捕获!!!!!!坑爹啊!!!!!此时mDevSupportManager为DisabledDevSupportManager(因为useDeveloperSupport=false),DisabledDevSupportManager直接抛出异常了,没鸟之前设置的mNativeModuleCallExceptionHandler!!!!!!!

mDevSupportManager.handleException(e);

} finally {

mReactContextInitAsyncTask = null;

}

}

具体抛出地方如下:

public class DefaultNativeModuleCallExceptionHandler implements NativeModuleCallExceptionHandler {

@Override

public void handleException(Exception e) {

if (e instanceof RuntimeException) {

// Because we are rethrowing the original exception, the original stacktrace will be

// preserved.

throw (RuntimeException) e;

} else {

throw new RuntimeException(e);

}

}

}

所以这是个坑爹故事!AsyncTask 的 onPostExecute 方法运行在主线程中,我在 ReactRootView 级别捕获个粑粑啊,所以一旦出现类似情况就只能让他崩溃了,给 ReactInstanceManager 设置setNativeModuleCallExceptionHandler 都没用啊,因为 release 中压根就不走这货!!!!

所以解决办法就是把上面 onPostExecute 的catch 中的 mDevSupportManager.handleException(e); 替换为如下:

NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null

? mNativeModuleCallExceptionHandler

mDevSupportManager;

exceptionHandler.handleException(e);

这样通过外部设置捕获就能抓住这个坑爹的异常了!!!

3-7 RN Android OEM ROM 上运行问题

1、MIUI ROM 上 Text 文本截断问题。

开发过程中遇到一个诡异的现象,手头几台设备都没有问题,在跑 MIUI ROM 的设备上 Text 被不同程度截断了,坑爹啊,尤其是 ListView、ScrollView、和固定宽度情况下各种被截断不居中,包括一些开源第三方库也存在这个问题,没办法,只能修改规避,毕竟是小米的锅,规避的办法依据不同场景有如下几种(自己可以继续尝试别的办法):

给 Text 设置 numberOfLines 属性; 给 Text 设置 flex 比例; 尽量给 Text 使用 padding 代替 margin;

总之在小米设备上最好还是打开显示布局边界好好看看关于 Text 的区域吧, OEM 都很坑爹的。

2、大多数 OEM ROM 上定制类转换异常问题。

就拿我前东家设备上的一个异常来说吧(别的设备也有,OPPO等),比较低版本的 RN 在 Flyme OS 上可能会遇见如下一个崩溃:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频 如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)

这里我就分享一份资料,希望可以帮助到大家提升进阶。

内容包含:Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。分享给大家,非常适合近期有面试和想在技术道路上继续精进的朋友。

如果你有需要的话,可以点击Android学习PDF+架构视频+面试文档+源码笔记获取免费领取方式

喜欢本文的话,不妨给我点个小赞、评论区留言或者转发支持一下呗~

[外链图片转存中…(img-tdWqkRm9-1710671277797)] [外链图片转存中…(img-Gjg18vty-1710671277798)] [外链图片转存中…(img-f1YsT6er-1710671277798)] [外链图片转存中…(img-SxuLBDgJ-1710671277799)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频 如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android) [外链图片转存中…(img-TSSyf9Go-1710671277800)]

这里我就分享一份资料,希望可以帮助到大家提升进阶。

内容包含:Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。分享给大家,非常适合近期有面试和想在技术道路上继续精进的朋友。

如果你有需要的话,可以点击Android学习PDF+架构视频+面试文档+源码笔记获取免费领取方式

喜欢本文的话,不妨给我点个小赞、评论区留言或者转发支持一下呗~

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: