React Native Android 从学车到补胎和成功发车经历


【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

1 背景

好几个月没发车了,完全生疏了,为了接下来能持续性的发好车,这次先准备发个小车—— React Native。没错,就是这个从去年到现在官方都憋不出大招 1.0 版本,而被我朝开发者疯狂追捧备受争议的破车。怎么说呢,这玩意刚出来时有了解过,当时的内心是抵触的,但是内心总是架不住天朝的炒作能力,更架不住硬性指标,于是我就这么被 React Native 蹂躏了一番,也就有了下面这次补胎经历。

虽然已经被发车了,但是还是发表一下自己的观点吧(看完观点赞同的就继续观赏,不赞同的勿喷,请绕道即可,当然,那些已经上高速的司机直接绕道吧),个人看法如下:

作为 Android 开发者来说,对待 RN 个人建议要保持一个端正的态度,什么原生 Android App 已死、RN 很牛逼之类的话听听就行了;至少到目前为止个人觉得原生开发才是王道,RN 也就只能胜任一些常规的 CS 模式应用,整体还是很弱的,不要告诉我它支持很方便的封装 Native UI 和 Module 到 js ,这就是扯蛋,除过一些通用 SDK 接口封装具备一定价值以外,个性化 UI 封装有毛用,因为封装不仅加多了开发工作量,还丧失了一定需求下的热更新能力(被封装的 Native 接口变化依旧需要通过发版本才能解决),而 RN 被最看好的无非就是一个合理简单不具备黑科技的热更新能力和 Native 般的UI 体验。

对于大多数 Android 开发者来说也有过与前端打交道联调的时候,RN 如果能发车带来的好处还是挺直接的,最起码能促使你去了解前端,了解一些前端开发模式和框架及思维,这不仅拓展了我们视野,还能对我们做原生开发一些启发(譬如 Flux 架构在 Android 上的一些应用中也是很棒的);也能促使我们去掌握一些 JavaScript 语言,避免与前端对接的一些尴尬。

对于大公司来说这个可以作为一个课题进行预研和局部尝试接入使用(崩溃率KPI),但是对于小创业公司来说前期可能这是最佳的选择,因为小公司对于人力成本、开发能力、速度等个方面都有不小的挑战,所以 Learn once, do anywhere. 可以给他们带来更好的受益。

技术是无罪的,所以即便 RN 现在胜负难断,但是作为一个开发者对自己关注领域的新技术应该尽可能的持有一个关注的心态,以免真的能颠覆时找不到赛道,更别提发车,更何况现在已经有很多 JD 竟然列出了 React Native 开发工程师的职位(包括鹅厂),薪水福利也还不错。

PS:如果你依旧对 RN 抱着怀疑的心态,那请你打开这个 showcase 看看吧,国内外已经有很多有名和没名的 App 都已经接入了 RN。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

2 React Native 学习历程

虽然 React Native 官方版本迭代很快,加上版本迭代也基本不考虑前向兼容性,网上相关基础文章也很多,在这里我依旧想传送们一把我补胎过程中的一些网络资料,方便聚合。下面就是假设你是一个 RN 小白到能开发一些应用的学习历程和打开姿势:

1、首先你得有个开发环境,本人在 Windows 下和 Ubuntu 12.04 LTS、Ubuntu 16.04 LTS上面都配过,按照官方文档来就行,没啥坑,但是敲命令前要大概了解下命令意思,具体参考文档如下(不用完全按照文档来,Android环境如果是 OK 的情况下注意细节以后安装个 node 环境就行了,别的是辅助的):

官方英文环境搭建教程
非官方中文社区翻译环境搭建教程

2、你得有个好写代码的 IDE,坑爹的是 React Native 是 JS、Android、iOS 工程的混合体,通过 npm 来管理的,所以除过 Android IDE 以外你得有个写 JS 的IDE,极力推荐 Visual Studio Code,上网搜搜吧,一堆配置教程,插件能很到位的支持断点调试(我还是喜欢 Chrome 调试)和 RN 代码提示;别的 IDE 也可以,自己看着玩吧。

3、有了第一步搭建环境,说明你已经用一个 Demo 工程验证了环境,但是你此时应该还是闷逼的,友情提醒到此时先别干别的,打开你工程根目录的 package.json 文件,看着是不是一堆配置,怎么有点 gradle 的感觉,哈哈,他就是一个配置管理文件,你现在需要去了解一下 npm 命令和 package.json 文件格式相关的东西,这样你就彻底明白了第一步那些命令的意思和接下来如何在 RN 中使用第三方库的姿势啦,同时也就明白 RN 工程上传代码不用上传 node_module,只用 package.json,简直就是一举两得,传送门如下:

