React Native学习React Native

ReactNative热更新&拆包

2018-01-24  本文已影响103人  玄策

目录


流程图

1)全量热更新-Android

全量热更新

-打更新包bundle(包括更新的图片和代码)

react-native bundle --entry-file index.android.js --bundle-output ./bundle/index.android.bundle --platform android --assets-dest ./bundle --dev false

-根据业务判断是否需要更新

private void checkVersion() {
  if (true) {
     // 有最新版本
     Toast.makeText(this, "开始下载", Toast.LENGTH_SHORT).show();
     initDownloadManager();  //开启广播接收器
     downLoadBundle();   //开始下载任务
  }
}

-下载zip 至指定的sdcard地址

    //注册广播接收器
    private void initDownloadManager() {
        mDownloadReceiver = new DownloadReceiver();
        registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }

    //下载任务
    private void downLoadBundle() {
        // 1.检查是否存在pat压缩包,存在则删除
        // /storage/emulated/0/Android/data/包名/cache/patches.zip
        zipfile = new File(FileConstant.get_JS_PATCH_LOCAL_PATH(this));
        if(zipfile != null && zipfile.exists()) {
            zipfile.delete();
        }
        // 2.下载
        DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
        //远程下载地址http://192.168.1.127/patches.zip
        DownloadManager.Request request = new DownloadManager
                .Request(Uri.parse(FileConstant.JS_BUNDLE_REMOTE_URL));
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE| DownloadManager.Request.NETWORK_WIFI);
        //下载目标地址 /storage/emulated/0/Android/data/包名/cache/patches.zip
        request.setDestinationUri(Uri.parse("file://"+ FileConstant.get_JS_PATCH_LOCAL_PATH(this)));
        mDownloadId = downloadManager.enqueue(request);
    }

-解析zip并写入sdcard

    private class DownloadReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //下载完成,收到广播
            long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            if(completeDownloadId == mDownloadId){
                // 1.解压并写入sdcard对应地址
                RefreshUpdateUtils.decompression(getApplicationContext());
                zipfile.delete();
            }
        }
    }
//~/RefreshUpdateUtils.java
    //解析压缩包,并写入手机存储位置
    public static void decompression(Context context) {
        try {
            //从下载目标地址 /storage/emulated/0/Android/data/包名/cache/patches.zip 获取压缩包
            ZipInputStream inZip = new ZipInputStream(new FileInputStream(FileConstant.get_JS_PATCH_LOCAL_PATH(context)));
            ZipEntry zipEntry;
            String szName;
            try {
                while((zipEntry = inZip.getNextEntry()) != null) {

                    szName = zipEntry.getName();
                    //如果是目录则创建,并写入/storage/emulated/0/Android/data/包名/cache/patches/目录下
                    if(zipEntry.isDirectory()) {

                        szName = szName.substring(0,szName.length()-1);
                        File folder = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator + szName);
                        folder.mkdirs();

                    }
                    //如果是文件则创建,并写入/storage/emulated/0/Android/data/包名/cache/patches/目录下
                    else{
                        File folder = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator);
                        if (!folder.exists()){
                            folder.mkdir();
                        }
                        File file1 = new File(FileConstant.get_JS_PATCH_LOCAL_FOLDER(context) + File.separator + szName);

                        boolean s = file1.createNewFile();
                        FileOutputStream fos = new FileOutputStream(file1);
                        int len;
                        byte[] buffer = new byte[1024];

                        while((len = inZip.read(buffer)) != -1) {
                            fos.write(buffer, 0 , len);
                            fos.flush();
                        }

                        fos.close();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            inZip.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

-RN调用JSBundle的时候判断,当sdcard对应位置的bundle不为空时加载sdcard中的bundle,否则加载原包内Assets位置的bundle


public class MainApplication extends Application implements ReactApplication {
    private ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            //Debug模式,这个模式才能在JS里作调试
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            //返回带有官方已有的package的集合
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new MyReactPackage()  //加入自定义的Package类
            );
        }

        @Nullable
        @Override
        protected String getJSBundleFile() {
            //判断sdcard中是否存在bundle,存在则加载,不存在则加载Assets中的bundle
            //路径 /storage/emulated/0/Android/data/包名/cache/patches/index.android.bundle
            File file = new File (FileConstant.get_JS_BUNDLE_LOCAL_PATH(getApplicationContext()));
            if(file != null && file.exists()) {
                return FileConstant.get_JS_BUNDLE_LOCAL_PATH(getApplicationContext());
            } else {
                return super.getJSBundleFile();
            }
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, /* native exopackage */ false);
    }
}

