React Native for Android 发布独立安装包

昨天发布了 ZhiHuDaily-React-Native 项目,有不少人问有没有 APK 包来体验一下。做了 React Native Android 开发的话,就会知道,开发的时候必须启动个 JS Server,然后要让手机连接这个 Server,否者会出现那个"吓人"的红色屏幕。这个我在第一篇 React Native 文章中就提到过。

如果要发布一个 React Native 写的 Android 应用,不可能要别人来连接这个 JS Server。可不可以不要连接这个 Server 就能运行呢?在网上找了一圈,发现资料很少,官方文档上也没有说支持。这篇文章就来讨论一种实现方案。

原理探究

我们来看一下 RN 应用连接这个 JS Server 干了啥事情?第一次运行应用的时候,会提示正在加载 JS 文件。应该就是连接这个 JS Server 下载 JS 文件吧,我们同时看到 JS Server 的窗口中打印出如下的 log:

Running packager on port 8081.  
...
transforming [========================================] 100% 330/330  
[19:45:44] <START> request:/index.android.bundle?platform=android
[19:45:44] <START> find dependencies
[19:45:44] <END>   find dependencies (127ms)
[19:45:44] <START> transform
[19:45:45] <END>   transform (490ms)
[19:45:45] <END>   request:/index.android.bundle?platform=android (630ms)
::ffff:10.237.208.110 - - [Thu, 24 Sep 2015 11:45:50 GMT] "GET /flow/ HTTP/1.1" 404 - "-" "okhttp/2.4.0"

可以看到一些信息,这是在访问 /index.android.bundle?platform=android 这个连接,服务器端口号是 8081。在浏览器中打开这个连接:http://localhost:8081/index.android.bundle?platform=android ,果然能加载一个文件,里面都是我们写的 React Native 的 JS 代码。这个时候我们在手机上看一下 APP 的应用数据,以我们这个知乎日报的客户端为例,应用数据路径在手机上的 /data/data/com.rctzhihudaily/ 目录(手机需要 Root 才能看到这个路径)。在这目录下 files 文件夹下找到一个 ReactNativeDevBundle.js 文件,文件内容就是我们刚才在浏览器打开那个连接得到的内容。

哈,我们快接近真相了。我尝试重新安装一个新的 APK,然后把这个文件 push 到对应的目录下,修改一下权限,果然,不需要连接 JS Server 就能打开了。

总结一下,第一次打开 React Native 应用的时候,会连接 JS Server 下载一个 ReactNativeDevBundle.js 文件,然后放到应用数据的 files 目录下,就能运行这个 JS 文件了。到这里我们也就找到解决方案了。

解决方案

知道原理了,我们就很好解决了,只要我们把这个 ReactNativeDevBundle.js 文件提前打包到 APK 中,然后 APK 运行的时候,把这个文件释放到 files 目录中。

我们可以把 ReactNativeDevBundle.js 先保存下来,放在 Android 工程的 assets 目录中,这会自动打包到 APK 中。在 APP 第一次运行的时候,把文件拷贝到目的位置,代码如下:

public class MainActivity extends Activity {

    private static final String JSBUNDLE_FILE = "ReactNativeDevBundle.js";

