前言
這篇來研究一下RN的熱更新,之前看資料見到過兩個現成的方案:
不過看了文檔就覺得沒勁,不如自己來實現,況且之前已經有點門路了。
原理
關于熱更新的原理,另開一篇,點這里。
實現
既然我們知道了原理,那么列一個大致的實現思路:
- 我們打好包jsbundle文件放到遠程服務器上。
- 請求服務器接口,當接口中返回的版本號跟我們rn中存儲的版本號不一致的時候,那么這個時候就需要更新版本了。
- 下載服務器上的jshundle,替換掉當前版本的jsbundle文件。
- 下次打開生效或者執行某個方法立即更新。
打包
回顧一下打包命令
$ react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/com/your-company-name/app-package-name/src/main/assets/index.android.bundle --assets-dest android/com/your-company-name/app-package-name/src/main/res/
發現打包分成了bundle和資源兩部分,但我們的demo里有沒任何圖片,所以我在index.android.js加了張圖片,以便驗證資源是否能熱加載成功。
<Image
source={require('./img/music_play.png')}
style={{width:92,height:92}}
/>
然后在根目錄建了一個finalbundle的文件夾,存放最終打出的包,執行
react-native bundle --platform android --dev false --entry-file index.android.js
--bundle-output finalbundle/index.android.bundle --assets-dest finalbundle/
在finalbundle文件夾中就生成了我們打好的包,壓縮好上傳到服務器即可。
更新和下載
要更新我們首先要把當前的版本號與服務端最新的版本號做比對,不一樣才執行下載動作。比對這步可以是
- 前端發Ajax請求,在回調里拿到版本號,比出不同,再調用android代碼執行下載、替換。
- 也可以全部邏輯都在android原生的代碼做掉,js端不用給任何反應。
兩者的區別其實就是需不需要讓用戶有感知,但第一種好像更靈活一點,另外的區別就是版本號存放的位置和比對狀態的區別。
第一種比較清晰,每次在入口的JS把客戶端的版本號和服務端比就行了,不一致就更新,下次比對就一致了,當然就需要你在打包時的版本號和插入服務端的一致;
而第二種麻煩一點,因為它只能拿到你隨包打的版本號,更新后沒前端發給后端,它是拿不到新的版本號,所以需要后端的一個存儲機制在更新后把更新的版本后記下來,所以比較的邏輯應該就是優先拿更新過的版本號和服務端的比,沒有更新過的才用原始隨包的版本號和服務的比。
我先來試試第二種:
首先需要知道怎么拿到隨包打的版本號,需要在打開app/build.gradle,然后添加buildConfigField定義,如下:
然后重新編譯,在BuildConfig看到就多了一條BUNDLE_VERSION
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.example.zhouwenkang.rnandnative";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
// Fields from default config.
public static final String BUNDLE_VERSION = "1.0.0";
}
所以我們就能用BuildConfig.BUNDLE_VERSION來獲取隨包打的版本號。
第二,開始判斷更新:
大致的思路是
- 先去SD(我們打算存放的位置)找bundle
- 沒有才去找默認的assets
- 然后才是異步判斷版本,下載、更新替換
我們開始改造一下MyRNActivity
package com.example.zhouwenkang.rnandnative;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.KeyEvent;
import com.facebook.react.JSCConfig;
import com.facebook.react.ReactApplication;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.react.ReactInstanceManagerBuilder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class MyRNActivity extends Activity implements DefaultHardwareBackBtnHandler {
private long mDownloadId;
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
private DownloadManager dm;
public static void startActivity(Context context){
Intent intent = new Intent(context, MyRNActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mReactRootView = new ReactRootView(this);
ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
.setApplication(getApplication())
//.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
.addPackage(new RNJavaReactPackage())
.setUseDeveloperSupport(true)
.setInitialLifecycleState(LifecycleState.RESUMED);
File bundleFile = new File(getExternalCacheDir()+"/finalbundle","index.android.bundle");
if(bundleFile.exists()){
builder.setJSBundleFile(bundleFile.getAbsolutePath());
} else {
builder.setBundleAssetName("index.android.bundle");
}
mReactInstanceManager = builder.build();
mReactRootView.startReactApplication(mReactInstanceManager, "rnandnative", null);
setContentView(mReactRootView);
updateJsBundle();
}
private void updateJsBundle(){
if(BuildConfig.BUNDLE_VERSION == "1.0.0"){//TODO:這里需要發起異步獲取服務端的版本號,然后和打包版本號比對
Context context=MyRNActivity.this;//首先,在Activity里獲取context
File file=context.getFilesDir();
String path=file.getAbsolutePath();
System.out.println(path);
System.out.println(Environment.getExternalStorageDirectory().toString());
System.out.println(getExternalCacheDir());
File reactDir = new File(getExternalCacheDir(),"finalbundle");
System.out.println(reactDir.getAbsolutePath());
if(!reactDir.exists()){
reactDir.mkdirs();
}
System.out.println("file://"+new File(getExternalCacheDir(),"finalbundle/finalbundle.zip").getAbsolutePath());
DownloadManager.Request request = new DownloadManager.Request(Uri.parse("https://raw.githubusercontent.com/wenkangzhou/YWNative/master/HotUpdateRes/finalbundle.zip"));
//request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setDestinationUri(Uri.parse("file://"+new File(getExternalCacheDir(),"finalbundle/finalbundle.zip").getAbsolutePath()));
//在通知欄中顯示,默認就是顯示的
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setVisibleInDownloadsUi(true);
dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
mDownloadId = dm.enqueue(request);
//注冊廣播接收者,監聽下載狀態
registerReceiver(receiver,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
//廣播接受者,接收下載狀態
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
checkDownloadStatus();//檢查下載狀態
}
};
//檢查下載狀態
private void checkDownloadStatus() {
System.out.println("檢查下載狀態");
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(mDownloadId);//篩選下載任務,傳入任務ID,可變參數
Cursor c = dm.query(query);
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
Log.i("heeeeeeee",">>>下載暫停");
System.out.println("下載暫停");
case DownloadManager.STATUS_PENDING:
Log.i("heeeeeeee",">>>下載延遲");
System.out.println("下載延遲");
case DownloadManager.STATUS_RUNNING:
Log.i("heeeeeeee",">>>正在下載");
System.out.println("正在下載");
break;
case DownloadManager.STATUS_SUCCESSFUL:
Log.i("heeeeeeee",">>>下載完成");
//下載完成
replaceBundle();
break;
case DownloadManager.STATUS_FAILED:
Log.i("heeeeeeee",">>>下載失敗");
System.out.println("下載失敗");
break;
}
}
}
protected void replaceBundle() {
System.out.println("下載成功");
File reactDir = new File(getExternalCacheDir(),"finalbundle");
System.out.println(reactDir.getAbsolutePath());
if(!reactDir.exists()){
System.out.println("創建");
reactDir.mkdirs();
}
final File saveFile = new File(reactDir,"finalbundle.zip");
boolean result = unzip(saveFile);
if(result){//解壓成功后保存當前最新bundle的版本
if(true) {//立即加載bundle
System.out.println("加載bundle");
// ((ReactApplication) getReactApplicationContext()).getReactNativeHost().clear();
// getCurrentActivity().recreate();
try {
Class<?> RIManagerClazz = mReactInstanceManager.getClass();
Field f = RIManagerClazz.getDeclaredField("mJSCConfig");
f.setAccessible(true);
JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager);
Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground",
com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class,
com.facebook.react.cxxbridge.JSBundleLoader.class);
method.setAccessible(true);
method.invoke(mReactInstanceManager,
new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()),
com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader(new File(getExternalCacheDir()+"/finalbundle","index.android.bundle").getAbsolutePath()));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NoSuchFieldException e){
e.printStackTrace();
}
}
}else{//解壓失敗應該刪除掉有問題的文件,防止RN加載錯誤的bundle文件
System.out.println("解壓失敗");
File reactbundleDir = new File(getExternalCacheDir(),"finalbundle");
deleteDir(reactbundleDir);
}
}
private static boolean unzip(File zipFile){
if(zipFile != null && zipFile.exists()){
ZipInputStream inZip = null;
try {
inZip = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry zipEntry;
String entryName;
File dir = zipFile.getParentFile();
while ((zipEntry = inZip.getNextEntry()) != null) {
entryName = zipEntry.getName();
if (zipEntry.isDirectory()) {
File folder = new File(dir,entryName);
folder.mkdirs();
} else {
File file = new File(dir,entryName);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
fos.write(buffer, 0, len);
fos.flush();
}
fos.close();
}
}
//("+++++解壓完成+++++");
return true;
} catch (IOException e) {
e.printStackTrace();
//("+++++解壓失敗+++++");
return false;
}finally {
try {
if(inZip != null){
inZip.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}else {
return false;
}
}
private static void deleteDir(File dir){
if (dir==null||!dir.exists()) {
return;
} else {
if (dir.isFile()) {
dir.delete();
return;
}
}
if (dir.isDirectory()) {
File[] childFile = dir.listFiles();
if (childFile == null || childFile.length == 0) {
dir.delete();
return;
}
for (File f : childFile) {
deleteDir(f);
}
dir.delete();
}
}
@Override
protected void onResume() {
super.onResume();
if(mReactInstanceManager != null){
mReactInstanceManager.onHostResume(this, this);
}
}
@Override
protected void onPause() {
super.onPause();
if(mReactInstanceManager != null){
mReactInstanceManager.onHostPause(this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
if(mReactInstanceManager != null){
mReactInstanceManager.onHostDestroy();
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
if(mReactInstanceManager != null){
mReactInstanceManager.onBackPressed();
}else{
super.onBackPressed();
}
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
//我們需要改動一下開發者菜單。
//默認情況下,任何開發者菜單都可以通過搖晃或者設備類觸發,不過這對模擬器不是很有用。
//所以我們讓它在按下Menu鍵的時候可以顯示
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
mReactInstanceManager.showDevOptionsDialog();
return true;
}
return super.onKeyUp(keyCode, event);
}
}
這里花了比較多時間,不過終于搞定了。
然后再試試第一種
通過JS端觸發更新,比第一種其實就多了兩點
- 需要一個update的modules,打通前端與原生
- 在更新后需要存儲更新狀態
JS:
NativeModules.updateBundle.check("5.0.0");
RNUpdateBundleModule.java:
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import com.facebook.react.JSCConfig;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.cxxbridge.JSBundleLoader;
import com.facebook.react.cxxbridge.JSCJavaScriptExecutor;
import com.facebook.react.cxxbridge.JavaScriptExecutor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.app.Activity;
import android.widget.Toast;
public class RNUpdateBundleModule extends ReactContextBaseJavaModule {
private SharedPreferences mSP;
private static final String BUNDLE_VERSION = "CurrentBundleVersion";
private DownloadManager dm;
private long mDownloadId;
private ReactInstanceManager mReactInstanceManager;
Activity myActivity;
public RNUpdateBundleModule(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
mSP = reactApplicationContext.getSharedPreferences("react_bundle", Context.MODE_PRIVATE);
}
@Override
public String getName() {
return "updateBundle";
}
/*
一個可選的方法getContants返回了需要導出給JavaScript使用的常量。
它并不一定需要實現,但在定義一些可以被JavaScript同步訪問到的預定義的值時非常有用。
*/
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
//跟隨apk一起打包的bundle基礎版本號,也就是assets下的bundle版本號
String bundleVersion = BuildConfig.BUNDLE_VERSION;
//bundle更新后的當前版本號
String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,"");
System.out.println("+++++check version+++++-" + cacheBundleVersion);
if(!TextUtils.isEmpty(cacheBundleVersion)){
System.out.println("-+++++check version+++++-" + cacheBundleVersion);
bundleVersion = cacheBundleVersion;
}
System.out.println("-+++++check version+++++-" + bundleVersion);
constants.put(BUNDLE_VERSION,bundleVersion);
return constants;
}
@ReactMethod
public void check(String currVersion) {
System.out.println("+++++check version+++++" + currVersion);
System.out.println("+++++check version+++++" + BuildConfig.BUNDLE_VERSION);
System.out.println("+++++check version+++++" + mSP.getString(BUNDLE_VERSION,""));
String jsBundleVersion = BuildConfig.BUNDLE_VERSION;
String cacheBundleVersion = mSP.getString(BUNDLE_VERSION,"");
if(!TextUtils.isEmpty(cacheBundleVersion)){
jsBundleVersion = cacheBundleVersion;
}
//測試時先隱藏
// if(jsBundleVersion.equals("1.0.0")){//和服務下發的比對
// System.out.println("已經是最新版本");
// return;
// }
updateJsBundle();
}
private void updateJsBundle(){
Context context= getReactApplicationContext();
File file=context.getFilesDir();
String path=file.getAbsolutePath();
System.out.println(path);
System.out.println(Environment.getExternalStorageDirectory().toString());
System.out.println(getReactApplicationContext().getExternalCacheDir());
File reactDir = new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle");
System.out.println(reactDir.getAbsolutePath());
if(!reactDir.exists()){
reactDir.mkdirs();
}
File reactZipDir = new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle/finalbundle.zip");
if(reactZipDir.exists()){
deleteDir(reactZipDir);
}
System.out.println("file://"+new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle/finalbundle.zip").getAbsolutePath());
DownloadManager.Request request = new DownloadManager.Request(Uri.parse("https://raw.githubusercontent.com/wenkangzhou/YWNative/master/HotUpdateRes/finalbundle.zip"));
//request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setDestinationUri(Uri.parse("file://"+new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle/finalbundle.zip").getAbsolutePath()));
//在通知欄中顯示,默認就是顯示的
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setVisibleInDownloadsUi(true);
myActivity = getCurrentActivity();
dm = (DownloadManager) myActivity.getSystemService(Context.DOWNLOAD_SERVICE);
mDownloadId = dm.enqueue(request);
//注冊廣播接收者,監聽下載狀態
myActivity.registerReceiver(receiver,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
//廣播接受者,接收下載狀態
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
checkDownloadStatus();//檢查下載狀態
}
};
//檢查下載狀態
private void checkDownloadStatus() {
System.out.println("檢查下載狀態");
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(mDownloadId);//篩選下載任務,傳入任務ID,可變參數
Cursor c = dm.query(query);
if (c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_PAUSED:
Log.i("heeeeeeee",">>>下載暫停");
System.out.println("下載暫停");
case DownloadManager.STATUS_PENDING:
Log.i("heeeeeeee",">>>下載延遲");
System.out.println("下載延遲");
case DownloadManager.STATUS_RUNNING:
Log.i("heeeeeeee",">>>正在下載");
System.out.println("正在下載");
break;
case DownloadManager.STATUS_SUCCESSFUL:
Log.i("heeeeeeee",">>>下載完成");
//下載完成
replaceBundle();
break;
case DownloadManager.STATUS_FAILED:
Log.i("heeeeeeee",">>>下載失敗");
System.out.println("下載失敗");
break;
}
}
}
protected void replaceBundle() {
System.out.println("下載成功");
File reactDir = new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle");
System.out.println(reactDir.getAbsolutePath());
if(!reactDir.exists()){
System.out.println("創建");
reactDir.mkdirs();
}
final File saveFile = new File(reactDir,"finalbundle.zip");
boolean result = unzip(saveFile);
if(result){//解壓成功后保存當前最新bundle的版本
if(true) {//立即加載bundle
System.out.println("加載bundle");
mSP.edit().putString(BUNDLE_VERSION,"1.0.2").apply();
Activity currActivity = getCurrentActivity();
// if(currActivity != null){
// ((ReactApplication) currActivity.getApplication()).getReactNativeHost().clear();
// currActivity.unregisterReceiver(receiver);
// currActivity.recreate();
// }
// try {
//
// Class<?> RIManagerClazz = mReactInstanceManager.getClass();
//
// Field f = RIManagerClazz.getDeclaredField("mJSCConfig");
// f.setAccessible(true);
// JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager);
//
// Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground",
// com.facebook.react.cxxbridge.JavaScriptExecutor.Factory.class,
// com.facebook.react.cxxbridge.JSBundleLoader.class);
// method.setAccessible(true);
// method.invoke(mReactInstanceManager,
// new com.facebook.react.cxxbridge.JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()),
// com.facebook.react.cxxbridge.JSBundleLoader.createFileLoader(new File(getReactApplicationContext().getExternalCacheDir()+"/finalbundle","index.android.bundle").getAbsolutePath()));
// } catch (NoSuchMethodException e) {
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// } catch (InvocationTargetException e) {
// e.printStackTrace();
// } catch (IllegalArgumentException e) {
// e.printStackTrace();
// } catch (NoSuchFieldException e){
// e.printStackTrace();
// }
// Toast.makeText(getCurrentActivity(), "Downloading complete", Toast.LENGTH_SHORT).show()
try {
ReactApplication application = (ReactApplication) getCurrentActivity().getApplication();
mReactInstanceManager = application.getReactNativeHost().getReactInstanceManager();
//builder.setJSBundleFile(bundleFile.getAbsolutePath());
Class<?> RIManagerClazz = application.getReactNativeHost().getReactInstanceManager().getClass();
Field f = RIManagerClazz.getDeclaredField("mJSCConfig");
f.setAccessible(true);
JSCConfig jscConfig = (JSCConfig)f.get(mReactInstanceManager);
Method method = RIManagerClazz.getDeclaredMethod("recreateReactContextInBackground",
JavaScriptExecutor.Factory.class, JSBundleLoader.class);
method.setAccessible(true);
method.invoke(application.getReactNativeHost().getReactInstanceManager(),
new JSCJavaScriptExecutor.Factory(jscConfig.getConfigMap()),
JSBundleLoader.createFileLoader(new File(getReactApplicationContext().getExternalCacheDir()+"/finalbundle","index.android.bundle").getAbsolutePath()));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NoSuchFieldException e){
e.printStackTrace();
}
}
}else{//解壓失敗應該刪除掉有問題的文件,防止RN加載錯誤的bundle文件
System.out.println("解壓失敗");
File reactbundleDir = new File(getReactApplicationContext().getExternalCacheDir(),"finalbundle");
deleteDir(reactbundleDir);
}
}
private static boolean unzip(File zipFile){
if(zipFile != null && zipFile.exists()){
ZipInputStream inZip = null;
try {
inZip = new ZipInputStream(new FileInputStream(zipFile));
ZipEntry zipEntry;
String entryName;
File dir = zipFile.getParentFile();
while ((zipEntry = inZip.getNextEntry()) != null) {
entryName = zipEntry.getName();
if (zipEntry.isDirectory()) {
File folder = new File(dir,entryName);
folder.mkdirs();
} else {
File file = new File(dir,entryName);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1) {
fos.write(buffer, 0, len);
fos.flush();
}
fos.close();
}
}
//("+++++解壓完成+++++");
return true;
} catch (IOException e) {
e.printStackTrace();
//("+++++解壓失敗+++++");
return false;
}finally {
try {
if(inZip != null){
inZip.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}else {
return false;
}
}
private static void deleteDir(File dir){
if (dir==null||!dir.exists()) {
return;
} else {
if (dir.isFile()) {
dir.delete();
return;
}
}
if (dir.isDirectory()) {
File[] childFile = dir.listFiles();
if (childFile == null || childFile.length == 0) {
dir.delete();
return;
}
for (File f : childFile) {
deleteDir(f);
}
dir.delete();
}
}
}
TODO:這里遇到一個問題,立即刷新無效,下載和第二次開啟app都正常。
遇到問題
1.關于圖片加載,如果是asserts文件夾,圖片需要在res,如果是外部sd,需要和bundle同級,也就是最好把圖片和bundle打在一起,如果單獨更新,需要去asserts目錄復制到你的目錄下,具體可以看看圖片更新的流程。
2.Android 6.0(sdk>=23)的讀寫權限,不僅在AndroidManifest.xml配置,還需要在用的時候發出請求,但cache目錄是不需要的,建議放在cache目錄下。
3.request.setDestinationUri只能是外部存儲,不能是data/data下,還有模擬器網絡不是wifi,所以設置只是wifi也不會觸發下載,這里坑還是挺多的,建議去看看相關文檔DownloadManager
4.立即刷新不生效:這個問題只因為在開啟本地8081時,優先級比讀目錄的高,關閉服務,讀離線文件就OK了。
5.一些機子上32/64位ibgnustl_shared.so的問題死活就是解決不了。
后續完善
1.首次加載,會出現比較長得白屏
可否預先去判斷是否拉增量、預先加載bundle。
2.差量更新
每次只更新變更的,可能需要一些第三方的diff庫,在本地做好diff,上傳、下載是再想辦法合并。