-注意点:

@Override
  protected void onDestroy() {
    super.onDestroy();
    //杀死进程,否则就算退出App,App处于空进程并未销毁,再次打开也不会初始化Application
    //从而也不会执行getJSBundleFile去更换bundle的加载路径 !!!
    android.os.Process.killProcess(android.os.Process.myPid());
    //解除广播接收器
    unregisterReceiver(mDownloadReceiver);
}
<!--代码中使用getExternalCacheDir(), API >=19 是不需要申请的,若需兼容6.0以下则需写此权限 -->
<!--但写此权限若不加maxSdkVersion="18",会导致6.0已上机型会在设置中看到此权限开关,从而可能会关闭此权限-->>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="18"/>
//创建assets目录 ./android/app/src/main/assets
//创建离线bundle和打包本地资源
react-native bundle --entry-file index.android.js --bundle-output ./android/app/src/main/assets/index.android.bundle --platform android --assets-dest ./android/app/src/main/res/ --dev false
//打签名包即可
cd android && ./gradlew assembleRelease
//进入目录安装apk  ./android/app/build/outputs/apk/release
adb install app-release.apk 

-其他代码

~/FileConstant.java

public class FileConstant {
    //远程下载服务地址
    public static final String JS_BUNDLE_REMOTE_URL = "http://192.168.1.127/patches.zip";
    //本地bundle文件名
    public static final String JS_BUNDLE_LOCAL_FILE = "index.android.bundle";

    //sdcard中bundle的加载路径
    public static String get_JS_BUNDLE_LOCAL_PATH(Context context){
        return context.getExternalCacheDir().getPath() + File.separator+ "patches/index.android.bundle";
    }
    //sdcard中下载后文件的存放文件夹路径
    public static String get_JS_PATCH_LOCAL_FOLDER(Context context){
        return context.getExternalCacheDir().getPath() + File.separator+ "patches";
    }
    //sdcard中下载的zip包存放位置
    public static String get_JS_PATCH_LOCAL_PATH(Context context){
        return context.getExternalCacheDir().getPath() + File.separator+ "patches.zip";
    }
    //sdcard中下载后的增量包pat存放位置
    public static String get_JS_PATCH_LOCAL_FILE(Context context){
        return context.getExternalCacheDir().getPath() + File.separator+ "patches/patches.pat";
    }
}

2)拆包增量更新-Android

-打更新包bundle(同-1)

-生成差异化补丁文件

将初始版本old.bundle和热更版本new.bundle进行比对,生成patches.pat
=> 将pat和图片压缩成 patches.zip
=> 将zip包放入远程文件服务器待下载


生成patches.pat
patches.pat
    public static void main(String[] args) {
        String o = getStringFromPat("/Users/tugaofeng/Desktop/old.bundle");
        String n = getStringFromPat("/Users/tugaofeng/Desktop/new.bundle");
        // 对比
        diff_match_patch dmp = new diff_match_patch();
        LinkedList<diff_match_patch.Diff> diffs = dmp.diff_main(o, n);
        // 生成差异补丁包
        LinkedList<diff_match_patch.Patch> patches = dmp.patch_make(diffs);
        // 解析补丁包
        String patchesStr = dmp.patch_toText(patches);

        try {
            // 将补丁文件写入到某个位置
            Files.write(Paths.get("/Users/tugaofeng/Desktop/patches.pat"), patchesStr.getBytes());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

-根据业务判断是否需要更新(同-1)

-下载zip 至指定的sdcard地址(同-1)

-解析zip并写入sdcard

    private class DownloadReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //下载完成,收到广播
            long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            if(completeDownloadId == mDownloadId){
                // 1.解压并写入sdcard对应地址
                RefreshUpdateUtils.decompression(getApplicationContext());
                zipfile.delete();

                // 2.将下载好的patches文件与assets目录下的原index.android.bundle合并,得到新的
                // bundle文件,并写入sdcard中
                mergePatAndAsset();
            }
        }
    }

-将Assets内的index.android.bundle和下载完成的差异化补丁pat合并,并生成新的index.android.bundle写入sdcard对应位置

