目錄
- 1)全量熱更新-Android
- 2)拆包增量更新-Android
- 3)圖片增量更新-Android
- 4)全量熱更新-iOS
1)全量熱更新-Android
-打更新包bundle(包括更新的圖片和代碼)
react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false
-
運行此命令會將代碼和圖片打入根目錄下的bundle文件夾碧信,將這些文件壓縮至zip包
=> patches.zip
=>將zip包放入遠程文件服務(wù)器待下載
-根據(jù)業(yè)務(wù)判斷是否需要更新
private void checkVersion() {
if (true) {
// 有最新版本
Toast.makeText(this, "開始下載", Toast.LENGTH_SHORT).show();
initDownloadManager(); //開啟廣播接收器
downLoadBundle(); //開始下載任務(wù)
}
}
-下載zip 至指定的sdcard地址
//注冊廣播接收器
private void initDownloadManager() {
mDownloadReceiver = new DownloadReceiver();
registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
//下載任務(wù)
private void downLoadBundle() {
// 1.檢查是否存在pat壓縮包,存在則刪除
// /storage/emulated/0/Android/data/包名/cache/patches.zip
zipfile = new File(FileConstant.get_JS_PATCH_LOCAL_PATH(this));
if(zipfile != null && zipfile.exists()) {
zipfile.delete();
}
// 2.下載
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
//遠程下載地址http://192.168.1.127/patches.zip
DownloadManager.Request request = new DownloadManager
.Request(Uri.parse(FileConstant.JS_BUNDLE_REMOTE_URL));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE| DownloadManager.Request.NETWORK_WIFI);
//下載目標(biāo)地址 /storage/emulated/0/Android/data/包名/cache/patches.zip
request.setDestinationUri(Uri.parse("file://"+ FileConstant.get_JS_PATCH_LOCAL_PATH(this)));
mDownloadId = downloadManager.enqueue(request);
}
-解析zip并寫入sdcard
private class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//下載完成,收到廣播
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if(completeDownloadId == mDownloadId){
// 1.解壓并寫入sdcard對應(yīng)地址
RefreshUpdateUtils.decompression(getApplicationContext());
zipfile.delete();
}
}
}
//~/RefreshUpdateUtils.java
//解析壓縮包瘫里,并寫入手機存儲位置
public static void decompression(Context context) {
try {
//從下載目標(biāo)地址 /storage/emulated/0/Android/data/包名/cache/patches.zip 獲取壓縮包
ZipInputStream inZip = new ZipInputStream(new FileInputStream(FileConstant.get_JS_PATCH_LOCAL_PATH(context)));
ZipEntry zipEntry;
String szName;
try {
while((zipEntry = inZip.getNextEntry()) != null) {
szName = zipEntry.getName();
//如果是目錄則創(chuàng)建,并寫入/storage/emulated/0/Android/data/包名/cache/patches/目錄下
if(zipEntry.isDirectory()) {
szName = szName.substring(0,szName.length()-1);
File folder = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator + szName);
folder.mkdirs();
}
//如果是文件則創(chuàng)建,并寫入/storage/emulated/0/Android/data/包名/cache/patches/目錄下
else{
File folder = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator);
if (!folder.exists()){
folder.mkdir();
}
File file1 = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator + szName);
boolean s = file1.createNewFile();
FileOutputStream fos = new FileOutputStream(file1);
int len;
byte[] buffer = new byte[1024];
while((len = inZip.read(buffer)) != -1) {
fos.write(buffer, 0 , len);
fos.flush();
}
fos.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
inZip.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
-RN調(diào)用JSBundle的時候判斷,當(dāng)sdcard對應(yīng)位置的bundle不為空時加載sdcard中的bundle蓬衡,否則加載原包內(nèi)Assets位置的bundle
public class MainApplication extends Application implements ReactApplication {
private ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
//Debug模式唇礁,這個模式才能在JS里作調(diào)試
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
//返回帶有官方已有的package的集合
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyReactPackage() //加入自定義的Package類
);
}
@Nullable
@Override
protected String getJSBundleFile() {
//判斷sdcard中是否存在bundle碎捺,存在則加載路鹰,不存在則加載Assets中的bundle
//路徑 /storage/emulated/0/Android/data/包名/cache/patches/index.android.bundle
File file = new File (FileConstant.get_JS_BUNDLE_LOCAL_PATH(getApplicationContext()));
if(file != null && file.exists()) {
return FileConstant.get_JS_BUNDLE_LOCAL_PATH(getApplicationContext());
} else {
return super.getJSBundleFile();
}
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
-注意點:
- 1-退出的時候需要殺死進程,否則不會初始化Application就無法更換bundle的加載路徑了
@Override
protected void onDestroy() {
super.onDestroy();
//殺死進程收厨,否則就算退出App晋柱,App處于空進程并未銷毀,再次打開也不會初始化Application
//從而也不會執(zhí)行g(shù)etJSBundleFile去更換bundle的加載路徑 !!!
android.os.Process.killProcess(android.os.Process.myPid());
//解除廣播接收器
unregisterReceiver(mDownloadReceiver);
}
- 2-權(quán)限诵叁,由于我們采用App擴展存儲方式雁竞,若無需兼容6.0以下,則無需申請權(quán)限拧额。
<!--代碼中使用getExternalCacheDir(), API >=19 是不需要申請的碑诉,若需兼容6.0以下則需寫此權(quán)限 -->
<!--但寫此權(quán)限若不加maxSdkVersion="18",會導(dǎo)致6.0已上機型會在設(shè)置中看到此權(quán)限開關(guān)势腮,從而可能會關(guān)閉此權(quán)限-->>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
- 3-不能在開發(fā)環(huán)境調(diào)試联贩,需要打包調(diào)測
//創(chuàng)建assets目錄 ./android/app/src/main/assets
//創(chuàng)建離線bundle和打包本地資源
react-native bundle --entry-file index.android.js --bundle-output ./android/app/src/main/assets/index.android.bundle --platform android --assets-dest ./android/app/src/main/res/ --dev false
//打簽名包即可
cd android && ./gradlew assembleRelease
//進入目錄安裝apk ./android/app/build/outputs/apk/release
adb install app-release.apk
-其他代碼
~/FileConstant.java
public class FileConstant {
//遠程下載服務(wù)地址
public static final String JS_BUNDLE_REMOTE_URL = "http://192.168.1.127/patches.zip";
//本地bundle文件名
public static final String JS_BUNDLE_LOCAL_FILE = "index.android.bundle";
//sdcard中bundle的加載路徑
public static String get_JS_BUNDLE_LOCAL_PATH(Context context){
return context.getExternalCacheDir().getPath() + File.separator+ "patches/index.android.bundle";
}
//sdcard中下載后文件的存放文件夾路徑
public static String get_JS_PATCH_LOCAL_FOLDER(Context context){
return context.getExternalCacheDir().getPath() + File.separator+ "patches";
}
//sdcard中下載的zip包存放位置
public static String get_JS_PATCH_LOCAL_PATH(Context context){
return context.getExternalCacheDir().getPath() + File.separator+ "patches.zip";
}
//sdcard中下載后的增量包pat存放位置
public static String get_JS_PATCH_LOCAL_FILE(Context context){
return context.getExternalCacheDir().getPath() + File.separator+ "patches/patches.pat";
}
}
2)拆包增量更新-Android
- 雖然通過zip壓縮減小了一部分bundle體積,但是每次需要熱更新去打全量包在不更新圖片的情況下再小也有幾百kb捎拯,其中業(yè)務(wù)部分的代碼也只占一部分,著實浪費且不科學(xué)。
- 所以在每次原生迭代版本發(fā)布時署照,保留其附屬的RN版本bundle祸泪,并在此原生版本周期內(nèi)需要熱更新時,生成新的bundle建芙,使用google-diff-match-patch與原版本bundle比對没隘,生成差異化補丁。
- App判斷有熱更新時禁荸,下載此補丁右蒲,與Assets中的初始版本合并,生成新的index.android.bundle文件寫入sdcard中赶熟。
-打更新包bundle(同-1)
-生成差異化補丁文件
將初始版本old.bundle和熱更版本new.bundle進行比對瑰妄,生成patches.pat
=> 將pat和圖片壓縮成 patches.zip
=> 將zip包放入遠程文件服務(wù)器待下載
public static void main(String[] args) {
String o = getStringFromPat("/Users/tugaofeng/Desktop/old.bundle");
String n = getStringFromPat("/Users/tugaofeng/Desktop/new.bundle");
// 對比
diff_match_patch dmp = new diff_match_patch();
LinkedList<diff_match_patch.Diff> diffs = dmp.diff_main(o, n);
// 生成差異補丁包
LinkedList<diff_match_patch.Patch> patches = dmp.patch_make(diffs);
// 解析補丁包
String patchesStr = dmp.patch_toText(patches);
try {
// 將補丁文件寫入到某個位置
Files.write(Paths.get("/Users/tugaofeng/Desktop/patches.pat"), patchesStr.getBytes());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
-根據(jù)業(yè)務(wù)判斷是否需要更新(同-1)
-下載zip 至指定的sdcard地址(同-1)
-解析zip并寫入sdcard
private class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//下載完成,收到廣播
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if(completeDownloadId == mDownloadId){
// 1.解壓并寫入sdcard對應(yīng)地址
RefreshUpdateUtils.decompression(getApplicationContext());
zipfile.delete();
// 2.將下載好的patches文件與assets目錄下的原index.android.bundle合并映砖,得到新的
// bundle文件,并寫入sdcard中
mergePatAndAsset();
}
}
}
-將Assets內(nèi)的index.android.bundle和下載完成的差異化補丁pat合并间坐,并生成新的index.android.bundle寫入sdcard對應(yīng)位置
//拆包增量更新bundle
private void mergePatAndAsset() {
// 1.獲取本地Assets目錄下的bunlde
String assetsBundle = RefreshUpdateUtils.getJsBundleFromAssets(getApplicationContext());
// 2.獲取.pat文件字符串
// /storage/emulated/0/Android/data/包名/cache/patches/patches.pat
String patcheStr = RefreshUpdateUtils.getStringFromPat(FileConstant.get_JS_PATCH_LOCAL_FILE(this));
if (patcheStr == null || "".equals(patcheStr)){
return;
}
// 3.初始化 dmp
diff_match_patch dmp = new diff_match_patch();
// 4.轉(zhuǎn)換pat
LinkedList<diff_match_patch.Patch> pathes = (LinkedList<diff_match_patch.Patch>) dmp.patch_fromText(patcheStr);
// 5.與assets目錄下的bundle合并,生成新的bundle
Object[] bundleArray = dmp.patch_apply(pathes,assetsBundle);
// 6.保存新的bundle
// 至/storage/emulated/0/Android/data/包名/cache/patches/index.android.bundle
try {
Writer writer = new FileWriter(FileConstant.get_JS_BUNDLE_LOCAL_PATH(this));
String newBundle = (String) bundleArray[0];
writer.write(newBundle);
writer.close();
// 7.刪除.pat文件
// 路徑為/storage/emulated/0/Android/data/包名/cache/patches/patches.pat
File patFile = new File(FileConstant.get_JS_PATCH_LOCAL_FILE(this));
patFile.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
-其他代碼
//~/RefreshUpdateUtils.java
//將.pat or bundle文件轉(zhuǎn)換為String
public static String getStringFromPat(String patPath) {
FileReader reader = null;
String result = "";
try {
reader = new FileReader(patPath);
int ch = reader.read();
StringBuilder sb = new StringBuilder();
while (ch != -1) {
sb.append((char)ch);
ch = reader.read();
}
reader.close();
result = sb.toString();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
//從本地Assets獲取bundle
public static String getJsBundleFromAssets(Context context) {
String result = "";
try {
InputStream is = context.getAssets().open(FileConstant.JS_BUNDLE_LOCAL_FILE);
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
result = new String(buffer,"UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
3)圖片增量更新-Android
圖片增量更新需要修改RN源碼邑退。
-修改RN源碼竹宋。
注意:RN庫版本升級時別忘了修改。地技。
渲染圖片的方法在:node_modules / react-native / Libraries / Image /AssetSourceResolver.js 下:
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem() ?
//存在離線Bundle文件時蜈七,從Bundle文件所在目錄加載圖片
this.drawableFolderInBundle() :
//否則從Asset資源目錄下加載
this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetPathInBundle();
}
}
對源碼做如下修改:
...
import type { PackagerAsset } from 'AssetRegistry';
// 1-新增全局變量
// !!!注意:每次基于某個原生版本的RN熱更版本新增的圖片都要在此處新增加(不是覆蓋哦)
// 比如原生版本1.0.0,RN熱更版本1.0.0-1時新增a.png
//var patchImgNames = '|a.png|';
// 比如原生版本1.0.0莫矗,RN熱更版本1.0.0-2時新增b.png
//var patchImgNames = '|a.png|b.png|';
// 比如原生版本2.0.0(2.0的原生版本asset里已經(jīng)會包含a和b.png)宪潮,暫無RN熱更版本時
//var patchImgNames = '';
var patchImgNames = '|src_res_images_offer_message_red.png|src_res_images_banner_default.png|';
...
// 2-修改此函數(shù)
isLoadedFromFileSystem(): boolean {
// return !!this.bundlePath; //注釋此處,新增如下代碼
var imgFolder = getAssetPathInDrawableFolder(this.asset);
var imgName = imgFolder.substr(imgFolder.indexOf("/") + 1);
var isPatchImg = patchImgNames.indexOf("|"+imgName+"|") > -1;
return !!this.bundlePath && isPatchImg;
}
-打增量代碼包和增量圖片
=> 打更新包
=> 生成差異化補丁文件pat
=> 將pat和本次熱更新增的圖片壓縮成patches.zip
=> 將zip包放入遠程文件服務(wù)器待下載
- 注意:圖片每次熱更的時候可以只將當(dāng)次熱更新增的圖片打入zip包中趣苏,而不需要像修改源碼全局變量patchImgNames一樣需要追溯當(dāng)次原生版本的所有熱更圖片的文件名 !!!
-效果展示
4)全量熱更新-iOS
-修改podfile狡相,新增SSZipArchive【解壓】和AFNetworking【文件下載】
pod 'SSZipArchive'
pod 'AFNetworking', '~> 3.0'
cd /ios && pod install
-打更新包bundle(包括更新的圖片和代碼)
react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/
- 運行此命令會將代碼和圖片打入根目錄下的release_ios文件夾,將這些文件壓縮至zip包
=> patches.zip
=>將zip包放入遠程文件服務(wù)器待下載
-創(chuàng)建bundle存放路徑,使用plist文件去存儲版本號和下載路徑
//創(chuàng)建bundle路徑
-(void)createPath{
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:[self getVersionPlistPath]]) {
return;
}
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *path = [paths lastObject];
NSString *directryPath = [path stringByAppendingPathComponent:@"IOSBundle"];
[fileManager createDirectoryAtPath:directryPath withIntermediateDirectories:YES attributes:nil error:nil];
NSString *filePath = [directryPath stringByAppendingPathComponent:@"Version.plist"];
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
}
-根據(jù)業(yè)務(wù)判斷是否需要更新
//獲取版本信息
-(void)getAppVersion{
//從服務(wù)器上獲取版本信息,與本地plist存儲的版本進行比較
//1.獲取本地plist文件的版本號
NSString* plistPath=[self getVersionPlistPath];
NSMutableDictionary *data = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
NSInteger localV=[data[@"bundleVersion"]integerValue];
//本地plist的版本號
printf("%ld ", (long)localV);
//保留業(yè)務(wù)食磕,根據(jù)當(dāng)前熱更版本號與本地比對尽棕,進行判斷是否下載
if(true){
//下載bundle文件 存儲在 Doucuments/IOSBundle/下
NSString*url=@"http://192.168.1.127/patches.zip";
[[DownLoadTool defaultDownLoadTool] downLoadWithUrl:url];
}
}
-下載zip 至指定沙盒路徑地址
-(void)downLoadWithUrl:(NSString*)url{
//根據(jù)url下載相關(guān)文件
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:url];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
//獲取下載進度
NSLog(@"Progress is %f", downloadProgress.fractionCompleted);
} destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
//有返回值的block,返回文件存儲路徑
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
// file:///Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName
NSURL* targetPathUrl = [documentsDirectoryURL URLByAppendingPathComponent:@"IOSBundle"];
return [targetPathUrl URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if(error){
//下載出現(xiàn)錯誤
NSLog(@"%@",error);
}else{
// [self showPromptWithStr:@"更新完畢彬伦。請重新啟動******滔悉!"];
//下載成功
// file:///Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName/patches.zip
NSLog(@"File downloaded to: %@", filePath);
self.zipPath = [[filePath absoluteString] substringFromIndex:7];
//下載成功后更新本地存儲信息
NSDictionary*infoDic=@{@"bundleVersion":@3,@"downloadUrl":url};
[UpdateDataLoader sharedInstance].versionInfo=infoDic;
[[UpdateDataLoader sharedInstance] writeAppVersionInfoWithDictiony:[UpdateDataLoader sharedInstance].versionInfo];
//解壓并刪除壓縮包
[self unZip];
[self deleteZip];
}
}];
[downloadTask resume];
}
-解壓&刪除壓縮包
//解壓壓縮包
-(BOOL)unZip{
if (self.zipPath == nil) {
return NO;
}
//Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName/patches.zip
NSString *zipPath = self.zipPath;
// /Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/IOSBundle
NSString *destinationPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]stringByAppendingString:@"/IOSBundle"];
BOOL success = [SSZipArchive unzipFileAtPath:zipPath
toDestination:destinationPath];
return success;
}
//刪除壓縮包
-(void)deleteZip{
NSError* error = nil;
[[NSFileManager defaultManager] removeItemAtPath:self.zipPath error:&error];
}
-RN調(diào)用JSBundle的時候判斷,當(dāng)沙盒對應(yīng)位置的bundle不為空時加載其bundle单绑,否則加載原包內(nèi)的bundle
NSURL *jsCodeLocation;
NSString* iOSBundlePath = [[UpdateDataLoader sharedInstance] iOSFileBundlePath];
NSString* filePath = [iOSBundlePath stringByAppendingPathComponent:@"/main.jsbundle"];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
jsCodeLocation = [NSURL URLWithString:[iOSBundlePath stringByAppendingString:@"/main.jsbundle"]];
}else{
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
}
參考資料
React Native 實現(xiàn)熱部署回官、差異化增量熱更新
React-Native開發(fā)iOS篇-熱更新的代碼實現(xiàn)