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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM