之前寫過一個補丁更新的文章,此處會作一個更精簡的最小化實現,以便於集成.爲了使邏輯具備通用性,將剝離對AFNetworking和ReativeCocoa的依賴.原來的文章,能夠先看這裏: http://www.ios122.com/2015/12/jspatconline/ios
先交代動機和意義,或許應該成爲本身博客的一個標準框架內容之一,否則之後本身須要看着,也不過是一堆乾癟的代碼.基本的邏輯圖,如上!此處,我就從簡!git
從簡的緣由有3:github
補丁更新,狀態能夠設計的很複雜,就像開頭那篇文章提到的那樣,可是我感受沒多大必要,至少在咱們的App中;shell
我想演示一個相對完整的邏輯,可是又不想耗費太多的時間構建場景;json
從簡後的方案,簡單但夠用了,至少目前針對咱們的項目來講;緩存
因此說:這篇文章的意義,實際上是在於簡化已有的熱更新代碼,越簡單越好維護.服務器
App啓動時,判斷特定的服務器接口所返回的圖片url是否爲最新,判斷方式就是比對返回值中的md5字段與本地保存的資源的url是否一致;網絡
若是圖片資源有更新,則下載解壓到指定的緩存目錄,初步打算以資源文件的md5來劃分文件夾,來避免衝突;session
讀取圖片時,優先從緩存目錄讀取,緩存目錄不存在再從ipa資源包中讀取;app
下面就一步一步來實現了.
此處主要涉及到的可能的技術點:
先簡單封裝一個函數來獲取,用到了block.block常常用,但到如今都記不太清形式,大都是從其餘處copy下,而後改改參數.記不住,也懶得記!
- (void)fetchPatchInfo:(NSString *) urlStr completionHandler:(void (^)(NSDictionary * patchInfo, NSError * error))completionHandler { NSURLSessionConfiguration * defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]]; NSURL * url = [NSURL URLWithString:urlStr]; NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) { NSDictionary * patchInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];; completionHandler(patchInfo, error); }]; [dataTask resume]; }
基於block,調用的代碼也就很簡答了.
[self fetchPatchInfo: @"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json" completionHandler:^(NSDictionary * patchInfo, NSError * error) { if ( ! error) { NSLog(@"patchInfo:%@", patchInfo); }else { NSLog(@"fetchPatchInfo error: %@", error); } }];
好吧,我認可AFNetworking用習慣了,很久沒用原始的網絡請求的代碼了,有點low,莫怪!
開頭那篇文章連接裏,有提到.核心,實際上是在於下載文件以後,md5值的計算,剩餘的就是字符串比較操做了.
注意要先引入系統庫
#include <CommonCrypto/CommonDigest.h>
/** * 獲取文件的md5信息. * * @param path 文件路徑. * * @return 文件的md5值. */ -(NSString *)mcMd5HashOfPath:(NSString *)path { NSFileManager * fileManager = [NSFileManager defaultManager]; // 確保文件存在. if( [fileManager fileExistsAtPath:path isDirectory:nil] ) { NSData * data = [NSData dataWithContentsOfFile:path]; unsigned char digest[CC_MD5_DIGEST_LENGTH]; CC_MD5( data.bytes, (CC_LONG)data.length, digest ); NSMutableString * output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; for( int i = 0; i < CC_MD5_DIGEST_LENGTH; i++ ) { [output appendFormat:@"%02x", digest[i]]; } return output; } else { return @""; } }
好吧,我打算直接使用用戶配置文件,
NSString * source_patch_key = @"SOURCE_PATCH"; [[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key]; patchInfo = [[NSUserDefaults standardUserDefaults] objectForKey: source_patch_key]; NSLog(@"patchInfo:%@", patchInfo);
此處主要涉及到的可能的技術點:
問題自己有些繞口,其實我想作的就是根據補丁的md5,放到不一樣的緩存文件夾,如補丁md5爲 e963ed645c50a004697530fa596f180b,則對應放到 patch/e963ed645c50a004697530fa596f180b 文件夾.封裝一個簡單的根據md5返回緩存路徑的方法吧:
- (NSString *)cachePathFor:(NSString * )patchMd5 { NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); NSString * cachePath = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches/patch"] stringByAppendingPathComponent:patchMd5]; return cachePath; }
使用時,相似這樣:
NSString * urlStr = [patchInfo objectForKey: @"url"]; [weak_self downloadFileFrom:urlStr completionHandler:^(NSURL * location, NSError * error) { if (error) { NSLog(@"download file url:%@ error: %@", urlStr, error); return; } NSString * cachePath = [weak_self cachePathFor: [patchInfo objectForKey:@"md5"]]; NSLog(@"location:%@ cachePath:%@",location, cachePath); }];
若是須要安裝 CocoaPods ,建議使用 brew
:
brew install CocoaPods
解壓自己推薦 SSZipArchive 庫,一行代碼搞定:
[SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error];
建議是在下載並解壓資源文件到指定緩存目錄後,再更新補丁的相關緩存信息,由於這個信息,讀取圖片時,也是須要的.若是刪除某個補丁,按照目前的設計,一種比較偷懶的方案就是,在服務器上放上一個新的空資源文件就能夠了.
NSString * source_patch_key = @"SOURCE_PATCH"; [[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];
此處主要涉及到的可能的技術點:
依然是要封裝一個簡單函數,下載完成後,經過block傳出文件臨時的保存位置:
-(void) downloadFileFrom:(NSString * ) urlStr completionHandler: (void (^)(NSURL *location, NSError * error)) completionHandler { NSURL * url = [NSURL URLWithString:urlStr]; NSURLSessionConfiguration * defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]]; NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url completionHandler:^(NSURL * location, NSURLResponse * response, NSError * error) { completionHandler(location,error); }]; [downloadTask resume]; }
可使用 fileExistsAtPath,但其實使用 -pathForResource: ofType: 就夠了,由於找不到資源問加你時,它返回nil,因此咱們直接調用它,而後判斷返回是否爲 nil 便可:
NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"];
不須要初始複製到緩存目錄 + 初始請求最新的資源補丁信息 + 代碼遷移合併 + 接口優化
注意,按照目前的設計,就不須要初始把原來ipa中的bundle複製到緩存目錄了;當緩存目錄中沒有相關資源時,會自動嘗試從ipa中的bundle讀取,bundle約定統一使用 main.bundle 來簡化操做,
類目,對外暴露兩個方法:
#import <UIKit/UIKit.h> @interface UIImage (imageNamed_bundle_) /* load img smart .*/ + (UIImage *)yf_imageNamed:(NSString *)imgName; /* smart update for patch */ + (void)yf_updatePatchFrom:(NSString *) pathInfoUrlStr; @end
App啓動時,或在其餘合適的地方,要注意檢查有無更新:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. /* fetch pathc info every time */ NSString * patchUrlStr = @"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json"; [UIImage yf_updatePatchFrom: patchUrlStr]; return YES; }
內部實現,優化了許多,但也算不上覆雜:
#import "UIImage+imageNamed_bundle_.h" #import <SSZipArchive.h> @implementation UIImage (imageNamed_bundle_) + (NSString *)yf_sourcePatchKey{ return @"SOURCE_PATCH"; } + (void)yf_updatePatchFrom:(NSString *) pathInfoUrlStr { [self yf_fetchPatchInfo: pathInfoUrlStr completionHandler:^(NSDictionary *patchInfo, NSError *error) { if (error) { NSLog(@"fetchPatchInfo error: %@", error); return; } NSString * urlStr = [patchInfo objectForKey: @"url"]; NSString * md5 = [patchInfo objectForKey:@"md5"]; NSString * oriMd5 = [[[NSUserDefaults standardUserDefaults] objectForKey: [self yf_sourcePatchKey]] objectForKey:@"md5"]; if ([oriMd5 isEqualToString:md5]) { // no update return; } [self yf_downloadFileFrom:urlStr completionHandler:^(NSURL *location, NSError *error) { if (error) { NSLog(@"download file url:%@ error: %@", urlStr, error); return; } NSString * patchCachePath = [self yf_cachePathFor: md5]; [SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error]; if (error) { NSLog(@"unzip and move file error, with urlStr:%@ error:%@", urlStr, error); return; } /* update patch info. */ NSString * source_patch_key = [self yf_sourcePatchKey]; [[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key]; }]; }]; } + (NSString *)yf_relativeCachePathFor:(NSString *)md5 { return [@"patch" stringByAppendingPathComponent:md5]; } + (UIImage *)yf_imageNamed:(NSString *)imgName{ NSString * bundleName = @"main"; /* cache dir */ NSString * md5 = [[[NSUserDefaults standardUserDefaults] objectForKey: [self yf_sourcePatchKey]] objectForKey:@"md5"]; NSString * relativeCachePath = [self yf_relativeCachePathFor: md5]; return [self yf_imageNamed: imgName bundle:bundleName cacheDir: relativeCachePath]; } + (UIImage *)yf_imageNamed:(NSString *)imgName bundle:(NSString *)bundleName cacheDir:(NSString *)cacheDir { NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); bundleName = [NSString stringWithFormat:@"%@.bundle",bundleName]; NSString * ipaBundleDir = [NSBundle mainBundle].resourcePath; NSString * cacheBundleDir = ipaBundleDir; if (cacheDir) { cacheBundleDir = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches"] stringByAppendingPathComponent:cacheDir]; } imgName = [NSString stringWithFormat:@"%@@3x",imgName]; NSString * bundlePath = [cacheBundleDir stringByAppendingPathComponent: bundleName]; NSBundle * mainBundle = [NSBundle bundleWithPath:bundlePath]; NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"]; /* try load from ipa! */ if ( ! imgPath && ! [ipaBundleDir isEqualToString: cacheBundleDir]) { bundlePath = [ipaBundleDir stringByAppendingPathComponent: bundleName]; mainBundle = [NSBundle bundleWithPath:bundlePath]; imgPath = [mainBundle pathForResource:imgName ofType:@"png"]; } UIImage * image; static NSString * model; if (!model) { model = [[UIDevice currentDevice]model]; } if ([model isEqualToString:@"iPad"]) { NSData * imageData = [NSData dataWithContentsOfFile: imgPath]; image = [UIImage imageWithData:imageData scale:2.0]; }else{ image = [UIImage imageWithContentsOfFile: imgPath]; } return image; } + (void)yf_fetchPatchInfo:(NSString *) urlStr completionHandler:(void (^)(NSDictionary * patchInfo, NSError * error))completionHandler { NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]]; NSURL * url = [NSURL URLWithString:urlStr]; NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSDictionary * patchInfo = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];; completionHandler(patchInfo, error); }]; [dataTask resume]; } + (void) yf_downloadFileFrom:(NSString * ) urlStr completionHandler: (void (^)(NSURL *location, NSError * error)) completionHandler { NSURL * url = [NSURL URLWithString:urlStr]; NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:nil delegateQueue: [NSOperationQueue mainQueue]]; NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { completionHandler(location,error); }]; [downloadTask resume]; } + (NSString *)yf_cachePathFor:(NSString * )patchMd5 { NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); NSString * cachePath = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches"] stringByAppendingPathComponent:[self yf_relativeCachePathFor: patchMd5]]; return cachePath; } @end
如今,加載圖片的代碼更簡單了:
UIImage * image = [UIImage yf_imageNamed:@"sub/sample"]; self.sampleImageView.image = image;
若是熱更新生效,運行看到的應該是一個錘子圖片:
我以爲,這篇文章最大的特色是,完整記錄了一次優化解決問題的過程;示例代碼看起來先後有些不太統一,是由於: 我不是先有了方案再寫博客,而是藉助博客自己來梳理思路,簡化邏輯!如此,寫博客,就不僅僅是一個耗時的分享知識的過程,更成爲了一個幫助本身思考的有力工具!贊!!!