//拆包增量更新bundle
    private void mergePatAndAsset() {
        // 1.获取本地Assets目录下的bunlde
        String assetsBundle = RefreshUpdateUtils.getJsBundleFromAssets(getApplicationContext());
        // 2.获取.pat文件字符串
        // /storage/emulated/0/Android/data/包名/cache/patches/patches.pat
        String patcheStr = RefreshUpdateUtils.getStringFromPat(FileConstant.get_JS_PATCH_LOCAL_FILE(this));
        if (patcheStr == null || "".equals(patcheStr)){
            return;
        }
        // 3.初始化 dmp
        diff_match_patch dmp = new diff_match_patch();
        // 4.转换pat
        LinkedList<diff_match_patch.Patch> pathes = (LinkedList<diff_match_patch.Patch>) dmp.patch_fromText(patcheStr);
        // 5.与assets目录下的bundle合并,生成新的bundle
        Object[] bundleArray = dmp.patch_apply(pathes,assetsBundle);
        // 6.保存新的bundle
        // 至/storage/emulated/0/Android/data/包名/cache/patches/index.android.bundle
        try {
            Writer writer = new FileWriter(FileConstant.get_JS_BUNDLE_LOCAL_PATH(this));
            String newBundle = (String) bundleArray[0];
            writer.write(newBundle);
            writer.close();
            // 7.删除.pat文件
            // 路径为/storage/emulated/0/Android/data/包名/cache/patches/patches.pat
            File patFile = new File(FileConstant.get_JS_PATCH_LOCAL_FILE(this));
            patFile.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

-其他代码

~/diff_match_patch.java

//~/RefreshUpdateUtils.java
    //将.pat or bundle文件转换为String
    public static String getStringFromPat(String patPath) {
        FileReader reader = null;
        String result = "";
        try {
            reader = new FileReader(patPath);
            int ch = reader.read();
            StringBuilder sb = new StringBuilder();
            while (ch != -1) {
                sb.append((char)ch);
                ch  = reader.read();
            }
            reader.close();
            result = sb.toString();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    //从本地Assets获取bundle
    public static String getJsBundleFromAssets(Context context) {
        String result = "";
        try {
            InputStream is = context.getAssets().open(FileConstant.JS_BUNDLE_LOCAL_FILE);
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            result = new String(buffer,"UTF-8");

        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

3)图片增量更新-Android

图片增量更新需要修改RN源码。

-修改RN源码。
注意:RN库版本升级时别忘了修改。。

渲染图片的方法在:node_modules / react-native / Libraries / Image /AssetSourceResolver.js 下:

defaultAsset(): ResolvedAssetSource {  
  if (this.isLoadedFromServer()) {  
    return this.assetServerURL();  
  }  
  
  if (Platform.OS === 'android') {  
    return this.isLoadedFromFileSystem() ?  
//存在离线Bundle文件时,从Bundle文件所在目录加载图片
      this.drawableFolderInBundle() :  
//否则从Asset资源目录下加载
      this.resourceIdentifierWithoutScale();  
  } else {  
    return this.scaledAssetPathInBundle();  
  }  
}  

对源码做如下修改:

...
import type { PackagerAsset } from 'AssetRegistry';
// 1-新增全局变量
// !!!注意:每次基于某个原生版本的RN热更版本新增的图片都要在此处新增加(不是覆盖哦)
// 比如原生版本1.0.0,RN热更版本1.0.0-1时新增a.png
//var patchImgNames = '|a.png|';
// 比如原生版本1.0.0,RN热更版本1.0.0-2时新增b.png
//var patchImgNames = '|a.png|b.png|';
// 比如原生版本2.0.0(2.0的原生版本asset里已经会包含a和b.png),暂无RN热更版本时
//var patchImgNames = '';
var patchImgNames = '|src_res_images_offer_message_red.png|src_res_images_banner_default.png|'; 

...
// 2-修改此函数
  isLoadedFromFileSystem(): boolean {
    // return !!this.bundlePath;  //注释此处,新增如下代码
    var imgFolder = getAssetPathInDrawableFolder(this.asset);  
    var imgName = imgFolder.substr(imgFolder.indexOf("/") + 1);  
    var isPatchImg = patchImgNames.indexOf("|"+imgName+"|") > -1;  
    return !!this.bundlePath && isPatchImg; 
  }

-打增量代码包和增量图片

=> 打更新包
=> 生成差异化补丁文件pat
=> 将pat和本次热更新增的图片压缩成patches.zip
=> 将zip包放入远程文件服务器待下载


第一次热更包 1.0.0-1
第二次热更包1.0.0-2

-效果展示

版本1.0.0热更至1.0.0-1.gif 版本1.0.0-1的sdcard图片目录.png
版本1.0.0-1热更至1.0.0-2.gif
版本1.0.0-2的sdcard图片目录.png

4)全量热更新-iOS

demo

-修改podfile,新增SSZipArchive【解压】和AFNetworking【文件下载】

  pod 'SSZipArchive'
  pod 'AFNetworking', '~> 3.0'
cd /ios && pod install

-打更新包bundle(包括更新的图片和代码)

react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

-创建bundle存放路径,使用plist文件去存储版本号和下载路径

//创建bundle路径
-(void)createPath{
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:[self getVersionPlistPath]]) {
        return;
    }
    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
    NSString *path = [paths lastObject];
    NSString *directryPath = [path stringByAppendingPathComponent:@"IOSBundle"];
    [fileManager createDirectoryAtPath:directryPath withIntermediateDirectories:YES attributes:nil error:nil];
    NSString *filePath = [directryPath stringByAppendingPathComponent:@"Version.plist"];
    [fileManager createFileAtPath:filePath contents:nil attributes:nil];
}

-根据业务判断是否需要更新

//获取版本信息
-(void)getAppVersion{
    
    //从服务器上获取版本信息,与本地plist存储的版本进行比较
    //1.获取本地plist文件的版本号 
    NSString* plistPath=[self getVersionPlistPath];
    NSMutableDictionary *data = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
    
    NSInteger localV=[data[@"bundleVersion"]integerValue];
  
    //本地plist的版本号
    printf("%ld ", (long)localV);

    //保留业务,根据当前热更版本号与本地比对,进行判断是否下载
    if(true){
        //下载bundle文件 存储在 Doucuments/IOSBundle/下
        NSString*url=@"http://192.168.1.127/patches.zip";
        [[DownLoadTool defaultDownLoadTool] downLoadWithUrl:url];
    }
}

-下载zip 至指定沙盒路径地址

-(void)downLoadWithUrl:(NSString*)url{
    //根据url下载相关文件
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
    NSURL *URL = [NSURL URLWithString:url];
    NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
        //获取下载进度
        NSLog(@"Progress is %f", downloadProgress.fractionCompleted);
    } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
        //有返回值的block,返回文件存储路径
        NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
        
        // file:///Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName
        NSURL* targetPathUrl = [documentsDirectoryURL URLByAppendingPathComponent:@"IOSBundle"];
        return [targetPathUrl URLByAppendingPathComponent:[response suggestedFilename]];
        
    } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
        if(error){
            //下载出现错误
            NSLog(@"%@",error);
            
        }else{
            // [self showPromptWithStr:@"更新完毕。请重新启动******!"];
            //下载成功
            //  file:///Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName/patches.zip
            NSLog(@"File downloaded to: %@", filePath);
            self.zipPath = [[filePath absoluteString] substringFromIndex:7];
            //下载成功后更新本地存储信息
            NSDictionary*infoDic=@{@"bundleVersion":@3,@"downloadUrl":url};
            [UpdateDataLoader sharedInstance].versionInfo=infoDic;
            
            [[UpdateDataLoader sharedInstance] writeAppVersionInfoWithDictiony:[UpdateDataLoader sharedInstance].versionInfo];
            
            //解压并删除压缩包
            [self unZip];
            [self deleteZip]; 
        }
    }];
    [downloadTask resume];
}

