简介

以前写过一个补丁更新的文章,此处会做一个更精简的最小化实现,以便于集成.为了使逻辑具有通用性,将剥离对AFNetworking和ReativeCocoa的依赖.原来的文章,可以先看这里: http://www.ios122.com/2015/12/jspatconline/

这么做的意义

先交代动机和意义,或许应该成为自己博客的一个标准框架内容之一,不然以后自己需要看着,也不过是一堆干瘪的代码.基本的逻辑图,如上!此处,我就从简!

从简的原因有3:

  1. 补丁更新,状态可以设计的很复杂,就像开头那篇文章提到的那样,但是我感觉没多大必要,至少在我们的App中;
  2. 我想演示一个相对完整的逻辑,但是又不想耗费太多的时间构建场景;
  3. 从简后的方案,简单但够用了,至少目前针对我们的项目来说;

所以说:这篇文章的意义,其实是在于简化已有的热更新代码,越简单越好维护.

基本思路

  1. App启动时,判断特定的服务器接口所返回的图片url是否为最新,判断方式就是比对返回值中的md5字段与本地保存的资源的url是否一致;
  2. 如果图片资源有更新,则下载解压到指定的缓存目录,初步打算以资源文件的md5来划分文件夹,来避免冲突;
  3. 读取图片时,优先从缓存目录读取,缓存目录不存在再从ipa资源包中读取;

下面就一步一步来实现了.

App启动时,判断有无最新图片资源

此处主要涉及到的可能的技术点:

1. 如何用基础的网络类库发送网络请求?

先简单封装一个函数来获取,用到了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,莫怪!

2. 如何校验下载的文件的md5值,如果你需要的话?

开头那篇文章链接里,有提到.核心,其实是在于下载文件之后,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 @"";
}
}

3. 使用什么保存与获取本地缓存资源的md5等信息?

好吧,我打算直接使用用户配置文件,

NSString * source_patch_key = @"SOURCE_PATCH";

[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];
patchInfo = [[NSUserDefaults standardUserDefaults] objectForKey: source_patch_key]; NSLog(@"patchInfo:%@", patchInfo);

补丁下载与解压

此处主要涉及到的可能的技术点:

1. 如何基于图片缓存信息来找到指定的缓存目录?

问题本身有些绕口,其实我想做的就是根据补丁的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); }];

2. 如何解压文件到指定目录?

如果需要安装 CocoaPods ,建议使用 brew:

brew install CocoaPods

解压本身推荐 SSZipArchive 库,一行代码搞定:

[SSZipArchive unzipFileAtPath:location.path toDestination: patchCachePath overwrite:YES password:nil error:&error];

3. 在什么时候更新本地的缓存资源的相关信息?

建议是在下载并解压资源文件到指定缓存目录后,再更新补丁的相关缓存信息,因为这个信息,读取图片时,也是需要的.如果删除某个补丁,按照目前的设计,一种比较偷懒的方案就是,在服务器上放上一个新的空资源文件就可以了.

NSString * source_patch_key = @"SOURCE_PATCH";

[[NSUserDefaults standardUserDefaults] setObject:patchInfo forKey: source_patch_key];

读取图片功能扩展

此处主要涉及到的可能的技术点:

1. 如何用基础的网络类库下载文件?

依然是要封装一个简单函数,下载完成后,通过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. 如何判断bundle中是否含有某文件?

可以使用 fileExistsAtPath,但其实使用 -pathForResource: ofType: 就够了,因为找不到资源问加你时,它返回nil,所以我们直接调用它,然后判断返回是否为 nil 即可:

NSString * imgPath = [mainBundle pathForResource:imgName ofType:@"png"];

3. 将代码如何与原有的imageNamed:逻辑合并?

不需要初始复制到缓存目录 + 初始请求最新的资源补丁信息 + 代码迁移合并 + 接口优化

相对完整的逻辑代码

注意,按照目前的设计,就不需要初始把原来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;

如果热更新生效,运行看到的应该是一个锤子图片:

后记

我觉得,这篇文章最大的特点是,完整记录了一次优化解决问题的过程;示例代码看起来前后有些不太统一,是因为: 我不是先有了方案再写博客,而是借助博客本身来梳理思路,简化逻辑!如此,写博客,就不单单是一个耗时的分享知识的过程,更成为了一个帮助自己思考的有力工具!赞!!!

参考资源:

