简介

以前写过一个补丁更新的文章,此处会做一个更精简的最小化实现,以便于集成.为了使逻辑具有通用性,将剥离对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. wex5&.net开发

    一.环境开发 1.baas.net:菜单“窗口”->“首选项” 确定后左侧生成bass.net目录,设置iis网站目录(因为.net网站必须在iis中部署) 2.sqlserver:根据项目可选 ...

  2. 【vscode】如何在vscode 中配置:TypeScript开发node环境

    入门流程,大神绕行. 安装环境 这就不多说了,安装开发的环境. 安装vscode 下载地址:https://code.visualstudio.com/ 安装Nodejs 下载地址:https://n ...

  3. Java虚拟机3:常用JVM命令参数

    之后写的东西就会用到虚拟机参数了,现在这里汇个总自己平时用到的.看到的一些虚拟机参数.现在看不懂没关系,反正之后都会用到的: (1)-Xms20M 表示设置堆容量的最小值为20M,必须以M为单位 (2 ...

  4. 用avalon实现一个完整的todomvc(带router)

    照着todomvc官网的例子,做了一个avalon版的todos,功能全都有了,而且加了router模块,比司徒大大写的都完善(≧▽≦)/~ js文件整整100行,初次使用avalon,书写过程中绕了 ...

  5. java 锁3

    先谈线程的状态: 具体来说有, NEW. Running. Blocked.此状态的线程阻塞,它正在等待监视器锁——等待另外一个线程释放锁(通俗说就是等它执行完synchronized了的方法/代码块 ...

  6. Java-继承,多态练习09-22-01

    1.实现如下类之间的继承关系,并编写Music类来测试这些类. 父类: package com.lianxi; public class Instrument { //属性 private Strin ...

  7. Node.js入门:Hello World

    马上开始我们第一个Node.js应用:“Hello World”.打开你的编辑器,创建一个hello.js文件.编写代码保存该文件,并通过Node.js来执行. 控制台输出 1 console.log ...

  8. ubuntu下在apache部署python站点

    ubuntu下在apache部署python站点 我的是ubuntu14 32为的虚拟机,默认安装的python为3.4 环境:apache + mysql + django + python3 软件 ...

  9. Java EE开发平台随手记6——Mybatis扩展4

    这篇博客中来说一下对Mybatis动态代理接口方式的扩展,对于Mybatis动态代理接口不熟悉的朋友,可以参考前一篇博客,或者研读Mybatis源码. 扩展11:动态代理接口扩展 我们知道,真正在My ...

  10. c# BlowFish 高速 对称加密

    BlowFish 高速 对称加密 string key = "this is my key"; BlowFish algo = new BlowFish(key); string ...