RN熱更新原理

熱更新

ReactNative告別CodePush残家,自建熱更新版本升級(jí)環(huán)境

微軟的CodePush熱更新非常難用大家都知道泣港,速度更墻了沒什么區(qū)別。

另一方面历葛,加入不希望代碼放到別人的服務(wù)器上正塌,自己寫接口更新總歸安全一些。

那如何自己做一個(gè)ReactNative更新管理工具恤溶。

ReactNative啟動(dòng)原理

首先我們要弄清react-native啟動(dòng)的原理乓诽,是直接調(diào)用jslocation中的jsbundle文件和assets資源文件。

由此咒程,我們可以自己通過請(qǐng)求服務(wù)器接口來判斷版本鸠天,并下載最新的然后替換相應(yīng)的文件,然后從這個(gè)文件調(diào)用啟動(dòng)APP帐姻。這就像之前的一些H5 APP一樣的做版本的管理稠集。

以iOS為例,我們需要分一下幾個(gè)步驟搭建自己的RN升級(jí)工具:

一饥瓷、設(shè)置默認(rèn)jsbundle地址(比如document文件夾)

1.首先打包的時(shí)候把jsbundle和assets放入copy bundle resource剥纷,每次啟動(dòng)后,檢測(cè)document文件夾是否存在呢铆,不存在則拷貝到document文件夾晦鞋,然后給RN框架讀取啟動(dòng)。

我們建立如下的bundle文件管理類:

MXBundleHelper.h

#import <Foundataion/Foundation.h>
@interface MXBundleHelper : NSObject
  
+(NSURL *)getBundlerPath;

@end

MXBundlerHelper.m

#import "MaxBundleHelper.h"
#import "RCTBundleURLProvider.h"
@implementation MABundleHelper
+(NSURL *)getBundlePath {
  #ifdef DEBUG
  NSURL * jsCodeLocation = [[RCTBundleURLProvider sharedSetting] jsBundleURLForBundleRoot:@"index.ios" fallbackResource: nil];
  return jsCodeLocation;
  #else
  // 需要存放和讀取的document路徑
  // jsbundle地址
  NSString * jsCachePath = [NSString stringWithFormat:@"%@/\%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, Yes)[0],@"main.jsbundle"];
  // assets文件夾地址
  NSString *assetsCachePath = [NSString stringWithFormat:@"%@/\%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0],@"assets"];
  
  // 判斷JSBundle是否存在
  BOOL jsExist = [[NSFileManager defaultManager] fileExistsAtPath: jsCachePath];
  // 如果已經(jīng)存在
  if (jsExist) {
    NSLog(@"js已存在:%@",jsCachePath);
  } else {
    // 如果不存在
    NSString * jsBundle = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"jsbundle"];
    [[NSFileManager defaultManager] copyItemAtPath: jsBundlePath toPath:jsCache error:nil];
    NSLog(@"js已拷貝到Document: %@", jsCachePath);
  }
  
  // 判斷assets是否存在
  BOOL assetsExist = [[NSFileManager defaultManager] fileExistsAtPath: assetsCachePath];
  // 如果已存在
  if (assetsExist) {
    NSLog(@"assets已存在:%@",assetsCachePath);
  } else {
    NSString *assetsBundlePath = [[NSBundle mainBundle] pathForResource:@"assets" ofType: nil];
    [[NSFileManager defaultManager] copyItemAtPath: assetsBundlePath toPath: assetsCachePath error: nil];
    NSLog(@"assets已拷貝至Document:%@"棺克,assetsCachePath)悠垛;
  }
  return [NSURL URLWithString: jsCachePath];
#endif
}

二、做升級(jí)檢測(cè)娜谊,有更新則下載确买,然后對(duì)本地文件進(jìn)行替換:

加入我們不立即做更新,可以更新后替換因俐,然后不會(huì)影響本次APP的使用,下次使用就會(huì)默認(rèn)是最新的了周偎。如果立即更新的話抹剩,需要使用到RCTBridge類里的relaod函數(shù)進(jìn)行重啟。

這里通過NSURLSession進(jìn)行下載蓉坎,然后zip解壓縮等方法來實(shí)現(xiàn)文本的替換澳眷。

MXUpdateHelper.h

#import <Foundation/Foundation.h>
typedef void(^FinishBlock) (NSInteger status, id data);
@interface MXUpdateHelper : NSObject
+(void)checkUpdate:(FinishBlock)finish;
@end

MXUpdateHelper.m

#import "MXUpdateHelper.h"

@implementation MXUpdateHelper
+(void)checkupdate:(FinishBlock)finish {
  NSString *url = @"http://www.xxx.com/xxxx";
  NSMutableURLRequest * newRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString: url]];
  [newRequest setHTTPMethod:@"GET"];
  [NSURLConnection sendAsynchronousRequest: newRequest queue:[NSOperationQueue mainQueue] completionHandler: ^(NSURLResponse * response, NSData * data, NSError * connectionError) {
    if (connectionError == nil) {
      // 請(qǐng)求自己服務(wù)器的API,判斷當(dāng)前的JS版本是否最新
      /*{
        "version": "1.0.5",
        "fileUrl":"http://www.xxxx.com/xxx.zip",
        "message": "有新版本蛉艾,請(qǐng)更新到我們最新的版本"钳踊,
        "forecUpdate": "NO"
      }*/
      // 加入需要更新
      NSString * curVersion = @"1.0.0";
      NSString * newVersion = @"2.0.0"
      // 一般情況下不一樣衷敌,就是舊版本了
      if (![curVersion isEqualToString: newVersion]) {
        finish(1,data);
      } else {
        finish(0,nil);;
      }
    }
  }];
}
@end