package.json官方英文文档
package.json第三方中文文档
npm命令相关第三方文档博客

到此你再回头看你 RN Demo工程是不是就明白咋回事啦,不过你可能会发现在第一步初始化工程时巨慢,这是坑爹中国特色,所以建议你用上面刚刚学到的 npm 命令给自己重新换个镜像源,譬如淘宝啥的,这样你会发现速度明显上去啦,镜像源相关文档参考如下:

换镜像源的几种方法博文
国内好的镜像源推荐

4、这时候你要是有 JS 基础和 React 概念则直接开搞,没有那就建议你先学习一把 JS 和 React,同时学习下 ECMAScript 6 的语法,推荐如下:

《ECMAScript 6入门》阮大大写的,很棒,很入门
React、JSX等相关前端技术汇总贴

5、上面这些你都差不多搞 OK 了以后就进入真正的 React Native了,具体参看如下:

React Native官方文档
React Native官方文档中文翻译

这时候的路线应该是按照文档一个一个的敲一遍,理解验证,主要就是 state 和 props 思维的转变;文档简单撸一遍以后你已经入门啦(不用每个属性都验证,每个属性都要看一遍,但是不用都验证)。

6、此时你应该去网络搜索一些 React Native 的开源项目学习学习,观摩观摩他们写法,这个最多花上一两天就是突飞猛进的节奏。

7、这时候你会发现开源项目里大家怎么用了那么多第三方库,那你是不是该研究下第三方库啦,最经典的和必须掌握的有 React-Redux,这个网上一堆,大多都是前端工程师分析的,具体也可以看看官方文档。如果你想使用 RN 第三方库除啦可以上 github 进行搜索以外,推荐如下网站搜索第三方库:

常备第三方库搜索地

8、此时应该说你基本具备了 RN 的搭建开发能力了,但是集成进现有 App 会发现是个大坑,那就自己慢慢踩,一般这个没有太大共性,譬如我们项目还是 Ant 编译,还在迁移 gradle,所以更加麻烦。

9、上面 OK 以后就该搞搞热更新啦,新版本的 RN 在微软提交 PR 以后已经成为了具备热更新 JS 和资源的模式了,只是相关策略等看你们怎么处理了,是用第三方还是自己搞,我们目前热更新是要自己搞,不想依赖别人服务器等。

10、上面这些技能都差不多了以后,当然不能放过一个装逼大招啊,那就是源码分析啊,其实在我看来学习 RN 的精髓就在于 RN 源码框架的阅读,你会发现 Facebook 的工程师们真的很聪明,他们才是真正的全栈应用型,总之阅读 RN 源码会给自己代码非常多的感触,完全就是一个全新的思路,从 JS 到 C++ 到 JSC 核心引擎,再到 Java,完全就是一个学习的活宝,代码量没有系统那么复杂,却又表现出一个系统 shell 层一样的思想,唉,总之很叼,这一步核心看懂就行了。

11、到这里基本上你 RN 已经 OK 了,剩下的就是搞搞性能优化,依旧使用 Android 的优化工具,譬如 systrace 等;另一方面就是搞搞 JS 和相关牛逼控件的编写能力。然后最后的大招就是看看 RN 的编译脚本和裁剪及相关源码性能优化、譬如 RN 的 ListView 性能问题和集成项目共用网络、图片库等问题(不过这一步建议自己项目组酌情考虑,因为 RN 升级太快,团队人力不足的情况下还是慎重裁剪,不过自己玩玩也是可以的)。

到此 RN 从考驾照到修车到发车就基本 OK 了,这个过程依据你的理解能力和学习能力及是否有相关背景知识来决定时常,通常来说 RN 还是很好理解的,因为它再怎么的也只是一个第三方框架,一个牛逼的框架而已(所以及其不建议观看视频,尤其是一些搞营销卖的视频,要相信自己的理解能力)。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

3 React Native 项目踩坑记

关于 React Native 项目开发到集成其实是有很多坑的,网上也有很多文章给出了各种攻略,但我个人还是倾向于 React Native一旦翻车以后尽量使用如下工具进行呼救:

React Native github 开源项目 issues 中搜索你遇到的问题关键词

stackoverflow 中搜索你遇到的问题关键词

到此 RN 翻车踩坑的问题基本上在上面这两个地方都能找到解释的,其他的就需要自己看源码和自己依据自己项目进行总结了,下面总结下我认为我遇到的最坑的 RN 填坑记录。

