zoukankan      html  css  js  c++  java
  • ReactNative学习笔记(四)热更新和增量更新

    概括

    关于RN的热更新,网上有很多现成方案,但是一般都依赖第三方服务,我所希望的是能够自己管控所有一切,所以只能自己折腾。

    热更新的思路

    热更新一般都是更新JS和图片,也就是在不重新安装apk的情况下更新JS和图片,这个需求是很普遍的。通过前面的了解我们知道RN的JS都被打包成了一个bundle文件,默认是在assets文件夹下面,但是这个文件夹是只读不可写的,那怎么办呢?好在RN有一个getJSBundleFile方法可以自定义bundle文件的路径,把它自定义到一个我们有写入权限的地方然后下载覆盖就可以了(比如/data/data/下面)。

    又由于图片也需要更新,所以可以将更新资源(图片+JSBundle文件)打包成一个zip,在每次启动apk之后检测是否有更新包,如果有,后台偷偷下载下来,那么什么时候解压呢?个人推荐在下次启动apk的时候解压,那样可以保证图片和JS同时更新(因为我没有尝试过在程序运行时覆盖bundle文件会有什么问题)。

    思路的具体实现

    生成bundle文件

    前面提到,RN会将所有JS压缩混淆成一个bundle文件,所以要做热更新,我们首先需要掌握如何自己手动生成bundle文件。

    执行如下命令即可(记得先在项目根目录新建一个bundle文件夹,否则报错):

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

    W422xH247

    注意,bundle文件在哪,那么图片也必须放在哪,如果bundle默认放在assets下面,会自动读取apk内部res文件夹下的资源文件,但是如果你将bundle文件放在了其它自定义目录下,那么图片也要跟着复制过去,否则图片全部空白。

    自定义bundle文件路径

    特别注意,getJSBundleFile方法位置在0.29版本以后发现了变化。

    0.28及以前版本:

    public class MainActivity extends ReactActivity
    {
    	@Override
    	protected @Nullable String getJSBundleFile()
    	{
    		String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
    		File file = new File(jsBundleFile);
    		return file != null && file.exists() ? jsBundleFile : null;
    	}
    }
    

    0.29及以后版本:

    public class MainApplication extends Application implements ReactApplication
    {
    	private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this)
    	{
    		@Override
    		protected @Nullable String getJSBundleFile()
    		{
    			String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
    			File file = new File(jsBundleFile);
    			return file != null && file.exists() ? jsBundleFile : null;
    		}
    	}
    }
    

    假如我的包名是com.helloworld,定义了如上代码之后,启动APK首先会尝试加载/data/data/com.helloworld/files/index.android.bundle文件,找不到再去加载assets里面的。

    封装下载方法

    前面忘记介绍如何开发一个原生模块让JS调用了,这里正好借封装下载方法的机会介绍一下。

    这里只是简单的实现一个下载的方法,实际项目中建议用更成熟方案。

    新建一个HotUpdateModule.java文件:

    public class HotUpdateModule extends ReactContextBaseJavaModule
    {
    	public HotUpdateModule(ReactApplicationContext reactContext) {
    		super(reactContext);
    	}
    	@Override
    	public String getName() {
    		return "hotupdate"; // 返回的名字就是最终模块的名字,前端调用时:NativeModules.hotupdate.xxx
    	}
    
    	@ReactMethod
    	public void download(final String url, String newFileName, final Promise promise)
    	{
    		final String savePath = getReactApplicationContext().getFilesDir() + "/" + newFileName;
    		new Thread(new Runnable()
    		{
    			@Override
    			public void run()
    			{
    				try
    				{
    					String result = SimpleDownloadUtil.download(url, savePath);
    					WritableMap map = Arguments.createMap();
    					map.putString("result", result);
    					promise.resolve(map);
    				}
    				catch (Exception e)
    				{
    					promise.reject("unknown error", e);
    				}
    			}
    		}).start();
    	}
    }
    

    其中,SimpleDownloadUtil.java如下:

    public class SimpleDownloadUtil
    {
    	/**
    	 * 简单的下载工具类
    	 * @param downloadUrl
    	 * @param savePath
    	 * @return 返回保存路径,如果下载失败,返回空
    	 */
    	public static String download(String downloadUrl, String savePath) throws Exception
    	{
    		Log.i("info", "开始下载:"+downloadUrl);
    		HttpURLConnection con = (HttpURLConnection) new URL(downloadUrl).openConnection();
    		con.setRequestMethod("GET");
    		con.setUseCaches(false);
    		con.setInstanceFollowRedirects(true);
    		con.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31");
    		con.setRequestProperty("accept", "*/*");// 这个可以不设置
    		con.connect();// 连接
    		InputStream is = con.getInputStream();
    		File file = new File(savePath);
    		FileOutputStream fos = new FileOutputStream(file);
    		byte[] buf = new byte[1024];
    		int len = -1;
    		while ((len = is.read(buf)) != -1) fos.write(buf, 0, len);
    		is.close();
    		fos.close();
    		con.disconnect();// 断开连接
    		Log.i("info", "下载完毕:" + savePath);
    		return savePath;
    	}
    }
    

    然后新建一个TestReactPackage.java

    public class TestReactPackage implements ReactPackage {
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> modules = new ArrayList<>();
            // modules.add(new TestModule(reactContext));
            modules.add(new HotUpdateModule(reactContext)); // 多个模块依次添加
            return modules;
        }
        @Override
        public List<Class<? extends JavaScriptModule>> createJSModules() {
            return Collections.emptyList();
        }
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
    

    再修改MainApplication中的如下方法,将上面的TestReactPackage添加上去:

    @Override
    protected List<ReactPackage> getPackages()
    {
    	return Arrays.<ReactPackage>asList(
    			new MainReactPackage(),
    			new TestReactPackage() // 自定义的
    	);
    }
    

    至此,一个使用原生实现的下载功能就完成了,JS中只需要调用NativeModules.hotupdate.download()即可(记得要引入NativeModules模块)。

    模拟服务器

    假设有一个检测是否需要更新的接口,返回如下字段:

    {
    	"needUpdate": true, // 表示是否需要更新
    	"updateUrl": "http://192.168.191.1/update/bundle.zip" // 更新地址
    }
    

    为了简单起见,直接用JSON文件模拟,bundle.zip就是我们上面用命令生成的bundle文件夹压缩后的文件(如果希望用批处理方式生成zip的话可以参考我之前写的Windows下使用命令行解压和压缩zip)。

    检测更新并下载

    import React, { Component } from 'react';
    import { NativeModules } from 'react-native';
    
    class TestComponent extends Component
    {
    	// 省略其它代码
    	componentDidMount()
    	{
    		fetch('http://192.168.191.1/update/check_update.json')
    		.then((response) => response.json())
    		.then((json) => 
    		{
    			if(json.needUpdate && json.updateUrl)
    			{
    				Epg.tip('检测到省流量更新文件,开始自动下载!');
    				NativeModules.hotupdate.download(json.updateUrl, 'bundle.zip')
    				.then((e) => alert('下载成功:'+e.result+',下次重启时生效!'))
    				.catch((error) => alert('下载失败:'+error));
    			}
    		})
    		.catch((error) => alert('检测更新失败:'+error));
    	}
    }
    

    解压zip

    由于JS本身可能需要更新,所以解压zip用JS来完成的话可能不太适合,我把它直接写在Activity里面:

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        String root = this.getFilesDir().getAbsolutePath();
        File zip = new File(root, "bundle.zip");
        if(zip.exists()) // 如果检测到zip更新包,解压之
        {
            ZipUtil.extract(root+"/bundle.zip", root); // 这个ZipUtil是自己随便封装的
            zip.delete(); // 解压之后删除zip文件
        }
        super.onCreate(savedInstanceState);
    }
    

    测试

    一整个过程走下来感觉是有点折腾人的,虽然都比较简单,测试的时候最麻烦,因为必须生成release包之后热更新才能看到效果。

    测试过程可以这样:

    先打一个release包并安装,把needUpdate暂时设置为false避免更新,然后故意修改一些JS代码以及增加图片,然后用命令生成bundle,然后把bundle文件和图片一起打包放到服务器上,然后needUpdate改回true,重启apk,可以看到自动下载zip的提示,然后再重启,检查一下修改之后的代码是否生效了,如果生效表示热更新成功了。

    增量更新

    图片的增量更新

    前面提到了,bundle文件在哪,图片也要在哪,否则图片会找不到,但是更新包里面把所有的图片都包括进去太大了,有一种思路是:每次启动APK立即检测私有目录下是否有bundle文件,没有就从assets下复制一个,这样可以保证无论何时bundle文件都是从sd卡读取,现在要做的就是把图片也复制过去,但是图片是放在res文件夹作为资源文件存在的,怎么把res下的图片文件完整复制到sd卡,这个我还真不会,暂时也没有找到合适的方法,如果哪位知道方法还烦请告知(主要是针对非root用户,已经root的用户就好办了)。

    所以目前的一个比较笨的办法是,打包时人工将所有图片丢到assets下,因为assets下的文件是可以随意复制的,缺点就是apk体积变大了,一个apk里面放了2份图片。

    上述问题解决了,图片的增量更新就好办了,每次只需要把需要替换或增加的图片放到更新的zip包里面去就可以了。

    bundle文件的增量更新

    这是个文本文件,一般有几百kb,不作增量做全量更新问题也不大,但是还是有必要研究一下的。网上一般思路是用bsdiff对比文件,或者分离bundle,这个我没去做具体尝试,所以就不详细赘述了,有兴趣的可以看文末的参考链接。

    参考

    http://www.jianshu.com/p/2cb3eb9604ca

  • 相关阅读:
    Direct3D光与材质的颜色值
    Direct中灯光的注意事项
    DirectInput:poll轮询理解
    GetAsyncKeyState函数返回值
    关于PeekMessage中hwnd参数
    VS链接MySql需注意的一些问题(C/C++)
    Windows配置:环境变量是个什么玩意儿?
    项目中ofstream 打开当前文件夹下内容失败原因
    hdoj--2073--无限的路(数学规律)
    hdoj--1205--吃糖果(规律)
  • 原文地址:https://www.cnblogs.com/liuxianan/p/react-native-4.html
Copyright © 2011-2022 走看看