三、APPdelegate中的定制拓瞪、彈框缴罗,直接強(qiáng)制更新等

如果需要強(qiáng)制刷新reload,我們新建RCTView的方式也需要稍微修改下祭埂,通過新建一個(gè)RCTBridge的對(duì)象面氓。因?yàn)镽CTBridge中有reload的接口可以使用。

#import "AppDelegate.h"
#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"
#import "MXBundleHelper.h"
#import "MXUpdateHelper.h"
#import "MXFileHelper.h"
#import "SSZipArchive.h"
@interface AppDelegate()<UIAlertViewDelegate>
@property (nonatomic, strong) RCTBridge *bridge;
@property (nonatomic, strong) NSDictionary *versionDic;
@end

@implementation Appdelegate
- (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions {
  NSURL *jsCodeLocation;
  jsCodeLocation = [MXBundleHelper getBundlePath];
  
  _bridge = [[RCTBridge alloc] initWithBundleURL: jsCodeLocation mouduleName:@"MXVersionManger" initialProperties: nil];
  
  rootView.backgroundColor = [UIColor alloc] initWithRed: 1.0f green:1.0f blue:1.0f alpha:1];
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIVeiwController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  
  [self.window makeKeyAndVisible];
  
  __weak AppDelegate *weakself = self;
  // 更新檢測(cè)
  [MXUpdateHelper checkUpdate:^(NSInteger status, id data) {
    if (status == 1) {
      wekself.versionDic = data;
      /*
      這里具體關(guān)乎用戶體驗(yàn)的方式就多種多樣了蛆橡,比如自動(dòng)立即更新舌界,彈框立即更新,自動(dòng)下載打開再更新等泰演。
      */
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示" message:data[@"message"] delegatee:self cancelButtonTitle:@"取消" otherButtonTitle:@"現(xiàn)在更新", nil];
      [alert show];
      // 進(jìn)行下載呻拌,并更新
      //  下載完,覆蓋JS和assets,并reload界面
    }
  }];
  return YES;
}

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
  if (buttonIndex == 1) {
    // 更新
    [[MXFileHelper shared] downloadFileWithURLString: _versionDic[@"fileurl"] finish:^(NSInteger status, id data) {
      if (status == 1) {
        NSLog(@"下載完成");
        NSError *error;
        NSString *filePath = (NSString *)data;
        NSString *desPath = [NSString stringWithFormat:@"%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]];
        [SSZipArchive unzipFileAtPath: filePath toDestination: desPath overwrite: YES password:nil error:&error];
        if (!error) {
          NSLog(@"解壓成功");
          [_bridge reload];
        } else {
          NSLog(@"解壓失敗");
        }
      }
     }];
  }
}

