实现iOS图片等资源文件的热更新化(四):一个最小化的补丁更新逻辑
这么做的意义
先交代动机和意义,或许应该成为自己博客的一个标准框架内容之一,不然以后自己需要看着,也不过是一堆干瘪的代码.基本的逻辑图,如上!此处,我就从简!
从简的原因有3:
补丁更新,状态可以设计的很复杂,就像开头那篇文章提到的那样,但是我感觉没多大必要,至少在我们的App中;
我想演示一个相对完整的逻辑,但是又不想耗费太多的时间构建场景;
从简后的方案,简单但够用了,至少目前针对我们的项目来说;
所以说:这篇文章的意义,其实是在于简化已有的热更新代码,越简单越好维护.
基本思路
App启动时,判断特定的服务器接口所返回的图片url是否为最新,判断方式就是比对返回值中的md5字段与本地保存的资源的url是否一致;
如果图片资源有更新,则下载解压到指定的缓存目录,初步打算以资源文件的md5来划分文件夹,来避免冲突;
读取图片时,优先从缓存目录读取,缓存目录不存在再从ipa资源包中读取;
下面就一步一步来实现了.
App启动时,判断有无最新图片资源
此处主要涉及到的可能的技术点:
1.如何用基础的网络类库发送网络请求?
先简单封装一个函数来获取,用到了block.block经常用,但到现在都记不太清形式,大都是从其他处copy下,然后改改参数.记不住,也懒得记!
-(void)fetchPatchInfo:(NSString)urlStrcompletionHandler:(void(^)(NSDictionarypatchInfo,NSErrorerror))completionHandler
{
NSURLSessionConfigurationdefaultConfigObject=[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSessiondefaultSession=[NSURLSessionsessionWithConfiguration:defaultConfigObjectdelegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
NSURLurl=[NSURLURLWithString:urlStr];
NSURLSessionDataTaskdataTask=[defaultSessiondataTaskWithURL:url
completionHandler:^(NSDatadata,NSURLResponseresponse,NSErrorerror){
NSDictionarypatchInfo=[NSJSONSerializationJSONObjectWithData:dataoptions:0error:&error];;
completionHandler(patchInfo,error);
}];
[dataTaskresume];
}
基于block,调用的代码也就很简答了.
[selffetchPatchInfo:@"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json"
completionHandler:^(NSDictionarypatchInfo,NSErrorerror){
if(!error){
NSLog(@"patchInfo:%@",patchInfo);
}else
{
NSLog(@"fetchPatchInfoerror:%@",error);
}
}];
好吧,我承认AFNetworking用习惯了,好久没用原始的网络请求的代码了,有点low,莫怪!
2.如何校验下载的文件的md5值,如果你需要的话?
开头那篇文章链接里,有提到.核心,其实是在于下载文件之后,md5值的计算,剩余的就是字符串比较操作了.
注意要先引入系统库
1
#include
/
获取文件的md5信息.
@parampath文件路径.
@return文件的md5值.
/
-(NSString)mcMd5HashOfPath:(NSString)path
{
NSFileManagerfileManager=[NSFileManagerdefaultManager];
//确保文件存在.
if([fileManagerfileExistsAtPath:pathisDirectory:nil])
{
NSDatadata=[NSDatadataWithContentsOfFile:path];
unsignedchardigest[CC_MD5_DIGEST_LENGTH];
CC_MD5(data.bytes,(CC_LONG)data.length,digest);
NSMutableStringoutput=[NSMutableStringstringWithCapacity:CC_MD5_DIGEST_LENGTH2];
for(inti=0;i {
[outputappendFormat:@"%02x",digest[i]];
}
returnoutput;
}
else
{
return@"";
}
}
3.使用什么保存与获取本地缓存资源的md5等信息?
好吧,我打算直接使用用户配置文件,
NSStringsource_patch_key=@"SOURCE_PATCH";
[[NSUserDefaultsstandardUserDefaults]setObject:patchInfoforKey:source_patch_key];
patchInfo=[[NSUserDefaultsstandardUserDefaults]objectForKey:source_patch_key];
NSLog(@"patchInfo:%@",patchInfo);
补丁下载与解压
此处主要涉及到的可能的技术点:
1.如何基于图片缓存信息来找到指定的缓存目录?
问题本身有些绕口,其实我想做的就是根据补丁的md5,放到不同的缓存文件夹,如补丁md5为e963ed645c50a004697530fa596f180b,则对应放到patch/e963ed645c50a004697530fa596f180b文件夹.封装一个简单的根据md5返回缓存路径的方法吧:
-(NSString)cachePathFor:(NSString)patchMd5
{
NSArrayLibraryPaths=NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSStringcachePath=[[[LibraryPathsobjectAtIndex:0]stringByAppendingFormat:@"/Caches/patch"]stringByAppendingPathComponent:patchMd5];
returncachePath;
}
使用时,类似这样:
NSStringurlStr=[patchInfoobjectForKey:@"url"];
[weak_selfdownloadFileFrom:urlStrcompletionHandler:^(NSURLlocation,NSErrorerror){
if(error){
NSLog(@"downloadfileurl:%@error:%@",urlStr,error);
return;
}
NSStringcachePath=[weak_selfcachePathFor:[patchInfoobjectForKey:@"md5"]];
NSLog(@"location:%@cachePath:%@",location,cachePath);
}];
2.如何解压文件到指定目录?
如果需要安装CocoaPods,建议使用brew:
1
brewinstallCocoaPods
解压本身推荐SSZipArchive库,一行代码搞定:
1
[SSZipArchiveunzipFileAtPath:location.pathtoDestination:patchCachePathoverwrite:YESpassword:nilerror:&error];
3.在什么时候更新本地的缓存资源的相关信息?
建议是在下载并解压资源文件到指定缓存目录后,再更新补丁的相关缓存信息,因为这个信息,读取图片时,也是需要的.如果删除某个补丁,按照目前的设计,一种比较偷懒的方案就是,在服务器上放上一个新的空资源文件就可以了.
NSStringsource_patch_key=@"SOURCE_PATCH";
[[NSUserDefaultsstandardUserDefaults]setObject:patchInfoforKey:source_patch_key];
读取图片功能扩展
此处主要涉及到的可能的技术点:
1.如何用基础的网络类库下载文件?
依然是要封装一个简单函数,下载完成后,通过block传出文件临时的保存位置:
-(void)downloadFileFrom:(NSString)urlStrcompletionHandler:(void(^)(NSURLlocation,NSErrorerror))completionHandler
{
NSURLurl=[NSURLURLWithString:urlStr];
NSURLSessionConfigurationdefaultConfigObject=[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSessiondefaultSession=[NSURLSessionsessionWithConfiguration:defaultConfigObjectdelegate:selfdelegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionDownloadTaskdownloadTask=[defaultSessiondownloadTaskWithURL:url
completionHandler:^(NSURLlocation,NSURLResponseresponse,NSErrorerror)
{
completionHandler(location,error);
}];
[downloadTaskresume];
}
2.如何判断bundle中是否含有某文件?
可以使用fileExistsAtPath,但其实使用-pathForResource:ofType:就够了,因为找不到资源问加你时,它返回nil,所以我们直接调用它,然后判断返回是否为nil即可:
1
NSStringimgPath=[mainBundlepathForResource:imgNameofType:@"png"];
3.将代码如何与原有的imageNamed:逻辑合并?
不需要初始复制到缓存目录+初始请求最新的资源补丁信息+代码迁移合并+接口优化
相对完整的逻辑代码
注意,按照目前的设计,就不需要初始把原来ipa中的bundle复制到缓存目录了;当缓存目录中没有相关资源时,会自动尝试从ipa中的bundle读取,bundle约定统一使用main.bundle来简化操作,
类目,对外暴露两个方法:
#import
@interfaceUIImage(imageNamed_bundle_)
/loadimgsmart./
+(UIImage)yf_imageNamed:(NSString)imgName;
/smartupdateforpatch/
+(void)yf_updatePatchFrom:(NSString)pathInfoUrlStr;
@end
App启动时,或在其他合适的地方,要注意检查有无更新:
-(BOOL)application:(UIApplication)applicationdidFinishLaunchingWithOptions:(NSDictionary)launchOptions{
//Overridepointforcustomizationafterapplicationlaunch.
/fetchpathcinfoeverytime/
NSStringpatchUrlStr=@"https://raw.githubusercontent.com/ios122/ios_assets_hot_update/master/res/patch_04.json";
[UIImageyf_updatePatchFrom:patchUrlStr];
returnYES;
}
内部实现,优化了许多,但也算不上复杂:
#import"UIImage+imageNamed_bundle_.h"
#import
@implementationUIImage(imageNamed_bundle_)
+(NSString)yf_sourcePatchKey{
return@"SOURCE_PATCH";
}
+(void)yf_updatePatchFrom:(NSString)pathInfoUrlStr
{
[selfyf_fetchPatchInfo:pathInfoUrlStr
completionHandler:^(NSDictionarypatchInfo,NSErrorerror){
if(error){
NSLog(@"fetchPatchInfoerror:%@",error);
return;
}
NSStringurlStr=[patchInfoobjectForKey:@"url"];
NSStringmd5=[patchInfoobjectForKey:@"md5"];
NSStringoriMd5=[[[NSUserDefaultsstandardUserDefaults]objectForKey:[selfyf_sourcePatchKey]]objectForKey:@"md5"];
if([oriMd5isEqualToString:md5]){//noupdate
return;
}
[selfyf_downloadFileFrom:urlStrcompletionHandler:^(NSURLlocation,NSErrorerror){
if(error){
NSLog(@"downloadfileurl:%@error:%@",urlStr,error);
return;
}
NSStringpatchCachePath=[selfyf_cachePathFor:md5];
[SSZipArchiveunzipFileAtPath:location.pathtoDestination:patchCachePathoverwrite:YESpassword:nilerror:&error];
if(error){
NSLog(@"unzipandmovefileerror,withurlStr:%@error:%@",urlStr,error);
return;
}
/updatepatchinfo./
NSStringsource_patch_key=[selfyf_sourcePatchKey];
[[NSUserDefaultsstandardUserDefaults]setObject:patchInfoforKey:source_patch_key];
}];
}];
}
+(NSString)yf_relativeCachePathFor:(NSString)md5
{
return[@"patch"stringByAppendingPathComponent:md5];
}
+(UIImage)yf_imageNamed:(NSString)imgName{
NSStringbundleName=@"main";
/cachedir/
NSStringmd5=[[[NSUserDefaultsstandardUserDefaults]objectForKey:[selfyf_sourcePatchKey]]objectForKey:@"md5"];
NSStringrelativeCachePath=[selfyf_relativeCachePathFor:md5];
return[selfyf_imageNamed:imgNamebundle:bundleNamecacheDir:relativeCachePath];
}
+(UIImage)yf_imageNamed:(NSString)imgNamebundle:(NSString)bundleNamecacheDir:(NSString)cacheDir
{
NSArrayLibraryPaths=NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
bundleName=[NSStringstringWithFormat:@"%@.bundle",bundleName];
NSStringipaBundleDir=[NSBundlemainBundle].resourcePath;
NSStringcacheBundleDir=ipaBundleDir;
if(cacheDir){
cacheBundleDir=[[[LibraryPathsobjectAtIndex:0]stringByAppendingFormat:@"/Caches"]stringByAppendingPathComponent:cacheDir];
}
imgName=[NSStringstringWithFormat:@"%@@3x",imgName];
NSStringbundlePath=[cacheBundleDirstringByAppendingPathComponent:bundleName];
NSBundlemainBundle=[NSBundlebundleWithPath:www.baiyuewang.netbundlePath];
NSStringimgPath=[mainBundlepathForResource:imgNameofType:@"png"];
/tryloadfromipa!/
if(!imgPath&&![ipaBundleDirisEqualToString:cacheBundleDir]){
bundlePath=[ipaBundleDirstringByAppendingPathComponent:bundleName];
mainBundle=[NSBundlebundlewww.wang027.comWithPath:bundlePath];
imgPath=[mainBundlepathForResource:imgNameofType:@"png"];
}
UIImageimage;
staticNSStringmodel;
if(!model){
model=[[UIDevicecurrentDevice]model];
}
if([modelisEqualToString:@"iPad"]){
NSDataimageData=[NSDatadataWithContentsOfFile:imgPath];
image=[UIImageimageWithData:imageDatascale:2.0];
}else{
image=[UIImageimageWithContentsOfFile:imgPath];
}
returnimage;
}
+(void)yf_fetchPatchInfo:(NSString)urlStrcompletionHandler:(void(^)(NSDictionarypatchInfo,NSErrorerror))completionHandler
{
NSURLSessionConfigurationdefaultConfigObject=[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSessiondefaultSession=[NSURLSessionsessionWithConfiguration:defaultConfigObjectdelegate:nildelegateQueue:[NSOperationQueuemainQueue]];
NSURLurl=[NSURLURLWithString:urlStr];
NSURLSessionDataTaskdataTask=[defaultSessiondataTaskWithURL:url
completionHandler:^(NSDatadata,NSURLResponseresponse,NSErrorerror){
NSDictionarypatchInfo=[NSJSONSerializationJSONObjectWithData:dataoptions:0error:&error];;
completionHandler(patchInfo,error);
}];
[dataTaskresume];
}
+(void)yf_downloadFileFrom:(NSString)urlStrcompletionHandler:(void(^)(NSURLlocation,NSErrorerror))completionHandler
{
NSURLurl=[NSURLURLWithString:urlStr];
NSURLSessionConfigurationdefaultConfigObject=[NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSessiondefaultSession=[NSURLSessionsessionWithConfiguration:defaultConfigObjectdelegate:nildelegateQueue:[NSOperationQueuemainQueue]];
NSURLSessionDownloadTaskdownloadTask=[defaultSessiondownloadTaskWithURL:url
completionHandler:^(NSURLlocation,NSURLResponseresponse,NSErrorerror)
{
completionHandler(location,error);
}];
[downloadTaskresume];
}
+(NSString)yf_cachePathFor:(NSString)patchMd5
{
NSArrayLibraryPaths=NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSStringcachePath=[[[LibraryPathsobjectAtIndex:0]stringByAppendingFormat:@"/Caches"]stringByAppendingPathComponent:[selfyf_relativeCachePathFor:patchMd5]];
returncachePath;
}
@end
现在,加载图片的代码更简单了:
1
2
UIImageimage=[UIImageyf_imageNamed:@"sub/sample"];
self.sampleImageView.image=image;
|
|