3-1 最低版本兼容性问题

现有项目的 minSdkVersion=14,而 RN 最低支持 API 16;当遇上这个问题时心里是操蛋的,到底是将现有项目 minSdkVersion 升级到 16 还是保持 minSdkVersion 为 14 呢?其实都是可以的,这个取决于你项目目前在 16 以下用户量有多少的问题,如果不多则可以一劳永逸直接升级到 16,如果多的话则可以让 RN 兼容 现有项目 minSdkVersion 编译打包,自己在入口处做好判断即可,支持 RN 的版本走一套, 不支持的走另一套(WEB 或者 别的 Native)或者压根不显示,顶多就是在 16 以下的用户能正常使用你 App 但是没有 RN 这部分功能而已。具体做法如下:

按照文档 gradle 添加相关 RN 依赖和仓库路径等,然后直接尝试编译一次吧,你会得到类似如下错误:
这里写图片描述
坑爹, RN 最低支持 API 16,我又不想改项目 minSdkVersion,看样子只能是上面说的添加 tools:overrideLibrary=”com.facebook.react” 来绕过了,自己做好判断,具体如下:

//AndroidManifest.xml

<uses-sdk tools:overrideLibrary="com.facebook.react"/> 
//如果有多个库有异常,则逗号分割即可,这样AndroidManifest.xml合并时就忽略了最低版本的限制

3-2 依赖冲突问题

由于原来项目中已经存在许多lib依赖,RN 集成进来以后编译会有依赖冲突,这个其实没啥的,依据报错或者执行 gradlew andDep 查看下哪些需要解除即可,譬如如下:

#$gradlew andDep
......
\--- com.facebook.react:react-native:0.33.0
     +--- LOCAL: infer-annotations-1.5.jar
     +--- com.facebook.fresco:imagepipeline-okhttp3:0.11.0
     |    +--- com.facebook.fresco:fbcore:0.11.0
     |    \--- com.facebook.fresco:imagepipeline:0.11.0
     |         +--- com.android.support:support-v4:23.2.1
     |         |    \--- LOCAL: internal_impl-23.2.1.jar
     |         +--- com.facebook.fresco:fbcore:0.11.0
     |         \--- com.facebook.fresco:imagepipeline-base:0.11.0
     |              +--- com.android.support:support-v4:23.2.1
     |              |    \--- LOCAL: internal_impl-23.2.1.jar
     |              \--- com.facebook.fresco:fbcore:0.11.0
     +--- com.facebook.soloader:soloader:0.1.0
     +--- org.webkit:android-jsc:r174650
     +--- com.facebook.fresco:fresco:0.11.0
     |    +--- com.facebook.fresco:drawee:0.11.0
     |    |    +--- com.android.support:support-v4:23.2.1
     |    |    |    \--- LOCAL: internal_impl-23.2.1.jar
     |    |    \--- com.facebook.fresco:fbcore:0.11.0
     |    +--- com.facebook.fresco:fbcore:0.11.0
     |    \--- com.facebook.fresco:imagepipeline:0.11.0
     |         +--- com.android.support:support-v4:23.2.1
     |         |    \--- LOCAL: internal_impl-23.2.1.jar
     |         +--- com.facebook.fresco:fbcore:0.11.0
     |         \--- com.facebook.fresco:imagepipeline-base:0.11.0
     |              +--- com.android.support:support-v4:23.2.1
     |              |    \--- LOCAL: internal_impl-23.2.1.jar
     |              \--- com.facebook.fresco:fbcore:0.11.0
     +--- com.android.support:recyclerview-v7:23.0.1
     |    \--- com.android.support:support-v4:23.2.1
     |         \--- LOCAL: internal_impl-23.2.1.jar
     \--- com.android.support:appcompat-v7:23.0.1
          \--- com.android.support:support-v4:23.2.1
               \--- LOCAL: internal_impl-23.2.1.jar

查看完依赖冲突关系以后在项目中解除即可,如下:

//build.gradle中各种姿势的exclude掉依赖就行了
compile ("com.facebook.react:react-native:+"){ // From node_modules.
    exclude module: 'cglib' //by artifact name
    exclude group: 'org.jmock' //by group
    exclude group: 'org.unwanted', module: 'iAmBuggy' //by both name and group
}

当然啦,如果你是修改过 RN 源码工程然后将源码引入的模式,依赖摘除也类似,这都是 Android 开发的必备技术了,不再多提了。不过如果你想裁剪优化 RN 则这里的依赖可以不摘除,直接想办法替换为自己项目共用已有优质 lib 即可,只不过这个过程依据团队规模和投入慎重考虑,因为 RN 版本太快,合并代码很苦逼。