流程簡(jiǎn)單睦焕,通過接口請(qǐng)求版本藐握,然后下載到document去訪問。其中需要做版本緩存复亏,Zip的解壓縮趾娃,以及文件拷貝。

// demo: https://github.com/rayshen/MXHotdog

差異化更新

以上我們完成了代碼的熱更新工作缔御。但是如果bundle太大的情況下抬闷,會(huì)增加用戶的流浪消耗,我們可以用生成補(bǔ)丁包的方式來進(jìn)一步減少更新包zip的體積耕突。

以安卓為例:

促使化項(xiàng)目發(fā)布時(shí)笤成,生成并保留一份index.android.bundle文件。

有版本更新時(shí)眷茁,生成新的index.android.bundle文件炕泳,使用google-diff-match-patch對(duì)比兩個(gè)文件,并生成差異不定文件上祈。app下載補(bǔ)丁文件培遵,在使用google-diff-match-patch和assets目錄下的初始版本合并,生成新的index.android.bundle文件

1.添加google-diff-match-patch庫

google-diff-match-patch庫包含了多種編程語言的庫文件登刺,我們使用其中的java版本籽腕,所以我們將其的提取出來,方便大家下載使用:

http://download.csdn.net/detail/u013718120/9833398

下載之后添加到項(xiàng)目目錄即可

2.生成補(bǔ)丁包

String oldPackeg = RefreshUpdateUtils.getStringFromPat(oldPath);
String newPackeg = RefreshUpdateUtils.getStringFromPat(newPath);

// 對(duì)比
diff_match_patch dmp = new diff_match_patch();
LinkedList<Diff> diffs = dmp.diff_main(oldPackeg, newPackeg);

// 生成差異補(bǔ)丁包
LinkedList<Patch> patches = dmp.patch_make(diffs);

// 解析補(bǔ)丁包
String patchesStr = dmp.patch_toText(patches);

try {
  // 將補(bǔ)丁寫入到某個(gè)位置
  Files.write(Paths.get("targetPath"), pathcesStr.getBytes());
} catch (IOException e) {
  e.printStacckTrace();
}
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;
}

3.下載完成皇耗,解壓后執(zhí)行mergePatAndAsset方法將Assets目錄下的index.android.bundle和pat文件合并

/**
* 下載完成后收到廣播
*/
publci class CompleteReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    long completeId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    if (completeID == mDownLoadId) {
      // 1. 解壓
      RefreshUpdateUtils.decompression();
      zipfile.delete();
      // 2. 將下載好的patches文件與assets目錄下的原index.android.bundle合并,得到新的bundle文件
      mergePatAndAsset();
      startActivity(new Intent(MainActivity.this, MyReactActivity.class));
    }
  }
}

4揍很、合并

/**
* 合并patches文件
*/
private void mergePatAndAsset() {
  // 1. 獲取Assets目錄下的bundle
  String assetsBundle = RefreshUpdateUtils.getJsBundleFromAssets(getApplicationContext());
  // 2. 獲取.pat淄川
  String patchStr = RefreshUpdateUtils.getStringFromPat(FileConstant.JS_PATCH_LOCAL_FILE);
  // 3. 初始化 dmp
  diff_match_patch dmp = new diff_match_patch();
  // 4. 轉(zhuǎn)換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
  try {
    Writer writer = new FileWriter(FileConstant.JS_BUNDLE_LOCAL_PATH);
    String newBundle = (String) bundleArray[0];
    writer.write(newBundle);
    writer.close();
    // 7. 刪除.pat文件
    File patFile = new File(FileConstant.JS_PATCH_LOCAL_FILE);
    patFile.delete();
  } catch(IOException e) {
    e.printStackTrace();
  }
}

總結(jié)下來万伤,合并分為如下過程:

(1)獲取Assets目錄下的bundle文件,轉(zhuǎn)換為字符串呜袁。

(2)解析.pat文件將其轉(zhuǎn)換為字符串敌买。

(3)調(diào)用patch_fromText獲取patches補(bǔ)丁包。