-解压&删除压缩包

//解压压缩包
-(BOOL)unZip{
    if (self.zipPath == nil) {
        return NO;
    }
//Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/kiOSFileName/patches.zip
    NSString *zipPath = self.zipPath;
    
    // /Users/tugaofeng/Library/Developer/CoreSimulator/Devices/C4A3CBA7-3313-4EF1-A281-FF04064041B0/data/Containers/Data/Application/20FC6C6B-1397-45C7-A7C5-836EA272EE1C/Documents/IOSBundle
    NSString *destinationPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]stringByAppendingString:@"/IOSBundle"];
    BOOL success = [SSZipArchive unzipFileAtPath:zipPath
                                   toDestination:destinationPath];
    return success;  
}  
//删除压缩包  
-(void)deleteZip{  
    NSError* error = nil;  
    [[NSFileManager defaultManager] removeItemAtPath:self.zipPath error:&error];  
}  
下载前沙盒文件
下载解压后沙盒文件

-RN调用JSBundle的时候判断,当沙盒对应位置的bundle不为空时加载其bundle,否则加载原包内的bundle

    NSURL *jsCodeLocation;
    
    NSString* iOSBundlePath = [[UpdateDataLoader sharedInstance] iOSFileBundlePath];
    NSString* filePath = [iOSBundlePath stringByAppendingPathComponent:@"/main.jsbundle"];
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            jsCodeLocation = [NSURL URLWithString:[iOSBundlePath stringByAppendingString:@"/main.jsbundle"]];
    
    }else{
        jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
    }

参考资料

React Native 实现热部署、差异化增量热更新
React-Native开发iOS篇-热更新的代码实现

上一篇 下一篇

猜你喜欢

热点阅读