RN热更新之Android篇

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容