    private static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while((read = in.read(buffer)) != -1){
            out.write(buffer, 0, read);
        }
    }

    private void prepareJSBundle() {
        File targetFile = new File(getFilesDir(), JSBUNDLE_FILE);
        if (!targetFile.exists()) {
            try {
                OutputStream out = new FileOutputStream(targetFile);
                InputStream in = getAssets().open(JSBUNDLE_FILE);

                copyFile(in, out);
                out.close();
                in.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        prepareJSBundle();
        ...
    }
}

使用实例可以参考这里:MainActivity.java

当然,这样已经可行了,但是还不够好,如果每次都要手动去添加这个 ReactNativeDevBundle.js 文件,难免会出错,或者遗漏。我这里在 build.gradle 中添加一个 task,让它每次打包的是自动访问 http://localhost:8081/index.android.bundle?platform=android 下载文件到 app/src/main/assets/ 目录中,脚本如下:

final def TARGET_BUNDLE_DIR = 'app/src/main/assets/'  
final def TARGET_BUNDLE_FILE = 'ReactNativeDevBundle.js'  
final def DOWNLOAD_URL = 'http://localhost:8081/index.android.bundle?platform=android'

task downloadJSBundle << {  
    def dir = new File(TARGET_BUNDLE_DIR)
    if (!dir.exists()) {
        dir.mkdirs()
    }
    def f = new File(TARGET_BUNDLE_DIR + TARGET_BUNDLE_FILE)
    if (f.exists()) {
        f.delete()
    }
    new URL(DOWNLOAD_URL).withInputStream{ i -> f.withOutputStream{ it << i }}
}
// 保证每次编译之前,先下载 JS 文件
preBuild.dependsOn downloadJSBundle  

这样,每次打包的时候,都会先帮我们下载好 JS 文件到指定位置了。当然,打包的时候,你要保证你的 JS Server 是开着的。完整代码可以参考:build.gradle

到这里,我们就实现了一个可行的方案了,可以独立发布 APK 了。

思考和改进

上面我们已经实现了一个可行方案了。这里我们也看到了一个更大潜在的特性——在线更新。通过上面的实现,我们可以看到,只要通过网络下载并替换这个 JS 文件,就可以实现 APP 的更新。不需要下载 APK 包更新,也不需要市场发布,只要后台上线一个新的 JS,客户端就能立即更新了。这就绕过了应用市场,解决了应用更新困难的问题,修复 BUG 一秒上线,新 Feature 一秒到达用户,这个特性是不是很颠覆?!!!

最后,值得注意的是,我这里只是一个简陋的解决方案,这里有些问题需要改进。首先,我们的 JS 文件都是明文的,基本上就是你的源代码,用在生产环境的话,做混淆是必须的。相信官方很快也会出标准的解决方案,毕竟 iOS 已经支持了。另外,如果要做在线更新的话,需要保证你更新 JS 的服务器的安全,因为这些 JS 代码可以直接运行到用户手机上。

最后附上 ZhiHuDaily-React-Native 的 APK,可以下载体验一下。

Read more

Android 上的低功耗蓝牙实践

这是我在 Droidcon Beijing 2016 和 GDG Devfest 2016 上做的分享,以下是正文: Slide 01 我今天分享的主题是 Android 上低功耗蓝牙的实践。这个主题比较小众。我在过去的一年多的时间里,主要是在做低功耗蓝牙相关的开发。接触过程中发现,BLE 的开发和通常的 Android APP 的开发有点不一样,这里需要访问硬件资源,而且涉及到一些协议相关的内容,而且这方面的资料也比较少。今天我从 Android 开发者的角度,来分享一下低功耗蓝牙开发实践。 Slide 02 今天分享的内容,主要包含如下几个部分:首先对蓝牙和低功耗蓝牙做一个简单的介绍;然后介绍 Android 上对低功耗蓝牙的支持;再介绍一下在 Android 平台上可以开发哪些低功耗蓝牙应用;然后是,开发过程中,可以帮助我们调试的工具;最后,总结一下所谓的 “最佳实践”,低功耗蓝牙开发的一些小经验。 Slide

By Race604

React Native 触摸事件处理详解

触控是移动设备的核心功能,也移动应用交互的基础,Android 和 iOS 各自都有完善的触摸事件处理机制。React Native(以下简称 RN)提供了一套统一的处理方式,能够方便的处理界面中组件的触摸事件、用户手势等。本文尝试介绍 RN 中触摸事件处理。 1. RN 基本触摸组件 RN 的组件除了 Text,其他组件默认是不支持点击事件,也不能响应基本触摸事件,所以 RN 中提供了几个直接处理响应事件的组件,基本上能够满大部分的点击处理需求TouchableHighlight, TouchableNativeFeedback, TouchableOpacity 和 TouchableWithoutFeedback。因为这几个组件的功能和使用方法基本类似,只是 Touch 的反馈效果不一样,所以一般我们用 Touchable** 代替。Touchable** 有如下几个回调方法: * onPressIn:点击开始; * onPressOut:点击结束或者离开; * onPress:单击事件回调; * onLongPress:长按事件回调。 它们的基本使用方法如下,

By Race604

React Native 中 ScrollView 性能探究

1 基本使用 ScrollView 是 React Native(后面简称:RN) 中最常见的组件之一。理解 ScrollView 的原理,有利于写出高性能的 RN 应用。 ScrollView 的基本使用也非常简单,如下: <ScrollView> <Child1 /> <Child2 /> ... </ScrollView> 它和 View 组件一样,可以包含一个或者多个子组件。对子组件的布局可以是垂直或者水平的,通过属性 horizontal=true/false 来控制。甚至还默认支持“下拉”刷新操作。另外还有一个特别赞的特性,超出屏幕的 View 会自动被移除,从而节省资源和提高绘制效率。我们来看如下一个例子: class

By Race604

30 天入门 Android 开发, Google 与你一起圆梦

经常会有朋友让我推荐 Android 开发入门的教程或者视频,我一直是推荐看官方的教程。大部分人或者觉得比较迷茫,或者觉得坚持不下去。这次推荐这个《30 天入门 Android 开发》是 Google 亲自发起的免费教学,以学习小组方式,大家可以一起学习和交流。一个好的开始,是成功的一半。让 Google 工程师带领你一起进入多彩的 Android 开发大门。点击这里 报名。 Android 设备已经随处可见,你想尝试一下在 Android 设备上的开发和创新吗?快来跟随 Google 的步伐,一起学习 Android 入门课吧! Google Study Jams 活动介绍 Study Jams 是一个学习 Google 在线课程的活动。该活动由学员自主发起课程学习小组,带领小组成员入门 Android 开发,最终将

By Race604