1、为什么Core Animation完成时,layer又恢复到原先的状态
因为产生的动画都是假象,实质上并没有对layer进行改变,这里需要说一下图层树里的呈现树,呈现树实际上是模型树的复制,它的属性值决定了当前的显示效果,动画的过程实际上只是修改了呈现树,并没有改变图层的属性,所以在动画结束后会恢复到原来的状态。
2、iOS最实用的13种设计模式(全部有github代码)
① 适配器模式
1. 适配器模式将一个类的接口适配成用户所期待的。一个适配器通常允许因为接口不兼容而不能一起工作的类能够在一起工作,做法是将类自己的接口包裹在一个已存在的类中。(联想一下现实生活中的各类适配,就比较容易理解了)
2. 如何使用适配器模式?
以下情况比较适合使用 Adapter 模式:
- 当你想使用一个已经存在的类,而它的接口不符合你的需求;
- 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作;
- 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口,对象适配器可以适配它的父亲接
适配器模式的优缺点?
优点:降低数据层和视图层(对象)的耦合度,使之使用更加广泛,适应复杂多变的变化。
缺点:降低了可读性,代码量增加,对于不理解这种模式的人来说比较难看懂。github示例代码 ios设计模式之适配器模式
②、策略模式
1. 何为策略模式?
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
2. 如何使用策略模式?
在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
一个系统需要动态地在几种算法中选择一种。
如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
-
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
策略模式的优缺点?
优点:简化操作,提高代码维护性。算法可以自由切换,避免使用多重条件判断,扩展性良好。
缺点:在使用之前就要确定使用某种策略,而不是动态的选择策略。策略类会增多,所有策略类都需要对外暴露。github示例代码ios设计模式之策略模式
③、观察者模式
1. 何为观察者模式?
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。
2. 如何使用观察者模式?
一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
3. 观察者模式的优缺点?
优点:
* 观察者和被观察者是抽象耦合的。
* 建立一套触发机制。
缺点:
* 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
* 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
* 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
4. gitHub示例代码
[iOS设计模式之观察者模式](https://github.com/wupeng4321/ObserverPattern)
④、原型/外观模式
1. 何为原型/外观模式?
原型模式:(Prototype Pattern)用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。
外观模式:(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
2. 如何使用原型/外观模式?
原型模式:
- 当一个系统应该独立于它的产品创建,构成和表示时。
- 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
- 为了避免创建一个与产品类层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
外观模式:
- 客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。
- 定义系统的入口。
- 原型/外观模式的优缺点?
原型模式:
优点:性能提高,逃避构造函数的约束。
缺点:
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易。
- 必须实现 Cloneable 接口。
- 逃避构造函数的约束。
外观模式
优点:减少系统相互依赖、提高灵活性、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
- 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。(以后会在工厂模式代码中体现)
github原型模式示例源码
github外观模式示例代码
⑤、装饰模式
- 何为装饰模式?
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。 - 如何使用装饰模式?
在不想增加很多子类的情况下扩展类。 - 装饰模式的优缺点?
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。 - github示例代码iOS设计模式之装饰模式
模型图如下
⑥、工厂模式
- 何为工厂模式?
- 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
- 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
- 如何使用工厂模式?
- 我们明确地计划不同条件下创建不同实例时。
- 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
- 工厂模式的优缺点?
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:
- 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
- 抽象工厂模式
- 抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
- 在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
- githubiOS设计模式之抽象工厂模式
⑦、桥接模式
- 何为桥接模式?
- 桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
- 这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。
- 如何使用桥接模式?
- 在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
- 实现系统可能有多个角度分类,每一种角度都可能变化。
- 把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
桥接模式的优缺点?
优点 :抽象和实现的分离、优秀的扩展能力、实现细节对客户透明。
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。-
githubiOS设计模式之桥接模式
模型图如下桥接模式原理.png
⑧、代理模式
- 何为代理模式?
- 在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
- 在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
- 如何使用代理模式?
- 在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
- 想在访问一个类时做一些控制。
- 代理模式的优缺点?
优点:
- 职责清晰、高扩展性、智能化。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
- githubiOS设计模式之代理模式
⑨、单例模式
- 何为单例模式?
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- 如何使用单例模式?
当您想控制实例数目,节省系统资源的时候。 - 单例模式的优缺点?
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用比如写文件操作。
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
- githubiOS设计模式之单例模式
(注:代码中的单例是“严格”的单例) - github单例模式优化本地存储
⑩、备忘录模式
- 何为备忘录模式?
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
- 如何使用备忘录模式?
很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。 - 备忘录模式的优缺点?
优点:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
- 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:
- 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
- githubiOS设计模式之备忘录模式
⑪、生成器模式
- 何为送生成器模式?
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 - 如何使用生成器模式?
- 主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
- 一些基本部件不会变,而其组合经常变化的时候。
- 生成器模式的优缺点?
优点:
- 建造者独立,易扩展。
- 便于控制细节风险。
缺点:
- 产品必须有共同点,范围有限制。
- 如内部变化复杂,会有很多的建造类。
- 使用场景
- 需要生成的对象具有复杂的内部结构。
- 需要生成的对象内部属性本身相互依赖。
注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
githubiOS设计模式之制造者模式(参考制造汽车的过程)
-
制造者模式思维导图
制造者模式.png
⑫、命令模式
- 何为命令模式?
命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。 - 主要解决的问题?
在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。 - 如何使用命令模式?
在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。 - 关键代码?
定义三个角色:
- received 真正的命令执行对象
- Command
- invoker 使用命令对象的入口
- 命令模式的优缺点?
优点:降低了系统耦合度,新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。 - 使用场景
认为是命令的地方都可以使用命令模式 - githubiOS设计模式之命令模式(实现View的明亮变化)
⑬、组合模式
- 何为组合模式?
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。 - 主要解决的问题?
它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。 - 如何使用适配器模式?
树枝和叶子实现统一接口,树枝内部组合该接口。 - 关键代码?
树枝内部组合该接口,并且含有内部属性 List,里面放 Component。 - 适配器模式的优缺点?
优点: 高层模块调用简单、节点自由增加。
缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。 - 使用场景?
部分、整体场景,如树形菜单,文件、文件夹的管理。
(注:定义时为具体类。) - githubiOS设计模式之组合模式(模拟文件夹)
总结
- 代码建议有兴趣的同学可以自己敲一遍,便于加深理解。如果觉得github代码还不错请不要吝惜star,每一个star都是我坚持走下去的动力,三克油。
- 每种设计模式都是有特定的使用背景的,在设计之前要多加进入‘上帝模式’,站的更高才能看的更远。
- 本文的13中设计模式只是比较常用的一些设计模式,还有其他的一些设计模式,希望不喜勿喷。
- 如果有什么建议请多多留言,我会一一回复的。
- 如果对设计模式还有浓厚的兴趣,可以看看《iOS21种设计模式》。
设计模式的基本原则,不够清楚的话,可以看这里
3、GCD队列与死锁
死锁发生的条件: 使用 sync函数往当前串行队列中添加任务,会卡主当前的串行队列
1、//log 2134
dispatch_queue_t sq = dispatch_queue_create("111",DISPATCH_QUEUE_SERIAL);
dispatch_async(sq, ^{
NSLog(@"1");
});
NSLog(@"2");
dispatch_sync(sq, ^{
NSLog(@"3");
});
NSLog(@"4");
2、//log 1 3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
[self performSelector:@selector(testAsync) withObject:nil afterDelay:0];
NSLog(@"3");
});
- (void)testAsync {
NSLog(@"2");
}
3、log 1 2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
});
4、死锁
dispatch_queue_t sq = dispatch_queue_create("111",DISPATCH_QUEUE_SERIAL);
dispatch_async(sq, ^{
NSLog(@"1");
dispatch_sync(sq, ^{
NSLog(@"2");
});
});
4、线程安全和锁
https://blog.csdn.net/qq_18505715/article/details/115363376
5、UIButton同时响应手势和点击事件
一、给按钮:添加手势,添加Action
1、如果TestBtn内部没有实现- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
则只会执行手势:tapBtn
2、如果TestBtn内部实现了- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
执行顺序:
-[TestBtn touchesBegan:withEvent:]
-[ViewController tapBtn:]
3、手势与action都能响应的办法:
当手势与button事件都有时
cancelsTouchesInView = YES,会导致button事件失效
cancelsTouchesInView = NO时,button事件才会被调用
@implementation TestView
- (instancetype)init
{
self = [super init];
if (self) {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapSelf:)];
tap.cancelsTouchesInView = NO;
[self addGestureRecognizer:tap];
}
return self;
}
- (void)tapSelf:(UITapGestureRecognizer *)tap {
NSLog(@"%s",__func__);
}
@end
5、HTTP与HTTPS有什么区别?
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
HTTPS的工作原理
我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取,所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。
HTTP与HTTPS的区别-马海祥博客
客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
(3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
(5)Web服务器利用自己的私钥解密出会话密钥。
(6)Web服务器利用会话密钥加密与客户端之间的通信。
四、HTTPS的优点
尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:
(1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
(2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
(3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
(4)谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。
五、HTTPS的缺点
虽然说HTTPS有很大的优势,但其相对来说,还是存在不足之处的:
(1)HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
(2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
(3)SSL证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用。
(4)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。
(5)HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
六、http切换到HTTPS
如果需要将网站从http切换到https到底该如何实现呢?
这里需要将页面中所有的链接,例如js,css,图片等等链接都由http改为https。例如:http://www.baidu.com改为https://www.baidu.com
BTW,这里虽然将http切换为了https,还是建议保留http。所以我们在切换的时候可以做http和https的兼容,具体实现方式是,去掉页面链接中的http头部,这样可以自动匹配http头和https头。例如:将http://www.baidu.com改为//www.baidu.com。然后当用户从http的入口进入访问页面时,页面就是http,如果用户是从https的入口进入访问页面,页面即使https的。
6、iOS图片加载过程优化
iOS 从磁盘加载一张图片,使用 UIImageVIew 显示在屏幕上,需要经过以下步骤:
- 从磁盘拷贝数据到内核缓冲区
- 从内核缓冲区复制数据到用户空间
- 生成 UIImageView,把图像数据赋值给 UIImageView
- 如果图像数据为未解码的 PNG/JPG,解码为位图数据
- CATransaction 捕获到 UIImageView layer 树的变化
- 主线程 Runloop 提交 CATransaction,开始进行图像渲染
- 如果数据没有字节对齐,Core Animation 会再拷贝一份数据,进行字节对齐。
- GPU 处理位图数据,进行渲染。
什么是解码?
以UIImageView为例。当其显示在屏幕上时,需要UIImage作为数据源。
UIImage持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。
当UIImage被赋值给UIImage时(例如imageView.image = image;),图像数据会被解码,变成RGB的颜色数据。
FastImageCache 分别优化了 2、4、6(1) 三个步骤:
- 使用 mmap 内存映射,省去了上述第 2 步数据从内核空间拷贝到用户空间的操作。
- 缓存解码后的位图数据到磁盘,下次从磁盘读取时省去第 4 步解码的操作。
- 生成字节对齐的数据,防止上述第 6(1) 步 CoreAnimation 在渲染时再拷贝一份数据。
二、内存映射
平常我们读取磁盘上的一个文件,上层 API 调用到最后会使用系统方法 read() 读取数据,内核把磁盘数据读入内核缓冲区,用户再从内核缓冲区读取数据复制到用户内存空间,这里有一次内存拷贝的时间消耗,并且读取后整个文件数据就已经存在于用户内存中,占用了进程的内存空间。
FastImageCache 采用了另一种读写文件的方法,就是用 mmap 把文件映射到用户空间里的虚拟内存,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统 VMS 才根据缺页加载的机制从磁盘加载对应的数据块到物理内存,再进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。
三、解码图像
一般我们使用的图像是 JPG/PNG,这些图像数据不是位图,而是是经过编码压缩后的数据,使用它渲染到屏幕之前需要进行解码转成位图数据,这个解码操作是比较耗时的,并且没有 GPU 硬解码,只能通过 CPU,iOS 默认会在主线程对图像进行解码。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage 的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。
FastImageCache 也是在子线程解码图像,不同的是它会缓存解码后的图像到磁盘。因为解码后的图像体积很大,FastImageCache 对这些图像数据做了系列缓存管理,详见下文实现部分。另外缓存的图像体积大也是使用内存映射读取文件的原因,小文件使用内存映射无优势,内存拷贝的量少,拷贝后占用用户内存也不高,文件越大内存映射优势越大。
四、字节对齐
Core Animation 在图像数据非字节对齐的情况下渲染前会先拷贝一份图像数据,官方文档没有对这次拷贝行为作说明,模拟器和 Instrument 里有高亮显示“copied images”的功能,但似乎它有 bug,即使某张图片没有被高亮显示出渲染时被 copy,从调用堆栈上也还是能看到调用了 CA::Render::copy_image 方法:
那什么是字节对齐呢,按我的理解,为了性能,底层渲染图像时不是一个像素一个像素渲染,而是一块一块渲染,数据是一块块地取,就可能遇到这一块连续的内存数据里结尾的数据不是图像的内容,是内存里其他的数据,可能越界读取导致一些奇怪的东西混入,所以在渲染之前 CoreAnimation 要把数据拷贝一份进行处理,确保每一块都是图像数据,对于不足一块的数据置空。大致图示:(pixel 是图像像素数据,data 是内存里其他数据)
块的大小应该是跟 CPU cache line 有关,ARMv7 是 32byte,A9 是 64byte,在 A9 下 CoreAnimation 应该是按 64byte 作为一块数据去读取和渲染,让图像数据对齐 64byte 就可以避免 CoreAnimation 再拷贝一份数据进行修补。FastImageCache 做的字节对齐就是这个事情。
五、实现
FastImageCache 把同个类型和尺寸的图像都放在一个文件里,根据文件偏移取单张图片,类似 web 的 css 雪碧图,这里称为 ImageTable。这样做主要是为了方便统一管理图片缓存,控制缓存的大小,整个 FastImageCache 就是在管理一个个 ImageTable 的数据。整体实现的数据结构如图:
一些补充和说明:
5.1 ImageTable#
一个 ImageFormat 对应一个 ImageTable,ImageFormat 指定了 ImageTable 里图像渲染格式/大小等信息,ImageTable 里的图像数据都由 ImageFormat 规定了统一的尺寸,每张图像大小都是一样的。
一个 ImageTable 一个实体文件,并有另一个文件保存这个 ImageTable 的 meta 信息。
图像使用 entityUUID作为唯一标示符,由用户定义,通常是图像url的hash值。ImageTable Meta的indexMap记录了entityUUID->entryIndex的映射,通过indexMap就可以用图像的entityUUID找到缓存数据在ImageTable对应的位置。
5.2 ImageTableEntry#
ImageTable的实体数据是ImageTableEntry,每个entry有两部分数据,一部分是对齐后的图像数据,另一部分是meta信息,meta保存这张图像的UUID和原图UUID,用于校验图像数据的正确性。
Entry数据是按内存分页大小对齐的,数据大小是内存分页大小的整数倍,这样可以保证虚拟内存缺页加载时使用最少的内存页加载一张图像。
图像数据做了字节对齐处理,CoreAnimation使用时无需再处理拷贝。具体做法是CGBitmapContextCreate创建位图画布时bytesPerRow参数传64倍数。
5.3 Chunk#
ImageTable和实体数据Entry间多了层Chunk,Chunk是逻辑上的数据划分,N个Entry作为一个Chunk,内存映射mmap操作是以chunk为单位的,每一个chunk执行一次mmap把这个chunk的内容映射到虚拟内存。为什么要多一层chunk呢,按我的理解,这样做是为了灵活控制mmap的大小和调用次数,若对整个ImageTable执行mmap,载入虚拟内存的文件过大,若对每个Entry做mmap,调用次数会太多。
5.4 缓存管理#
用户可以定义整个ImageTable里最大缓存的图像数量,在有新图像需要缓存时,如果缓存没有超过限制,会以chunk为单位扩展文件大小,顺序写下去。如果已超过最大缓存限制,会把最少使用的缓存替换掉,实现方法是每次使用图像都会把UUID插入到MRUEntries数组的开头,MRUEntries按最近使用顺序排列了图像UUID,数组里最后一个图像就是最少使用的。被替换掉的图片下次需要再使用时,再走一次取原图—解压—存储的流程。
六、使用
FastImageCache 适合用于 tableView 里缓存每个 cell 上同样规格的图像,优点是能极大加快第一次从磁盘加载这些图像的速度。但它有两个明显的缺点:
- 占空间大。因为缓存了解码后的位图到磁盘,位图是很大的,宽高 100100 的图像在 2x 的高清屏设备下就需要 200200*4byte/pixel = 156KB,这也是为什么 FastImageCache 要大费周章限制缓存大小。
- 接口不友好,需预定义好缓存的图像尺寸。FastImageCache 无法像 SDWebImage 那样无缝接入UIImageView,使用它需要配置 ImageTable,定义好尺寸,手动提供的原图,每种实体图像要定义一个 FICEntity 模型,使逻辑变复杂。
FastImageCache 已经属于极限优化,做图像加载/渲染优化时应该优先考虑一些低代价高回报的优化点,例如 CALayer 代替 UIImageVIew,减少 GPU 计算(去透明/像素对齐),图像子线程解码,避免 Offscreen-Render 等。在其他优化都做到位,图像的渲染还是有性能问题的前提下才考虑使用 FastImageCache 进一步提升首次加载的性能,不过字节对齐的优化倒是可以脱离 FastImageCache 直接运用在项目上,只需要在解码图像时 bitmap 画布的 bytesPerRow 设为 64 的倍数即可。
文章
iOS图片加载速度极限优化—FastImageCache解析