(4)調(diào)用patch_apply方法將第四步中生成patches補(bǔ)丁包與第一步中獲取的bundle合并生成新的bundle傅寡。

(5)保存bundle放妈。

6.讀取Assets目錄下的bundle文件

/**
* 獲取Assets目錄下的bundle文件
* @return
*/
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;
}

以上步驟執(zhí)行完成后,我們就獲取了新的bundle文件荐操,繼而加載新的bundle文件芜抒,實(shí)現(xiàn)React Native熱更新。上述差異包更新方式只能更新不含圖片引用的bundle代碼文件托启,如果需要增量更新文件宅倒,需要修改React Native源碼。

四屯耸、修改React Native 圖片加載源碼

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

defaultAsset(): ResolveAssetSource {
  if (this.isLoadedFromServer()) {
    return this.assetServerURL();
  } 
  
  if (Platform.OS === 'android') {
    return this.isLoadedFromFileSystem() ?
      this.drawableFolderInBundle() : this.resourceIdentifierWithoutScale();
  } else {
    return this.scaledAssetPathInBundle();
  }
}

defaultAsset方法中根據(jù)平臺(tái)的不同分別執(zhí)行不同的圖片加載邏輯拐迁。這里主要看android platform:drawableFolderInBundle方法為在存在離線Bundle文件時(shí),從Bundle文件所在目錄加載圖片疗绣。resourceIdentifierWithoutScale方法從Asset資源目錄下加載线召。由此,我們需要修改isLoadedFromFileSystem方法中的邏輯多矮。

(1)在AssetSourceResolver.js中增加增量圖片全局名稱變量

'use strict';

export type ResolvedAssetSource = {
  __packager_asset: boolean,
  width: number,
  height: number,
  uri: string,
  scale: number,
};

import type { PackagerAsset } from 'AssetRegistry';
// 全局緩存 
var patchImgNames = ''; // 新加的代碼
const PixelRatio = require('PixelRatio');
...

(2)修改isLoadedFromFileSystem方法

/* 原代碼
* isLoadedFromFileSystem(): boolean {
*    return !!this.bundlePath;
* }
*/
isLoadedFromFileSystem(): boolean {
  var imgFolder = getAssetPathInDrawableFolder(this.asset);
  var imgName = imgFolder.substr(imgFolder.indexOf("/")+1);
  var isPatchImg = patchImgNames.indexOf("|" + imgName + "|") > -1;
  return !!this.bundlePath && isPathcImg;
}

patchImgNames是增量更新的圖片名稱字符串全局緩存缓淹,其中包括所有更新和修改的圖片名稱,并且以“|”隔開塔逃。當(dāng)系統(tǒng)加載圖片時(shí)讯壶,如果在緩存中存在該圖片名稱,證明是我們?cè)隽扛禄蛐薷牡膱D片湾盗,所以需要系統(tǒng)從Bundle文件所在目錄下加載伏蚊。否則直接從原有asset資源加載。

(3)每當(dāng)有圖片增量更新格粪,修改patchImgName躏吊,例如images_ic_1.png和images_ic_2.png為增量更新或修改的圖片。

var patchImgNames = '|images_ic_1.png|images_ic_2.png|';

注:生成bundle目錄時(shí)帐萎,圖片資源都會(huì)放在同一目錄下(drawable-mdpi)比伏,如果引用圖片包含其他路徑,例如require("./img/test1.png")吓肋,圖片在img目錄下凳怨,則圖片加載時(shí)會(huì)自動(dòng)將img目錄轉(zhuǎn)換為圖片名稱:“img_test1.png”瑰艘,即圖片所在文件夾名稱會(huì)作為圖片名的前綴是鬼。此時(shí)圖片名配置文件中的名稱也需要聲明為"img_test1.png"肤舞,例如:"|img_test1.png|img_test2.png|"

(4)重新打包

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

(5)生成.pat差異補(bǔ)丁包,并壓縮為zip更新包

更新包沒有太大區(qū)別均蜜,依然是增量更新的圖片和pat李剖。

小提示:

因?yàn)镽N會(huì)從drawable-mdpi下加載圖片,所以我們只需要將drawable-mdpi打包即可囤耳,其余的篙顺,drawalbe-xx文件夾可以不放進(jìn)zip。