3-3 动态 so 库加载策略问题

现有项目中为了安装包体积和 CPU 兼容性问题,所有 so 动态库都是放在 armeabi 目录下的,没有其他目录,而 RN 却只支持编译如下 so:

//RN 的 Application.mk
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-9

这他妈就尴尬了,你提供 SDK 竟然不考虑提供完整的 ABI 编译支持。那我只能自己想办法了,首先想到的就是你不提供我就自己编译呗(前提是将 RN 以源码形式集成进项目),于是在 RN 的 Application.mk 的 APP_ABI 多添加了一个armeabi(别问我为何加在这里,后面等我写 RN 编译链分析你就明白了,别问我这是啥语法,这是 Android 开发应该必备的技能,和 RN 无关),在 build.gradle 中也对应只添加过滤 armeabi,然后编译了一把报错了,坑爹啊,依据错误信息一查看发现是有一处 Android.mk 执行时找不到一个文件,具体如下:

//编译报错的Android.mk文件路径
//react-native\ReactAndroid\src\main\jni\third-party\jsc

//Android.mk内容
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= jsc
LOCAL_SRC_FILES := jni/$(TARGET_ARCH_ABI)/libjsc.so //编译真实报错地方
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
include $(PREBUILT_SHARED_LIBRARY)

TARGET_ARCH_ABI 这玩意已经很明显了,做过 Android 都知道,指定是编译 armeabi ABI 时找不到 libjsc.so 文件,那就看看这个 so 是哪儿来的吧,通过 RN 源码自己的 build.gradle 可以看见如下:

// Create Android.mk library module based on so files from mvn + include headers fetched from webkit.org
task prepareJSC(dependsOn: downloadJSCHeaders) << {
    copy {
        from zipTree(configurations.compile.fileCollection { dep -> dep.name == 'android-jsc' }.singleFile)
        from {downloadJSCHeaders.dest}
        from 'src/main/jni/third-party/jsc/Android.mk'
        include 'jni/**/*.so', '*.h', 'Android.mk'
        filesMatching('*.h', { fname -> fname.path = "JavaScriptCore/${fname.path}"})
        into "$thirdPartyNdkDir/jsc";
    }
}

dependencies {
    ......
    compile 'org.webkit:android-jsc:r174650'
    ......
}

这脚本已经告诉你 libjsc.so 是来自 org.webkit:android-jsc:r174650 这个依赖的,坑爹啊,上 maven 去下了一个解压 aar 包打开 libs 目录才惊讶的发现妹的 android-jsc 这货没提供 armeabi ABI 的 libjsc.so,怪不得会报错,也是由此猜想到 RN 为毛作为一个 SDK 不遵守 SDK so 提供的基础准则,估计是它用了 jsc,jsc也没有 armeabi ABI 的原因(这个初步是判断是这样的,至于 android-jsc 这个 lib 能不能自己下源码编译 armeabi 的 so还没研究。。。。。时间有限,啥时候闲了一定要下一份源码编译一下),所以这条路就这么浪费了我接近一个小时的时间,然后还是暂时失败了。

这时候不得不重新换个思路了,想了想还是在这一版先放弃 x86 吧(第一版接入就完美几乎不可能),但是放弃 x86 以后 armeabi-v7a 与 现有 armeabi 的目录在加载 so 时还是存在问题啊,就是那个坑爹的策略问题,所以直接暴力测试了一下,修改编译脚本,armeabi-v7a 编译好以后剪切到 armeabi 下再打包(为了简单测试,你还可以直接解压现有 apk 中 armeabi-v7a 目录的文件复制到 armeabi 下打包测试,最后再修改脚本),然后通过 Build 辅助类进行架构判断。 此坑暂时 mark,T_T,过几天有时间了要仔细搞下这个 so 兼容性问题。

3-4 RN 模块拖垮现有项目问题

接入 RN 前后对比了一下内存开销,确实大了不少,加之 RN 官方自己 ListView 的性能问题没有很好的解决,所以从各方面来说都有点不太放心,于是采取了多进程的方式,让 RN 模块运行在独立进程中,这样的话就能避免很多尴尬。

不过多进程方式还是建议不要通过使用 Application 注册配合 ReactActivity 方式,推荐高内聚的模块化方式,也就是自己 Activity implements DefaultHardwareBackBtnHandler,自己 setContentView 一个 ReactRootView 的方式。至于怎么多进程我想不用说了吧,做安卓的自己看着办。

