前言
这篇来研究一下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,上传、下载是再想办法合并。