Achieve hot update of iOS images and other resource files (4): a minimal patch update logic, ios resource files
Introduction
I have previously written a patch update Article. Here we will make a more streamlined implementation to facilitate integration. to make the logic universal, the dependency on AFNetworking and ReativeCocoa will be removed. original article, you can look at here: http://www.ios122.com/2015/12/jspatconline/
Significance of doing so
First, explain the motives and meanings. Maybe it should be one of the standard framework content of your blog. Otherwise, you will need to watch it later, but it is just a bunch of dry code. The basic logic diagram is as above! Here, I will simplify it!
There are three reasons for simplicity:
Therefore, the significance of this article is to simplify the existing hot update code, the simpler the better the maintenance.
Basic Ideas
The following is a step-by-step implementation.
When the App is started, check whether there are any latest image resources
Possible technical points involved here:
1. How to send network requests using the basic network library?
First, a function is encapsulated to obtain the block. block is often used, but it cannot be clearly remembered until now. Most of them are copied from other places, and then the parameter is changed. I can't remember it!
- (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];}
Based on block, the called code is very simple.
[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); } }];
Okay, I admit that AFNetworking is used to it. I haven't used the original network Request Code for a long time. It's a little low, no wonder!
2. How to verify the md5 value of the downloaded file, if necessary?
At the beginning of the article, we mentioned that the core is actually the calculation of the md5 value after the file is downloaded, and the rest is the string comparison operation.
Note that you must first introduce the system library
#include <CommonCrypto/CommonDigest.h>
/*** Obtain the md5 information of the file. ** @ param path: file path. ** @ return indicates the md5 value of the file. */-(NSString *) mcMd5HashOfPath :( NSString *) path {NSFileManager * fileManager = [NSFileManager defaultManager]; // ensure that the file exists. 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 <vertex; I ++) {[output appendFormat: @ "% 02x", digest [I];} return output;} else {return @"";}}
3. What is used to save and obtain md5 information of local cache resources?
Okay, I plan to directly use the user configuration file,
NSString * source_patch_key = @"SOURCE_PATCH";[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];patchInfo = [[NSUserDefaults standardUserDefaults] objectForKey: source_patch_key];NSLog(@"patchInfo:%@", patchInfo);
Patch download and decompression
Possible technical points involved here:
1. How to Find the specified cache directory based on the image cache information?
The problem itself is a bit difficult. In fact, what I want to do is to put the patch md5 in different cache folders. For example, if the patch md5 is e963ed645c50a004697530fa596f180b, it is placed in the patch/release folder. encapsulate a simple method to return the cache path based on md5:
- (NSString *)cachePathFor:(NSString * )patchMd5{ NSArray * LibraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); NSString * cachePath = [[[LibraryPaths objectAtIndex:0] stringByAppendingFormat:@"/Caches/patch"] stringByAppendingPathComponent:patchMd5]; return cachePath;}
This is similar to the following:
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);}];
2. How do I extract files to a specified directory?
To install CocoaPods, we recommend that you usebrew
:
brew install CocoaPods
Recommended for decompressionSSZipArchiveLibrary, one line of code to deal:
[SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error];
3. When can I update information about local cache resources?
We recommend that you download and extract the resource file to the specified cache directory, and then update the cache information of the patch. Because of this information, it is also required to read images. if you delete a patch, according to the current design, a relatively lazy solution is to put a new blank Resource file on the server.
NSString * source_patch_key = @"SOURCE_PATCH";[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];
Extended image reading Function
Possible technical points involved here:
1. How to download files using the basic network library?
It is still to encapsulate a simple function. After the download is complete, the temporary storage location of the file is exported through the 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];}
2. How can I determine whether a bundle contains a file?
AvailableFileExistsAtPath, But actually use-PathForResource: ofType:This is enough because nil is returned when the resource cannot be found and you are asked, so we call it directly and determine whether the returned result isNilYou can:
NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"];
3. How to combine the Code with the original imageNamed: logic?
You do not need to initially copy to the cache directory + initial request for the latest resource patch information + code migration and merging + Interface Optimization
Relatively complete logic code
Note: according to the current design, you do not need to initially copy the bundle in the original ipa to the cache directory. When there are no relevant resources in the cache directory, it will automatically try to read the bundle from the ipa, bundle Conventions use main. bundle to simplify operations,
Category, two methods for external exposure:
#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
When the App is started, or in other appropriate places, check whether there are any updates:
- (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;}
Internal implementation has been optimized a lot, but it is not complicated:
#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
Now, the code for loading images is simpler:
UIImage * image = [UIImage yf_imageNamed:@"sub/sample"];self.sampleImageView.image = image;
If the hot update takes effect, the running should see a hammer image:
Postscript
I think the biggest feature of this article is that it fully records the process of optimizing and solving the problem. The sample code seems to be somewhat inconsistent because: I did not first have a solution to write a blog, instead, we use the blog itself to sort out ideas and simplify the logic! Therefore, writing a blog is not only a time-consuming process of sharing knowledge, but also a powerful tool to help you think! Like !!!
Reference resources:
- This section contains the complete Xcode project code, less than 100 KB
- Series of articles, exclusive github Project
- IOS NSURLSession Example (http get, POST, Background Downlads)
- Worth of experiences: JSPatch-based Real-time Bug fixing solution for iOS apps, with source code.
- ZipArchive is a simple utility class for zipping and unzipping files on iOS and Mac.
- Pod Installation and Use