(6)既然是增量更新拂蝎,就會(huì)分為第一次更新前雨后的情況掌桩。所以需要聲明一個(gè)標(biāo)識(shí)來表示當(dāng)前是否為第一次下發(fā)更新包

第一次更新前:

1.緩存中不存在更新包扳碍,pat補(bǔ)丁包需要與Asset下的index.android.bundle進(jìn)行合并,生成新的bundle文件宰僧。

2.增量圖片直接下發(fā)到緩存中。

第一次更新后观挎,即第一次更新后的更新操作:

1.緩存下存在更新包琴儿,需要將新的pat補(bǔ)丁包與緩存下上次生成的index.android.bundle進(jìn)行合并,生成新的bundle文件嘁捷。

2.增量圖片需要添加到緩存bundle所在文件下的drawable-mdpi目錄造成。

本次下發(fā)的更新包與之前的bundle進(jìn)行合并以及將圖片添加到之前drawable-mdpi后,需要?jiǎng)h除雄嚣。

核心代碼如下:

// 下載前檢查本地是否存在更新包晒屎。FIRST_UPDATE來標(biāo)識(shí)是否為第一次下發(fā)更新包
bundleFile = new File(FileConstant.LOCAL_FOLDER);
if (bundleFile != null && bundleFile.exists()) {
       ACache.get(getApplicationContext()).put(AppConstant.FIRST_UPDATE, false);
} else {
  // 第一次更新
   ACache.get(getApplicationContext()).put(AppcONSTANT.FIRST_UPDATE, true);
}
/**
 * 下載完成后,處理ZIP壓縮包
 */
private void handleZIP() {
  // 開啟單獨(dú)線程现诀,解壓夷磕,合并
  new Thread(new Runnable() {
    @Override
    public void run() {
      boolean result = (Boolean) ACache.get(getApplicationContext()).getAsObject(AppConstant.FIRST_UPDATE);
        if (result) {
        // 解壓到根目錄              
        FileUtils.decompression(FileConstant.JS_PATCH_LOCAL_FOLDER);
            // 合并
            mergePatAndAsset();
        } else {
            // 解壓到future目錄                 
          FileUtils.decompression(FileConstant.FUTURE_JS_PATCH_LOCAL_FOLDER);
            // 合并
            mergePatAndBundle();
        }
            // 刪除ZIP壓縮包
                FileUtils.deleteFile(FileConstant.JS_PATCH_LOCAL_PATH);
    }
  }).start();
}
/**
 * 與Asset資源目錄下的bundle進(jìn)行合并
 */
private void mergePatAndAsset() {
  // 解析Asset目錄下的bundle文件
  String assetsBundle = FileUtils.getJsBundleFromAssets(getApplicationContext());
  // 解析bundle當(dāng)前目錄下.pat文件字符串
  String patcheStr = FileUtils.getStringFromPat(FileConstant.JS_PATCH_LOCAL_FILE);
  // 合并
  merge(patcheStr, assetsBundle);
  // 刪除pat
  FileUtils.deleteFile(FileConstant.JS_PATCH_LOACL_FILE);
}
/**
 * 與本地下的bundle進(jìn)行合并
 */
private void mergePatAndBundle() {
  // 解析本地目錄下的bundle
  String assetsBundle = FileUtils.getJsBundleFromSDCard(FileConstant.JS_BUNDLE_LOACL_PATH);
  // 解析最新下發(fā)的.pat文件字符串
  String patcheStr = FileUtils.getStringFromPat(FileConstant.FUTURE_PAT_PATH);
  // 合并
  merge(patchesStr, assetsBundle);
  // 添加圖片
  FileUtils.copyPatchImgs(FileConstant.FUTURE_DRAWABLE_PATH, FileConstant.DRAWABLE_PATH);
  // 刪除本次下發(fā)的更新文件
  FileUtils.traversalFile(FileConstant.FUTURE_JS_PATCH_LOACAL_FOLDER);
}
/**
 * 合并,生成新的bundle文件
 */