3-5 RN 集成后热更新核心思路

RN 自身是不具备热更新全套机制的,尤其是比较老的低版本 RN 想要热更新是很费劲的,要做很多事情才能支持 JS 和 图片resource 的热更新;但是比较新版本的 RN 不存在这个问题了,因为有 PR 已经重新搞了 JS 和 resource 加载这块逻辑,所以热更新变得容易了很多,不过新版本中却又搞出了一个新的致命坑,下面第6点详细说明,这里还是探讨热更新。其实简单的热更新说白了就是一个典型的CS流程,客户端发起请求查询更新,依据返回 JSON 决定是否去 CDN 下载新包,然后客户端在指定新包路径 load 启动即可,大体如下流程:

这里写图片描述

其实你所看到的市面上的各种 RN 热更新框架无非都是这个主线,只是更加健壮和高效而已,譬如 CodePush 实质就是这么回事,但是我们不想受限服务器类型、也不想使用他人服务器,所以有必要自己搞一套热更新。出于商业项目问题,这里接下来只提供如何快速搞一个简单的热更新框架,其他细节需要自己完善,具体做法如下:

1、在现有代码中进行如下代码修改来支持热更新。

//在 RN ReactInstanceManager构造中通过setJSBundleFile方法设置外部热更新文件保存路径
mReactInstanceManager = ReactInstanceManager.builder()
......
.setJSBundleFile(RNHotUpdateAndroid.getJSBundleFile(this))
......
.build();

紧接着创建一个RNHotUpdateAndroid.java的类,实现如下:

public class RNHotUpdateAndroid {
    //上面setJSBundleFile方法设置的路径来自此处
    public static String getJSBundleFile(Context context) {
        //首先判断外部指定路径下是否存在新下载的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<inputSize; 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<ReactApplicationContext> 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<Void>() {
          @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<ReactApplicationContext> 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 上可能会遇见如下一个崩溃:

com.meizu.widget.OverScrollerExt cannot be cast to android.widget.OverScroller

典型的中国特色啊,好在这个问题在今年9月下旬被官方合并解决了,具体可以参考 issues。所以遇见这个问题就升级一下 RN 的版本吧,这样就解决了!

3-8 RN 集成后各种持续性优化的问题

关于 React Native 集成进项目以后 Android 一侧的性能优化就不再提了,我以前专门写过一篇,感兴趣的移步 《Android应用开发性能优化完全分析》即可。额外需要关注的性能优化就不多说了,参考下面一些官方文档即可:

Performance
性能

Profiling Android UI Performance
调试Android UI性能

除过官方一些文档以外推荐一些优质文章,关于 RN 优化等,如下:

腾讯 Dev Club React Native 项目实战总结
React Native For Android 架构初探

携程魏晓军:React Native Bundle拆分
那些携程火车票业务在RN实践中踩过的坑
携程是如何做React Native优化的
React Native 实践之携程 Moles 框架

相信通过看完上面这些优质资源和这些厂商们的实践经历对你还是有不小启发的吧,至少对我们而言是受益匪浅的,所以不要只是盲目的写 “React Native Demo” 项目,真正践行上线才是硬伤,能上线就预示着能成功发车了。

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

4 一点感想总结

现在市面 RN 的 App 一堆,但是没几个是考虑商业环境的,大多数算是 demo 级别,很多文章也一样,都在说 RN,却没几个真正记录踩坑经历的,除过领航的前几个梯队,譬如腾讯、阿里、携程等,其他基本就是在炒概念。随着移动 App 开发越来越成熟、需求迭代越来越频繁,开发人员已经越来越崩溃了,所以近年来火起来了很多热门词汇,热更新、热修复、动态加载、分包、DCloud、LuaView、React Native、weex、微信小程序等,不得不说移动 Android App 开发也在尝试探索各种便捷、高效的开发方式。所以对于 RN 个人建议依旧是在目前阶段最多接入部分业务即可,适可而止,特别是活动也面比较适合,可能比 H5 体验好很多。还有就是 RN 的接入你要做好一个背锅的心态,不确定性太多,可参考性已知问题过少,很多问题都只能靠发布上线来验证。

关于 React Native 的编译脚本分析、主框架分析、热修复原理分析后面会详细写文章介绍。

这里写图片描述

【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】

$(function () { $('pre.prettyprint code').each(function () { var lines = $(this).text().split('\n').length; var $numbering = $('').addClass('pre-numbering').hide(); $(this).addClass('has-numbering').parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($('').text(i)); }; $numbering.fadeIn(1700); }); });