实现iOS图片等资源文件的热更新化(四): 一个最小化的补丁更新逻辑的更多相关文章

  1. 实现iOS图片等资源文件的热更新化(五): 一个简单完整的资源热更新页面

    简介 一个简单的关于页面,有一个图片,版本号,App名称等,着重演示各个系列的文章完整集成示例. 动机与意义 这是系列文章的最后一篇.今天抽空写下,收下尾.文章本身会在第四篇的基础上,简单扩充下代码, ...

  2. 实现iOS图片等资源文件的热更新化(零): 序

    必要的序 以后在写系列文章,准备把基本的规划和动机等,单独作为一个小的序言部分给独立出来.序言部分,可以较为完整地交待系列文章的写作动机,所展示的编码技术可能的应用场景等.个人,我还是比较看重文章或者 ...

  3. 实现iOS图片等资源文件的热更新化(三):动态的资源文件夹

    简介 此文,将尝试动态从某个不确定的文件夹中加载资源文件.文章,会继续完善自定义的 imageNamed 函数,并为下一篇文章铺垫. 这么做的意义 正如我们经常所说的那样,大多数情景知道做事的意义往往 ...

  4. 实现iOS图片等资源文件的热更新化(二):自定义的动态 imageNamed

    这篇文章,要解决的是,使用一个自定义的 imageNamed 函数来替代系统的 imageNamed 函数.内部逻辑,将贯穿对比论证 关于"合适"的图片的定义.对iOS加载图片的规 ...

  5. 实现iOS图片等资源文件的热更新化(一): 从Images.xcassets导出合适的图片

    本文会基于一个已有的脚本工具自动导出所有的图片;最终给出的是一个从 Images.xcassets 到基于文件夹的精简 合适 的图片资源集的完整过程.难点在于从完整图片集到精简图片集,肯定是基于一个定 ...

  6. winform代码反编译后图片等资源文件恢复解决方案

    用Reflector工具反编译的winform代码,图片等资源文件不能很好的反编译成功. 这里有一个笨的解决方案.首先我们要了解图片资源当初加入到工程的几种方式,及他们所在的位置. 一般winform ...

  7. IntelliJ IDEA - 热部署插件JRebel ,对静态资源文件进行热部署?javascript、css、vm文件

    IntelliJ IDEA - 热部署插件JRebel ,对静态资源文件进行热部署?javascript.css.vm文件https://blog.csdn.net/feng_pump/article ...

  8. iOS - 集成Bundle资源文件包

    1.Bundle 文件 Bundle 文件,简单理解,就是资源文件包.我们将许多图片.XIB.文本文件组织在一起,打包成一个 Bundle 文件.方便在其他项目中引用包内的资源. Bundle 文件是 ...

  9. iOS开发那些事-iOS应用本地化-资源文件本地化

    资源文件包括:图片文件.音频文件以及前文提到的Localizable.strings等文件,它们的特点是都是随着应用一起打包发布.但就本地化而言无论是图片文件还是音频文件都必须实现的步骤都是类似的,因 ...

随机推荐

  1. Understanding ASP.NET MVC Filters and Attributes

    这篇文章把Asp.net MVC的filter介绍的很详细,值得收藏. http://www.dotnet-tricks.com/Tutorial/mvc/b11a280114-Understandi ...

  2. C#使用Timer.Interval指定时间间隔与指定时间执行事件

    C#中,Timer是一个定时器,它可以按照指定的时间间隔或者指定的时间执行一个事件. 指定时间间隔是指按特定的时间间隔,如每1分钟.每10分钟.每1个小时等执行指定事件: 指定时间是指每小时的第30分 ...

  3. 003. Asp.Net Routing与MVC 之一: 请求如何到达MVC

    基础知识 本文用到的基础知识:URL.HttpModule 与 HttpHandler.IIS 的请求处理过程. URL HttpModule与HttpHandler IIS7.0的请求处理过程 OK ...

  4. jQuery实现返回顶部

    由于项目需要,写了个返回顶部的小功能... /*返回顶部*/ function toTop() { $(".to_top").hide(); $(window).scroll(fu ...

  5. 【译】用jQuery 处理XML--浏览器中的XML与JavaScript

    用jQuery 处理XML--写在前面的话 用jQuery 处理XML-- DOM(文本对象模型)简介 用jQuery 处理XML--浏览器中的XML与JavaScript 用jQuery 处理XML ...

  6. Java中常量小知识

    常量分类:常量分为静态常量,非静态常量(全局常量),局部常量 静态常量:要么定义的时候赋初值,要么在静态代码块中赋值 非静态常量:要么在定义的时候赋初值,要么在代码块中赋值 局部常量:可以在定义时赋初 ...

  7. Atitit 发帖机系列(8)  词法分析器v5 版本新特性说明)

    Atitit 发帖机系列(8)  词法分析器v5 版本新特性说明) v5  增加对sql单引号的内部支持.可以作为string 结构调整,使用递归法重构循环发..放弃循环发. V4 java dsl词 ...

  8. Atitit usrQBK1600 技术文档的规范标准化解决方案

    Atitit usrQBK1600 技术文档的规范标准化解决方案 1.1. Keyword关键词..展关键词,横向拓展比较,纵向抽象细化拓展知识点1 1.2. 标题必须有高大上词汇,参考文章排行榜,1 ...

  9. Dapper sql in

    应用场景: 使用 sql的 Case When Then 批量更新某张表,底层数据库用到了Dapper 代码示例: public int UpdateClientReceivedResult(Dict ...

  10. salesforce 零基础学习(四十)Custom Settings简单使用

    有时候,项目中我们需要设置类似白名单的功能,即某些用户或者某种Profile的用户不走一些校验或者走一些校验,这时,使用Custom Settings功能可以很好的解决这一需求. Custom Set ...