private void merge(String patcheStr, String bundle) {
  // 初始化dmp
  diff_match_patch dmp = new diff_match_patch();
  // 轉(zhuǎn)化pat
  LinkedList<diff)match_patch.Patch> pathes = (LinkedList<diff_match_patch.Patch>)dmp.patch_fromText(patcheStr);
  // pat與bundle合并仔沿,并生成新的bundle
  Object[] bundleArray = dmp.patch_apply(patches, bundle);
  // 保存新的bundle文件
  try {
    Writer writer = new FleWriter(FileConstant.JS_BUNDLE_LOCAL_PATH);
    String newBundle = (String)bundleArray[0];
    writer.write(newBundle);
    writer.close();
  } catch (IOExcepiton e) {
    e.printStackTrace();
  }
}

FileUtils 工具類函數(shù)

/**
 * 將圖片復(fù)制到bundle所在文件夾下的drawable-mdpi
 * @param srcFilePath
 * @param destFilePath
 */
public static void copyPatchImgs(String srcFilePath, String destFilePath) {
  File root = new File(srcFilePath);
  File[] files;
  if (root.exists() && root.listFiles() != null) {
    files = root.listFiles();
    for (File file: files) {
      File oldFile = new File(srcFilePath+file.getName());
      File newFile = new File(destFilePath+file.getName());
      DataInputStream dis = null;
      DataOutputStream dos = null;
      try {
        dos = new DataOutputStream(new FileOutputStream(newFile));
        dis = new DataInputStream(new FileInputStream(oldFile));
      } catch (FileNotFoundException e) {
        e.printStackTrace();
      }
      
      int temp;
      try {
        while ((temp = dis.read()) != -1) {
          dos.write(temp);
        }
        dis.close();
        dos.close();
      }catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
/**
 * 遍歷刪除文件夾下所有文件
 * @param filePath
 */
public static void traversalFile(String filePath) {
  File file = new File(filePath);
  if (file.exists()) {
    File[] files = file.listFiles();
    for (File f: files) {
      if (f.isDirectory()) {
        traversalFile(f.getAbsolutePath());
      } else {
        f.delete();
      }
    }
    file.delete();
  }
}
/**
 * 刪除指定的File
 * @param filePath
 */
public static void deleteFile(String filePath) {
  File patFile = new File(filePath);
  if (patFile.exists()) {
    patFile.delete();
  }
}

當(dāng)客戶端下載解析后坐桩,圖片的增量更新就搞定了,這樣我們的更新包就小了很多封锉。缺點(diǎn)也很明顯绵跷,每次更新RN版本的時(shí)候,都要修改RN的源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末成福,一起剝皮案震驚了整個(gè)濱河市碾局,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奴艾,老刑警劉巖净当,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡像啼,警方通過查閱死者的電腦和手機(jī)俘闯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忽冻,“玉大人真朗,你說我怎么就攤上這事∩希” “怎么了遮婶?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長湖笨。 經(jīng)常有香客問我旗扑,道長,這世上最難降的妖魔是什么慈省? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任肩豁,我火速辦了婚禮,結(jié)果婚禮上辫呻,老公的妹妹穿的比我還像新娘清钥。我一直安慰自己,他們只是感情好放闺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布祟昭。 她就那樣靜靜地躺著,像睡著了一般怖侦。 火紅的嫁衣襯著肌膚如雪篡悟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天匾寝,我揣著相機(jī)與錄音搬葬,去河邊找鬼。 笑死艳悔,一個(gè)胖子當(dāng)著我的面吹牛急凰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播猜年,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抡锈,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了乔外?” 一聲冷哼從身側(cè)響起床三,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杨幼,沒想到半個(gè)月后撇簿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聂渊,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年四瘫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了歧沪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡莲组,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暖夭,到底是詐尸還是另有隱情锹杈,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布迈着,位于F島的核電站竭望,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏裕菠。R本人自食惡果不足惜咬清,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奴潘。 院中可真熱鬧旧烧,春花似錦、人聲如沸画髓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奈虾。三九已至夺谁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肉微,已是汗流浹背匾鸥。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碉纳,地道東北人勿负。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像劳曹,于是被迫代替她去往敵國和親笆环。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354