美文网首页
RN热更新之Android篇

RN热更新之Android篇

作者: 哪吒闹海全靠浪 | 来源:发表于2017-05-16 15:00 被阅读0次

    前言

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

    相关文章

      网友评论

          本文标题:RN热更新之Android篇

          本文链接:https://www.haomeiwen.com/subject/fjxtxxtx.html