iOS 中 UITableView 的优化

UITableView的性能优化

UITableView 是 iOS 开发中经常会用到的一种控件,复杂的 UITablView 如果不进行优化,会严重影响用户体验。那么我们可以从那些方面如后来对其进行优化呢?

首先 我们使用 UITableView 的大致流程如下 :

  • 获取将要在 UITableView 中显示的数据;
  • 把数据进行处理,封装为相应的 Model,并存入数组
  • UITableView 调用 reloadData 刷新数据
  • 在代理方法中获取或创建 UITableViewCell ,并将数据赋值给它
  • UITableViewCell 根据拿到的数据对其中的 UI 控件进行赋值
  • 在代理方法中计算 UITableViewCell 的行高
  • UITableViewCell 布局子控件

我们的优化就是针对这几个步骤来进行的。

针对 UITableView 的数据处理的优化

需要显示在 UITableView 中的数据一般是通过网络请求从服务器获得的数据,我们需要发起网络请求获得数据后进行处理,以使其能够适合我们要显示的 Cell。然而网络请求和数据处理都是比较耗时的操作,将它们在后台异步线程中进行处理完成后,再来刷新 UITableView 会使得显示更加流畅。

关于 UITableView 的重用机制

UITableView 只会创建在当前屏幕显示或多一点的 UITableViewCell,其它的都是从中取出来重用的。每当 Cell 不再屏幕中显示时,系统将其放入一个集合中,一般称为重用池。当要显示某一位置的 Cell 时,会优先去重用池中取,如果重用池中存在,则直接拿来显示,如果没有才会创建。这极大的减少了内存的开销。

UITableView 最主要的两个回调方法是 tableView:cellForRowAtIndexPath:tableView:heightForRowAtIndexPath: 。在显示 Cell 时,UITableView 显示多次调用 tableView:heightForRowAtIndexPath: 以确定 contentSize 及 Cell 的位置,然后才会调用 tableView:cellForRowAtIndexPath: ,来显示当前屏幕上的 Cell。

UITableView 的显示优化实际上主要就是针对这两个回调方法的优化。

UITableViewCell 高度计算

固定高度的 Cell 的显示自不必说,直接设置固定的高度即可:

1
self.tableView.rowHeight	=	HEIGHT_OF_CELL;

而对于动态高度的 Cell 来说,则需要在 tableView:heightForRowAtIndexPath: 方法中给出相应的高度。动态高度的计算依赖于要显示在 Cell 中的数据内容,我们可以直接在处理数据时就进行高度的计算,而不是在 tableView:heightForRowAtIndexPath: 方法中进行计算,这也会节省一些时间。

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
DataModel *model = self.dataSourceList[indexPath.row];
returen model.heightOfCell;
}

如果不提前进行 Cell 高度的计算,将已经计算好的高度进行缓存,避免重复进行计算也是一个可以优化的地方。

UITableViewCell 的内容布局

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

对于布局内容不固定的 Cell 来说,根据要显示的数据内容进行内容的布局即可,如果要进行布局的控件很多,则会极大的消耗系统的资源。

而实际上我们 Cell 添加控件时,实质上都是系统调用底层的接口进行绘制,因此对于控件较多的动态 Cell 来说,我们可以直接进行绘制,提高效率。

直接在 drawRect 方法中进行绘制:

1
2
3
- (void)drawRect {
//
}

或将绘制好的内容作为图片返回后直接显示 :

1
2
3
4
5
6
7
8
9
- (UIImage *)draw {
// 内容绘制
CGContextRef context = UIGraphicsGetCurrentContext();
···
// 需要注意的是,绘制是比较耗时的操作,放入异步后台线程进行任务比较好
UIImage *contentImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return contentImage;
}

异步处理耗时操作

获取数据并对其进新处理等耗时操作,应该放入后台线程异步处理,等到数据完成处理后再通知主线程刷新 UI。

另外,UIKit 和 CoreAnimation 相关的操作必须在主线程中进行,其它诸如图像的绘制等则应该在后台线程异步执行。

在 Cell 上添加系统控件时,实质上系统都需要调用底层的接口进行绘制,当 Cell 上有大量的控件需要添加时,对资源的开销也会很大,直接绘制将会大大地提高效率。在遇到需要显示复杂的 UITableViewCell 时,使用自定义绘制会更好。

避免频繁的创建对象

对象的创建会产生内存分配、属性调整等操作,频繁大量的创建对象会消耗大量的资源和时间,应尽量避免频繁大量的创建对象。应该使用轻量对象来代替重量对象,或将对象创建后进行缓存。

减少对象的属性赋值操作

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

简化视图结构

GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。

减少离屏渲染

下面的情况或操作会引发离屏渲染:

  • 为图层设置遮罩(layer.mask)
  • 将图层的 layer.masksToBounds / view.clipsToBounds 属性设置为 true
  • 将图层 layer.allowsGroupOpacity 属性设置为 YES 和 layer.opacity 小于1.0
  • 为图层设置阴影(layer.shadow *)。
  • 为图层设置 layer.shouldRasterize = true
  • 具有 layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing 的图层
  • 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
  • 使用 CGContext 在 drawRect : 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现

按需加载

iOS 与设计模式

iOS 与 设计模式

设计模式 (Design Pattern) 是一套被反复使用、多数人知晓、经过分类的、代码设计经验的总结。

我们使用设计模式的目的是为了提高代码可重用性、让代码更容易被理解、保证代码可靠性,使人们可以更加简单方便地复用成功的设计和体系结构。

设计原则

开闭原则

开闭原则就是说模块应该对扩展开放,而对修改关闭,即模块应该在尽量不修改原来代码的情况下进行扩展。

任何软件都需要面临一个很重要的问题,就是需求会随着时间的推移而发生变化。当软件系统需要面对新的需求时,应该尽量保证系统的设计框架是稳定的。如果一个软件符合开闭原则,那么就可以非常方便的对系统进行扩展并无需修改现有代码,使得系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。

为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,这就实现了在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

里氏代换原则

里氏代换原则严格表述是这样的 :如果每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 代换 o2 时,程序的 P 的行为没有变化,那么类型 S 就是 类型 T 的子类型。即所有引用父类的地方必须能透明地使用其子类的对象。

里氏代换原则是实现开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

里氏代换原则需要注意的问题 :

  • 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。为了保证系统的可扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
  • 应该尽量把父类设计成抽象类或接口,让子类继承父类或实现接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例。

在依照里氏代换原则实现的软件中将一个父类对象替换成它的子类对象,程序将不会产生任何错误和异常,反之则不成立。

依赖倒置原则

依赖倒置原则,即抽象应该不依赖于细节,细节应该依赖于抽象,换言之,要针对接口编程,而不是针对实现编程。

依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类去做这些事情。为了确保该原则的应用,一个具体类应该只实现接口或抽象类中声明过的方法,而不要给出多余的方法。

在实现依赖倒置原则时,我们需要针对抽象层进行编程,而将具体类的对象通过依赖注入的方法注入到其它对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式包括 :构造注入、设值注入和接口注入。构造注入是通过构造函数来传入具体类的对象,设值注入是通过 setter 方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。

在大多数情况下,以上三种设计原则一同出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则时手段。

接口隔离原则

接口隔离原则,使用多个专门的接口,而不是使用单一的接口,即客户端不应该依赖那些它不需要的接口。每一个接口应该承担相对独立的角色,不干不该干的事情,该干的事情都要干。

这里的接口往往有两种不同的含义,一种是指一个类型所具有的方特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言的具体的接口定义,有严格的定义和结构。

  • 当把接口理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可称为角色隔离原则。
  • 如果把接口理解成狭义的特定语言的接口,那么接口隔离原则是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏,应该为客户端提供尽可能小的单独的接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的指责单一,需要将大接口中的方法根据其指责不同分别放在不同的小接口中,确保每个接口使用起来都较为方便。接口应该尽量细化,同时接口中的方法应该尽量少。

单一职责原则

单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小,即一个类只负责一个功能领域中的相应职责,或者说 :就一个类而言,应该只有一个引起它变化的原因。

单一职责原则是实现高内聚、低耦合的知道方针,它是最简单但又最难运用的原则。

最少知识法则

也叫迪米特法则,即一个软件实体应当尽可能少地与其它实体发生相互作用。

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。

iOS 中的设计模式

单例模式

单例模式是一种常见的设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。

我们为什么需要单例模式呢?这是因为,对于系统中的某些类来说,只有一个实例很重要。比如如果不使用机制对窗口对象唯一化,将弹出多个窗口,如果这些窗口现实的内容完全一致,则是重复对象,浪费资源。如果这些窗口现实的内容不一致,则意味着某一瞬间系统有多个状态,与实际不符。

如何保证一个类只有一个实例且这个实例易于被访问呢?一般的解决方法是让类自身负责保存它的唯一实例。这个类可以保证没有其它实例被创建,并且它可以提供一个访问该实例的方法。

单例模式的优点

  • 单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  • 因为类控制了实例化过程,所以类可以灵活更改实例化过程。

单例模式的缺点

  • 虽然数量很少,但是每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。
  • 可能的开发混淆。
  • 不能解决删除单个对象的问题。

单例模式的代码实现

Objective-C :

1
2
3
4
5
6
7
8
+ (Singleton *)sharedInstance {
static dispatch_once_t onceToken;
static Singleton *sharedInstance = nil;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

Swift :

1
2
3
4
class Singleton {
static let sharedInstance = Singleton()
private init(){}
}

代理模式

代理模式也是一种常见的设计模式,为其它对象提供一种代理以控制对这个对象的访问。

代理模式的优点 :
  • 真实的角色就是实现实际的业务逻辑,不用关心其它非本职责的事物,通过后期的代理完成一件事物,附带的结果就是编程简洁清晰。
  • 代理对象在客户端和目标对象之间起到中介作用
  • 高扩展性

iOS 中的代理模式通过 @protocol 方式实现,用来传递事件或值。

1
2
3
4
5
6
@protocol ObjectDelegate
@required
- (void)requiredImplementDelegateMethod;
@optional
- (void)optionalImplementDelegateMethod;
@end

一般使用 weak 关键字来修饰代理属性,这是为了便面形成循环引用。

观察者模式

观察者模式也是 iOS 中常见的设计模式,观察者模式定义了一种一对多的依赖关系,一个或多个观察者对象同时监听某一个对象,当被观察者对象状态发生变化时,会通知所有观察者对象,至于观察者对象如何响应这个通知,则要看具体的实现。

在 iOS 中,观察者模式的实现具体表现为 :通知机制 (Notification) 和 KVO 机制 (Kev-Value Observing)

通知机制

被观察者达到某些触发条件后,发送通知给它的观察者对象。

注册通知接收者 :

1
2
3
4
[[NSNotificationCenter defaultCenter] addObserver:self									
selector:@selector(respondNotificationMethod)
name:@"NotificationName"
object:nil];

发布通知 :

1
2
3
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName"
object:nil
userInfo:@{}];

在 iOS 9 之后,你不需要在观察者的销毁方法中注销观察者,但是在之前的 iOS 版本中,你必须在销毁方法中注销观察者。

KVO 机制

被观察的对象的属性变化时,发送通知给它的观察者对象。

KVO 机制的观察者对象观察的是对象的属性的变化,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调部分。

添加监听者 :

1
2
3
4
[self.aProperty addObserver:self
forKeyPath:@"propertyName",
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOld
context:nil];

接收到属性变化通知时的回调 :

1
2
3
4
5
6
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// execute code
}

iOS 通过 Runtime 实现 KVO 机制 :

假设我们要观察对象 Computername 属性的变化,运行时 KVO 机制会动态的创建一个名为 NSKVONotifying_Computer 的新类,该类继承自 Computer 对象的本类,且 KVO 机制会重写 NSKVONotifying_Computername 属性的 setter 方法。这个重写的 setter 方法负责在调用 Computersetter 方法的之前和之后,通知所有观察者 name 属性的更改情况。

在这个过程中,Computerisa 指针被 KVO 机制修改为指向 NSKVONotifying_Computer 类,来实现当前类属性值改变的监听。

KVO 机制的键值观察通知依赖于 NSObjectwillChangeValueForKey:didChangeValueForKey: 两个方法。

工厂模式

工厂模式是常用的实例化对象模式,是用工厂方式代替 new 操作的一种模式。

简单工厂

简单工厂模式,又称静态工厂方法模式,是一种创建型模式,是由一个工厂对象决定创建出哪一种产品类的实例。

简单工厂的优缺点
  • 优点 :简单工厂模式能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。明确区分了各自的指责和权利,有利于整个软件体系结构的优化。
  • 缺点 :工厂类集中了所有实例的创建逻辑,包含了过度的判断条件,维护起来不方便。
简单工厂的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef NS_ENUM(NSInteger, InstanceType) {
InstanceType_001 = 0,
InstanceType_002 = 1,
... = ...
InstanceType_n = n-1
}

+ (id)getInstanceWithParameter:(InstanceType)someType {
Switch (someType) {
case : InstanceType_001
reture [self initInstance_001];
break;
...
case : InstanceType_n
reture [self initInstance_n];
break;
default :
break;
}
}

- (id)initInstance_001 {
...
}

...

- (id)initInstance_n {
...
}

工厂方法

工厂方法模式,又称多态性工厂模式。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂的角色,仅负责给出工厂子类必须实现的接口,而不负责哪一个产品类应当被实例化这种细节。

工厂方法模式就是简单工厂模式的衍生,解决了许多简单工厂模式的问题。它在基类中建立一个抽象方法,子类可以通过改写这个方法来改变创建对象的具体过程。工厂方法模式让子类来决定如何创建对象,来达到封装的目的。

工厂方法的优点
  • 优点 :
    • 子类提供挂钩。基类为工厂方法提供缺省实现,子类可以重写新的实现,也可以集成父类的实现,增加了灵活性。
    • 屏蔽产品类。产品类的实现如何变化,调用者都不需要关心,只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不会发生变化。
    • 典型的解耦框架。高层模块只需要知道产品的抽象类,其它的实现类都不需要关心。
    • 多态性,客户代码可以做到与特定应用无关,适用于任何实体类。

抽象工厂模式

抽象工厂模式提供一个接口,用于创建一个对象家族,而无需指定具体类。工厂方法只涉及到创建一个对象的情况,有时候我们需要一族对象。

抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。根据里氏替换原则,任何接受父类型的地方,都应当能够接受子类型。因此,实际上系统所需要的,仅仅是类型与这些抽象产品角色相同的一些实例,而不是这些抽象产品的实例。换言之,也就是这些抽象产品的具体子类的实例。工厂类负责创建抽象产品的具体子类的实例。

抽象工厂的优点/缺点
  • 优点:
    • 抽象工厂模式隔离了具体类的生产,使得客户并不需要知道什么被创建。
    • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
    • 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
  • 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。

事件传递与响应链探索


iOS App 使用响应者对象( Responder)来接收和处理事件。一个响应者对象是 UIResponder 类的实例,它常见的子类包括 UIViewUIViewControllerUIApplication。响应者对象接收原始事件数据,并且必须处理该事件或将它转发给另一个响应者对象。

当你的 App 接收到一个事件时,UIKit 自动将它发送给最适合的响应者对象,即第一响应者对象(First Responder)。未处理的事件从响应者对象被发送到正在活动的响应链中的响应者对象(响应者链是应用程序的响应者对象的动态配置,在应用程序中没有单个响应者链)。

UIKit 为事件如何从一个响应者对象传递到另一个响应者对象预定义了规则,但是你可以通过覆盖响应者对象中的属性来改变这些规则。


响应者对象 (Responder Object)

Responder 对象是 UIResponder 类的实例,构成了 UIKit 应用程序事件处理的主干。许多重要对象也是响应者,包括 UIApplication 对象,UIViewController 对象和所有 UIView 对象(包括 UIWindow)。当事件发生时,UIKit 将它们分派给你的应用程序的响应者对象进行处理。
UIResponder 对象能处理的事件,包括触摸事件,动作事件,远程控制事件和新闻事件。

为了处理特定类型的事件,响应者必须覆盖相应的方法。例如,为了处理触摸事件,响应者实现了 touchesBegan:with:touchesMoved:with:touchesEnded:with:touchesCancelled:with:方法。在触摸事件发生时,响应者使用 UIKit 提供的事件信息来跟踪触摸的变化,并适当地更新应用的界面。

除了处理事件之外,UIKit响应者还负责将未处理事件转发到应用程序的其他部分。如果给定的响应者不处理事件,则将该事件转发给响应者链中的下一个响应者对象。 UIKit 动态地管理响应者链,使用预定义的规则来确定接下来哪个对象应该接收事件。

响应者处理 UIEvent 对象,但也可以通过输入视图接受自定义输入。系统的键盘是输入视图最明显的例子。当用户点击屏幕上的 UITextFieldUITextView 对象时,视图成为第一响应者并显示其输入视图,这是系统键盘。同样,你可以创建自定义输入视图,并在其它响应者激活时显示它们。要将自定义输入视图与响应者相关联,请将该视图分配给响应者的 inputView 属性。

NOTE

与加速度计、陀螺仪和磁力计相关的运动事件不遵循响应者链。Core Motion 会直接将这些事件发送给你指定的对象。


第一响应者对象 (The First Responder)

UIKit 为各种类型的事件指定第一响应者对象并在事件发生时首先发送到第一响应者对象。

  • Touch events

    第一响应者对象是触摸发生的视图

  • Press events

    第一响应者对象是获得焦点的响应者对象

  • Shake-motion events

    第一响应者对象是你或 UIKit 指定的对象

  • Remote-control events

    第一响应者对象是你或 UIKit 指定的对象

  • Editing menu messages

    第一响应者对象是你或 UIKit 指定的对象

控件使用动作消息 (Action message) 与其关联的目标对象 (Target Object) 直接进行通信。当用户与控件进行交互时,控件调用其目标对象的动作方法 (Action Method),向目标对象(Target object)发送一个动作消息。动作消息不是一个事件,但它们仍可利用响应链。当控件的目标对象为 nil 时,UIKit 从目标对象开始,遍历响应者链,直到寻找到实现了相应方法的对象。

如果视图中有添加手势,那么手势会在视图接收之前接收触摸和按下事件。如果视图中的所有手势都无法处理这个事件,则将事件传递给视图进行处理。如果视图也不能处理,则 UIKit 会将事件传递给响应者链。


基于视图的点击测试 (View-Based Hit-Testing)

UIKit 使用基于视图的点击测试来确定触摸事件到底发生在哪里。UIKit 将触摸位置与处在视图层级中的视图的 bounds 进行比较。hitTest:withEvent: 方法遍历视图层级,寻找包含指定触摸的层级最深的子视图。 然后这个视图就成为第一响应者对象。

当用户触摸屏幕进行交互时,系统检测到手指触摸操作,并将触摸以 UIEvent 的方式加入 UIApplication 的事件队列中。UIApplication 从事件队列中取出最新的触摸事件进行分发传递到 UIWindow 进行处理。而 UIWindow 会通过 hitTest:withEvent: 方法寻找触点所在的视图,这个过程称之为 Hit-Test。

UIKit 将每个触摸永久绑定到包含它的视图对象。UIKit 在触摸第一次发生时创建 UITouch 对象,并只在触摸结束时释放它。当触摸位置或其它参数改变时,UIKit 使用新的信息来更新 UITouch 对象。唯一不改变的属性就是包含它的视图,即使触摸位置已经移出原始的视图。

Hit-Test 从顶级视图开始调用 pointInside:withEvevt: 方法判断触摸点是否在当前视图内,如果返回为 NO ,则 hitTest:withEvent: 方法返回 nil;如果返回 YES ,则向当前视图的所有子视图发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到最底层视图,直到有子视图返回非空对象或者全部子视图遍历完毕。

NOTE

如果一个触摸的位置超出了视图的边界,hitTest:withEvent: 方法就会忽略这个视图及其所有子视图。


响应者链 (Resoponder Chain)

当系统通过 Hit-Test 找到触摸点所在的视图,但是这个视图并没有或者无法正常处理此次触摸事件,这个时候,系统便会通过响应者链寻找下一个响应者,以对此次触摸事件进行响应。

https://github.com/apple272487813/codeLei.github.io/blob/master/images/iOS_Responder_Chain.jpg?raw=true

如果一个 View 有一个视图控制器,它的下一个响应者就是这个视图控制器,然后才是它的父视图,如果一直到根视图都没能处理这个事件,事件会传递到 UIWindow ,若在 UIWindow 中也没有处理,则会传递给 UIApplication ,它是一个响应者链的终点,它的下一个响应者指向 nil ,以劫数整个循环。


改变响应者链 (Altering the Responder Chain)

你可以通过覆盖你的响应者对象中的 nextResponder 属性来改变响应者链。当你这样做时,下一个响应者就是你返回的对象。

很多 UIKit 类已经覆盖了这个属性 :

  • UIView 对象。如果视图是视图控制器的根视图,那它的 nextResponder 是视图控制器,否则是视图的父视图。
  • UIViewController 对象。
    • 如果视图控制器的视图是一个窗口的根视图,nextResponder 是窗口对象。
    • 如果一个视图是被其它视图控制器推出的,nextResponder 是推出它的视图控制器。
  • UIWindow 对象。窗口的 nextResponderUIApplication 对象。
  • UIApplication 对象。只有在应用代理是 UIResponder 的实例而不是视图、视图控制器或者应用对象自身时,UIApplicationnextResponder 是应用程序代理。

Objective-C Runtime

Objective-C Runtime

动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化,类型的检查是在运行时做的,优点为方便阅读,清晰明了,缺点为不方便调试。

这里有三个名词容易被混淆:

Dynamic Programming Language,即动态语言或动态编程语言

Dynamically Typed Language,即动态类型语言

Statically Typed Language,即静态类型语言

所谓的动态类型语言,指的是类型的检查是在运行时做的;与此相对,静态类型语言的类型判断是在运行前判断(如编译阶段),C#、Java 等就是静态类型语言,静态类型的语言为了达到多态会采取一些类型鉴别手段,如接口、继承,而动态类型的语言却不需要,所以一般动态语言都会采用 dynamic typing,常出现于脚本语言中。

Objective-C 就是一门动态语言,它有一个运行时系统来执行编译的代码。这个运行时系统就是 Objc Runtime,它是一个由 C 和汇编语言编写的库。

Runtime 库的主要工作是封装和找出最终执行的代码。

  • 封装 :在 Objc Runtime 库中,对象可以用 C 中的结构体表示,而方法可以用 C 中的函数来实现,另外再加上一些额外的特性。这些结构体和函数被 Runtime 函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
  • 找出方法的最终执行代码:当程序执行 [object doSomething]时,会向消息接收者发送一条消息,Runtime 会根据消息接收者是否能响应该消息而做出不同的反应。

Objective-C Runtime 目前有两个版本:Modern RuntimeLegacy RuntimeModer Runtime覆盖了64位的 Mac OS X AppsiOS AppsLegacy Runtime是早期用来给32位的 Mac OS X Apps用的。

Objective-C Runtime 中的类与对象

类与对象的基本数据结构

Class

Objective-C 中的类是由 Class 类型来表示的,实际上是一个指向 objc_class 结构体的指针

1
typedef struct objc_class	*Class;	

它的结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct	objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

这些字段的意义如下:

  • isaObjective-C 中所有类自身也是对象,这个对象的 Class 里也有一个 isa 指针,它指向 metaClass 元类。
  • super_class :指向该类的父类,如果该类已经是最顶层的根类,则为 NULL
  • name :类名。
  • version :类的版本信息,默认为0。我们可以使用这个字段来提供类的版本信息,这对于对象的序列化非常有用,它可以让我们识别出不同类定义版本中实例变量布局的改变。
  • info :类信息,供运行期使用的一些位标识。
  • instance_size :类的实例变量的大小。
  • ivars :类的成员变量链表。
  • methodLists :方法定义的链表。
  • cache :用于缓存最近使用的方法。当一个接收者对象接收到一个消息时,它会根据 isa 指针去查找能够响应这消息的对象。在实际情况里,这个对象只有一部分方法是常用的,很多方法其实很少用或根本用不上。这种情况下,如果每次消息来时,都是在 methodLists 链表中遍历一遍,性能势必很差。更好的做法是,在我们每次调用过一个方法后,就将这个方法缓存到 cache 列表中,下次调用就会优先去 cache 中查找。
  • protocols :协议链表。

当我们向一个 Objective-C 对象发送消息时,Runtime 会根据实例对象的 isa 指针找到这个实例对象所属的类,然后 Runtime 会在类的方法列表及其父类的方法列表中寻找与消息对应的 selector 指向的方法,找到后即运行这个方法。

当创建一个特定类的实例对象时,分配的内存会包含一个 objc_object 数据结构,然后是类的实例变量的数据。NSObject 类的 allocallocWithZone: 方法使用函数 class_createInstance 来创建 objc_object 数据结构。

1
2
3
4
5
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
}

typedef struct objc_object *id;

常见的 id 是一个 objc_object 结构类型的指针。

objc_cache

objc_class 中的 cache 字段用于缓存调用过的方法,它是一个指向 objc_cache 结构体的指针。

1
2
3
4
5
struct	objc_cache {
unsigned int mask OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buchets[1] OBJC2_UNAVAILABLE;
}

该结构体的字段描述如下:

  • mask :一个整数,指定分配的缓存 bucket 的总数。在方法查找过程中,Runtime 使用这个字段来确定开始线性查找数组的索引位置。指向方法的 selector 指针与该字段做一个 AND 位操作。
  • occupied :一个整数,指定实际占用的缓存 bucket 的总数。
  • buckets : 指向 Method 数据结构指针的数组。这个数组可能包含不超过 mask+1 个元素。如果指针为 NULL,表明这个缓存 bucket 没有被占用,另外被占用的 bucket 可能是不连续的。

元类(Meta Class)

所有的类自身也是一个对象,我们也可以向这个对象发送消息。

当我们向一个对象发送消息时,Runtime 会在这个对象所属的类的方法列表中查找方法;而向一个类发送消息时,会在这个类的 meta-class 的方法列表中查找。

meta-class 存储着一个类的所有类方法。每个类都会有个一个单独的 meta-class ,因为每个类的类方法基本不可能完全相同。

meta-classisa 指针指向基类的 meta-class ,以此作为它的所属类。可以理解为,任何 NSObject 继承体系下的 meta-class 都使用 NSObjectmeta-class 作为自己的所属类,而基类的 meta-classisa 指针则指向它自己。

对于 NSObject 继承体系来说,其实例方法对体系中的所有实例、类和 meta-class 都是有效的;而类方法对于体系内的所有类和 meta-class 都是有效的。

类与对象操作函数

Runtime 提供了大量函数来操作类与对象。类的操作方法大部分以 class_ 为前缀,对象的操作方法大部分是以 objc_object_ 为前缀。

类名

1
2
// 获取类的类名
const char * class_getName (Class cls);

对于 class_getName 函数来说,如果传入的 clsnil,则返回一个字字符串。

父类(super_class)和元类(meta-class)

1
2
3
4
// 获取类的父类
Class class_getSuperClass (Class cls);
// 判断给定的类是否是元类
BOOL class_isMetaClass (Class cls);
  • class_getSuperClass 函数,当clsnilcls 为根类时,返回 nil
  • class_isMetaClass 函数,如果 cls 是元类,返回 YES。如果传入的 clsnil,则返回 NO

实例变量大小(instance_size)

1
2
// 获取实例大小
size_t class_getInstanceSize (Class cls);

成员变量(ivar)及属性

objc_class 中,所有的成员变量、属性的信息都是放在链表 ivars 中。ivars 是一个数组,数组中的每个元素是指向 ivar 的指针。

  • 成员变量操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取类中指定名称实例成员变量的信息
    Ivar class_getInstanceVariable (Class cls, const char *name);
    // 获取类成员变量的信息
    Ivar class_getClassVariable (Class cls, const char *name);
    // 添加成员变量
    BOOL class_addIvar (Class cls, const char *name, size_t size, uint8_t alignment, const char *types);
    // 获取整个成员变量列表
    Ivar * class_copyIvarList (Class cls, unsigned int *outCount);
    • class_getInstanceVariable 函数,它返回一个指向包含 name 指定的成员变量信息的 objc_ivar 结构体的指针 Ivar
    • class_getClassVariable 函数,目前没有找到关于 Objective-C 中类变量的信息,一般认为 Objective-C 不支持类变量。
    • Objective-C 不支持向已经存在的类中添加实例变量,但如果通过 Runtime 来创建一个类的话,就可以使用 class_addIvar 函数了。需要注意的是,这个函数只能在 objc_allocateClassPairobjc_registerClassPair 之间调用。另外这个类也不能是元类。
    • class_copyIvarList ,返回一个包含成员变量信息的数组,数组中的每个元素是指向该成员变量信息的 objc_ivar 结构体的指针。这个数组不包含在父类中声明的变量。outCount 指针返回数组的大小。需要注意的是,我们必须使用 free() 来释放这个数组。
  • 属性操作函数

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取指定的属性
    objc_property_t class_getProperty (Class cls, const char *name);
    // 获取属性列表
    objc_property_t * class_copyPropertyList (Class cls, unsigned int *outCount);
    // 为类添加属性
    BOOL class_addProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);
    // 替换类的属性
    void class_replaceProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);
  • Mac OS X 系统中,Runtime 提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理 strong/weak 引用。

    1
    2
    3
    4
    const	uint8_t * class_getIvarLayout	(Class cls);
    void class_setIvarLayout (Class cls, const uint8_t *layout);
    const uint_8 * class_getWeakIvarLayout (Class cls);
    void class_setWeakIvarLayout (Class cls, const uint8_t *layout);

    通常情况下,我们不需要去主动调用这些方法,在调用 objc_registerClassPair 时,会生成合理的布局。

方法(methodLists)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 添加方法
BOOL class_addMethod (Class cls, SEL name, IMP imp, const char *types);
// 获取实例方法
Method class_getInstanceMethod (Class cls, SEL name);
// 获取类方法
Method class_getClassMethod (Class cls, SEL name);
// 获取所有方法的数组
Method * class_copyMethodList (Class cls, unsigned int *outCount);
// 替代方法的实现
IMP class_replaceMethod (Class cls, SEL name, IMP imp, const char *types);
// 返回方法的具体实现
IMP class_getMethodImplementation (Class cls, SEL name);
IMP class_getMethodImplementation_stret (Class cls, SEL name);
// 类实例是否响应指定的 selector
BOOL class_respondsToSelector (Class cls, SEL sel);
  • class_addMethod 的实现会覆盖父类的方法实现,但不会取代本类中已经存在的实现,如果本类中包含一个同名的实现,则函数会返回 NO。如果要修改已经存在的实现,可以使用 method_setImplementation。一个 Objectice-C 方法是一个简单的 C 函数,它至少包含两个参数 self_cmd,所以我们实现函数至少需要两个参数。

    1
    2
    3
    void myMethodIMP (id self, SEL _cmd) {
    // implementation
    }

    与成员变量不同的是,我们可以动态的为类添加方法,不管这个类是否已经存在。

    参数 types 是一个描述传递给方法的参数类型的字符数组。

  • class_getInstanceMethodclass_getClassMethod 函数,与 class_copyMethodList 不同的是,这两个函数都会去搜索父类的实现。

  • class_copyMethodList 函数,返回包含所有实例方法的数组,如果需要获取类方法,则可使用 class_copyMethodList(object_getClass(Class cls), &count) (一个类的实例方法是定义在元类中的)。该列表不包含父类实现的方法。outCount 参数返回方法的个数。在获取到列表后,我们需要使用 free() 来释放它。

  • class_replaceMethod 函数,该函数的行为可以分为两种 :如果类中不存在参数 name 指定的方法,则类似于 class_addMethod 函数一样会添加方法;如果类中已经存在 name 指定的方法,则类似于 method_setImplementation 一样替代原方法的实现。

  • class_getMethodImplementation 函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比 method_getImplementation(Class cls, const char * name) 更快。返回的函数指针可能是一个指向 Runtime 内部的函数,而不一定是方法的实际实现。

  • class_respondsToSelector 函数,我们经常使用 NSObject 类的 respondsToSelector:instanceRespondToSelector: 方法来达到同样的目的。

协议 (objc_protocol_list)

协议相关的操作包含以下函数::

1
2
3
4
5
6
// 添加协议
BOOL class_addProtocol (Class cls, Protocol *protocol);
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol (Class cls, Protocol *protocol);
// 返回类实现的协议列表
Protocol *class_copyProtocolList (Class cls, unsigned int *outCout);

版本(version)

版本相关的操作函数:

1
2
3
4
// 获取版本号
int class_getVersion (Class cls);
// 设置版本号
void class_setVersion (Class cls, int version)

动态创建类和对象

Runtime 经常被用到的功能就是在运行时创建类和对象。

动态创建类

动态创建类涉及到一下几个函数

1
2
3
4
5
6
// 创建一个新类和元类
Class objc_allocateClassPair (Class superclass, const char *name, size_t extraBytes);
// 销毁一个类及其相关的类
void objc_disposeClassPair (Class cls);
// 在应用中注册由 objc_allocateClassPair 创建的类
void objc_registerClassPair (Class cls);
  • objc_allocateClassPair 函数 :如果我们要创建一个根类,则指定 superClassNilextraBytes 通常指定为 0 ,该参数是分配给类和元类对象尾部的索引 ivars 的字节数。

为了创建一个新类,我们需要调用 objc_allocateClassPair ,然后使用诸如 class_addMethod , class_addIvar 等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用 objc_registerClassPair 函数来注册类,之后这个类就可以在程序中使用了。

实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。

  • objc_disposeClassPair 函数用于销毁一个类,值得注意的是,如果程序运行中还存在类或其子类的实例,则不能对针对类调用该方法。

动态创建对象

动态创建对象的函数如下:

1
2
3
4
5
6
// 创建类实例
id class_createInstance (Class cls, size_t extraBytes);
// 在指定位置创建类实例
id objc_constructInstance (Class cls, void *bytes);
// 销毁类实例
void * objc_destructInstance (id obj);
  • class_createInstance 函数 :创建实例时,会在默认的内存区域为类分配内存。extraBytes 表示分配的额外字节数。这些额外的字节数可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。

    调用 class_createInstance 的效果和 +alloc 方法类似。

  • objc_constructInstance 函数 :在指定的位置(bytes)创建类实例。

  • objc_destructInstance 函数 :销毁一个类的实例,但不会释放并移除任何与其先关的引用。

实例操作函数

实例操作函数主要是针对创建的实例对象的一系列操作函数。

针对整个对象进行操作的函数

1
2
3
4
// 返回指定对象的一份拷贝
id object_copy (id obj, size_t size);
// 释放指定对象占用的内存
id object_dispose (id obj);

针对对象实例变量进行操作的函数

1
2
3
4
5
6
7
8
9
10
// 修改类实例的实例变量的值
Ivar object_setInstanceVariable (id obj, const char *name, void *value);
// 获取对象实例变量的值
Ivar object_getInstanceVariable (id obj, const char *name, void **outValue);
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars (id obj);
// 返回对象中实例变量的值
id object_getIvar (id obj, Ivar ivar);
// 设置对象中实例变量的值
void object_setIvar (id obj, Ivar ivar, id value);

如果实例变量的 Ivar 已经知道,那么调用 objc_getIvar 会比 objc_getInstanceVariable 函数快。相同情况下,object_setIvar 也比 object_setInstanceVariable 快。

针对对象的类进行操作的函数

1
2
3
4
5
6
// 返回给定对象的类名
const char * object_getClassName (id obj);
// 返回对象的类
Class object_getClass (id obj);
// 设置对象的类
Class object_setClass (id obj, Class cls);

获取类定义

Objective-C Runtime 会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用 objc_addClass 来注册它们。

获取类定义的相关函数

1
2
3
4
5
6
7
8
9
10
// 获取以注册的类定义的列表
int objc_getClassList (Class *buffer, int bufferCount);
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList (unsigned int *outCount);
// 返回指定类的类定义
Class objc_lookUpClass (const char *name);
Class objc_getClass (const char *name);
Class objc_getRequiredClass (const char *name);
// 返回指定类的元类
Class objc_getMetaClass (const char *name);
  • objc_getClassList 函数 :获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自 NSObject 体系的,所以在这些类上调用方法时,都应该先检测一下这个方法是否在这个类中实现。
  • 获取类定义的方法有三个 :objc_lookUpClass , objc_getClassobjc_getRequiredClass。如果类在运行时未注册,则 objc_lookUpClass 会返回 nil,而 objc_getClass 会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回 nil。而 objc_getRequiredClass 函数的操作与 objc_getClass 相同,只不过没有找到类,就会杀死进程。
  • objc_getMetaClass 函数 :如果指定的类没有注册,则该函数会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回 nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义。不管它是否有效。

Objective-C Runtime 中的成员变量与属性

类型编码(Type Encoding)

作为对 Runtime 的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的 selector 关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用 @encod 编译器指令来获取它。当给定一个类型时,@encode 会返回这个类型的字符串编码。

成员变量、属性

Runtime 中关于成员变量和属性的相关数据结构并不多,只有三个。

基础数据类型

Ivar

Ivar 是表示实例变量的类型,其实际上是一个指向 objc_ivar 结构体的指针 :

1
2
3
4
5
6
7
8
9
10
11
typedef struct	objc_ivar	*Ivar;

struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 变量名
char *ivar_type OBJC2_UNAVAILABLE; // 变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节

#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
objc_property_t

objc_property_t 是声明属性的类型,是一个指向 objc_property 结构体的指针 :

1
typedef	struct	objc_property	*objc_property_t;
objc_property_attribute_t

objc_property_attribute_t 定义了属性的特性,它是一个结构体:

1
2
3
4
typedef	struct {
const char *name; // 特姓名
const char *value; // 特性值
}

关联对象(Associated Object)

关联对象是Runtime 中一个非常实用的特性。关联对象类似于成员变量,不过是在运行时添加的。我们通常会把变量(Ivar)放在类声明的头文件中,或者放在类实现的 @implementation 后面。但这有一个缺点,我们不能在分类中添加成员变量。Objective-C 针对这一问题,提出了一个解决方案:即关联对象(Associated Object)。

我们可以把关联对象想象成一个Objective-C 对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void 指针。我们还需要指定一个内存管理策略,告诉Runtime 如何管理这个对象的内存,这个内存管理策略可以由一下指定:

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的是 OBJC_ASSOCIATION_ASSIGN

,则宿主释放时,关联对象不会被释放;而如果指定的是 retain 或者是 copy ,则宿主释放时,关联对象会被释放。

将一个对象连接到其它对象所需的就是下面两行代码:

1
2
static char aKey;
objc_setAssociatedObject(self, &aKey, anObject, OBJC_ASSOCIATION_RETAIN);

在这种情况下,self 对象将获取一个新的关联对象 anObject ,且内存管理策略是自动 retian 管理对象,当self 对象释放时,会自动 release 关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,在这种情况下,先前的关联对象会被妥善的处理掉,并且新的对象会使用它的内存。

1
2
// 获取指定key的关联对象
id anObject = objc_getAssociatedObject(self, &aKey);

使用 objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用 objc_setAssociatedObject 函数将key指定的关联对象设置为nil

成员变量、属性的操作方法

成员变量

成员变量的操作函数包括 :

1
2
3
4
5
6
// 获取成员变量名
const char * ivar_getName (Ivar v);
// 获取成员变量类型编码
const char * ivar_getTypeEncoding (Ivar v);
// 获取成员变量的偏移量
ptrdiff_t ivar_getOffset (Ivar v);
  • ivar_getOffset 函数,对于 id 或其它对象类型的实例变量,可以调用 object_getIvarobject_setIvar 来直接访问成员变量,而不使用偏移量。
关联对象

关联对象的操作函数包括 :

1
2
3
4
5
6
// 设置关联对象
void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy);
// 获取关联对象
id objc_getAssociatedObject (id object, const void *key);
// 移除关联对象
void objc_removeAssociatedObjects (id object);
属性

属性操作相关函数包括 :

1
2
3
4
5
6
7
8
// 获取属性名
const char * property_getName (objc_property_t property);
// 获取属性特性描述字符串
const char * property_getAttributes (objc_property_t property);
// 获取属性中指定的特性
char * property_copyAttributeValue (objc_property_t property, const char *attributeName);
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList (objc_property_t property, unsigned int *outCount);
  • property_copyAttributeValue 函数,返回的 char* 在使用完后需要调用 free() 释放。
  • property_copuAttributeList 函数,返回值在使用完后需要调用 free() 释放。

Objective-C Runtime 中的方法与消息

基础数据类型

SEL

SEL 又叫做选择器,是表示一个方法的 selector 的指针,定义如下:

1
typedef struct objc_selector *SEL;

方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会根据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int 类型的地址),这个标识就是 SEL

两个类之间,不管它们是不是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的 SEL 就是一样的。每一个方法都对应着一个 SEL。所以在 Objective-C 的同一个类(及类的继承体系)中,不能存在两个同名的方法,即使参数类型不同也不行。因为相同的方法只能对应一个 SEL,这导致 Objectice-C 在处理相同方法名且参数个数相同但类型不同的方法方面能力很差。

当然不同的类可以拥有相同的 selector ,这个没有问题。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中根据 selector 去寻找自己对应的 IMP

工程中的所有的 SEL 组成了一个 Set 集合,Set 特点就是唯一,因此 SEL 也是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的 SEL 就行了,SEL 实际上就是根据方法名 hash 化了的一个字符串,而对于字符串的比较仅仅需要比较它们的地址就可以了。

本质上,SEL 只是一个指向方法的指针(准确地说,只是一个根据方法名 hash 化了的 KEY 值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

我们可以在运行时添加新的 selector ,也可以在运行时获取已经存在的 selector ,我们可以通过下面三种方式来获取 SEL :

  • sel_registerName 函数
  • Objective-C 编译器提供的 @selector()
  • NSSelectorFromString() 方法

IMP

IMP 实际上是一个函数指针,指向函数实现的首地址。

1
id (*IMP)(id, SEL, ...)

这个函数使用当前 CPU 架构实现的标准 C 调用约定。第一个参数是指向 self (如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)的指针,第二个参数是 selector ,接下来是方法的实际参数列表。

通过取得 IMP ,我们可以跳过 Runtime 的消息传递机制,直接执行 IMP 指向的函数实现,这样就省去了 Runtime 消息传递过程中所做的一系列查找操作。

Method

Method 用于表示类定义中的方法

1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE; //
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

方法操作相关函数

Runtime 提供了一系列的方法来处理与方法相关的操作,包括方法本身及 SEL

方法

方法的相关操作函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
  • method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementationmethod_getName更快。
  • method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))
  • method_getReturnType函数,类型字符串会被拷贝到dst中。
  • method_setImplementation函数,注意该函数返回值是方法之前的实现。

方法选择器

选择器的相关操作函数包括 :

1
2
3
4
5
6
7
8
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  • sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。

方法调用流程

Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message] 转化为一个消息调用的函数,即 objc_msgSend 。这个函数将消息接收者和方法名作为其基础参数。

1
objc_msgSend(receiver, selector)

如果消息中还有其它参数,则该方法的形式如下

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

这个函数完成了动态绑定的所有事情:

  • 首先它找到 selector 对应的方法实现。因为同一个方法在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到确切的实现。
  • 它调用方法实现,并将接收者对象及方法的所有参数传递给它。
  • 最后,它将实现返回的值作为它自己的返回值。

消息的关键在于结构体 objc_class ,这个结构体中有两个字段是我们在分发消息时关注的:

  • 指向父类的指针
  • 一个类的方法分发表,即 methodLists

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承体系。

当消息发送给一个对象时,objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法列表里查找方法的 selector 。如果没有找到 selector ,则通过 objc_class 结构体中指向父类的指针找到其父类,并在父类的方法列表里寻找方法的 selector 。依此,会一直沿着类的的继承体系到达 NSObject 类。一旦定位到 selector ,函数就会获取到实现的入口点,并传入相应的参数来执行方法的具体实现。如果没有定位到 selector ,则会走消息转发流程。

为了加速消息的处理,运行时系统缓存使用过的 selector 及对应的方法的地址。

隐藏参数

objc_msgSend 有两个隐藏参数 :

  • 消息接受对象
  • 方法的 selector

这两个参数为方法的实现提供了调用者的信息,之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明,而是在编译期被插入实现代码的。

虽然这些参数没有声明,但是我们仍然能在代码中引用它们。我们可以用 self 来引用接收者对象,使用 _cmd 来引用选择器。

获取方法地址

Runtime 中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。

我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。

NSObject 类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。

我们通过以下代码来看看methodForSelector:的使用:

1
2
3
4
5
6
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++)
setter(targetList[i], @selector(setFilled:), YES);

这里需要注意的就是函数指针的前两个参数必须是idSEL

当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

消息转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

1
2
3
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,并由 NSObjectdoesNotRecognizeSelector 方法抛出 ‘unrecognized selector sent to instance’ 错误信息。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

下面我们详细讨论一下这三个步骤。

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}

不过这种方案更多的是为了实现@dynamic属性。

备用接收者

如果在上一步无法处理消息,则 Runtime会继续调以下方法 :

1
- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  1. 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  2. 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}

NSObjectforwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

消息转发与多重继承

回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。

不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:isKindOfClass:只能用于继承体系,而不能用于转发链。如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector])
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

Objective-C Runtime Method Swizzling

Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

这种情况下,我们就可以使用Method Swizzling,如在代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。

上面的例子很好地展示了使用method swizzling来向一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:

Swizzling应该总是在+load中执行

Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没有给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

选择器、方法与实现

Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。第一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。

理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

调用 _cmd

我们回过头来看看前面新的方法的实现代码:

1
2
3
4
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

乍看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。

Objective-C Runtime 中的协议与分类

Objective-C 中的分类允许我们通过给一个类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问类中的代码就可以做到。

Objective-C 中的协议是普遍存在的接口定义方式,即在一个类中通过@protocol定义接口,在另外类中实现接口,这种接口定义方式也称为“delegation”模式,@protocol声明了可以被其他任何方法类实现的方法,协议仅仅是定义一个接口,而由其他的类去负责实现。

基础数据类型

Category

Category 是表示一个指向分类的结构体的指针。

1
2
3
4
5
6
7
8
9
typedef struct objc_category *Category;

struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

Protocol

Protocol 的定义如下

1
typedef struct objc_object Protocol;

可以看到,Protocol 其实就是一个对象结构体。

操作函数

Runtime并没有在<objc/runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息。

而对于Protocol,runtime提供了一系列函数来对其进行操作,这些函数包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto );
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
  • objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil
  • objc_copyProtocolList函数,获取到的数组需要使用free()来释放
  • objc_allocateProtocol函数,如果同名的协议已经存在,则返回nil
  • objc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议

需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescriptionprotocol_addProtocolprotocol_addProperty往协议中添加方法等。

Objective-C Runtime 补充知识

super

Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super

如何使用super我们都知道。现在的问题是,它是如何工作的呢?

首先我们需要知道的是superself不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:

1
struct objc_super { id receiver; Class superClass; };

这个结构体有两个成员:

  1. receiver:即消息的实际接收者
  2. superClass:指针当前类的父类

当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是当前对象,与self相同;superClass指向当前类的父类。

接下来,发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数,其声明如下:

1
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

该函数第一个参数即为前面生成的objc_super结构体,第二个参数是方法的selector。该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找viewDidLoadselector,找到后以objc->receiver去调用这个selector,而此时的操作流程就是如下方式了

1
objc_msgSend(objc_super->receiver, @selector(viewDidLoad))

由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:

1
objc_msgSend(self, @selector(viewDidLoad))

库相关操作

库相关的操作主要是用于获取由系统提供的库相关的信息,主要包含以下函数:

1
2
3
4
5
6
// 获取所有加载的Objective-C框架和动态库的名称
const char ** objc_copyImageNames ( unsigned int *outCount );
// 获取指定类所在动态库
const char * class_getImageName ( Class cls );
// 获取指定库或框架中所有类的类名
const char ** objc_copyClassNamesForImage ( const char *image, unsigned int *outCount )

通过这几个函数,我们可以了解到某个类所有的库,以及某个库中包含哪些类。

块操作

我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:

1
2
3
4
5
6
// 创建一个指针函数的指针,该函数调用时会调用特定的block
IMP imp_implementationWithBlock ( id block );
// 返回与IMP(使用imp_implementationWithBlock创建的)相关的block
id imp_getBlock ( IMP anImp );
// 解除block与IMP(使用imp_implementationWithBlock创建的)的关联关系,并释放block的拷贝
BOOL imp_removeBlock ( IMP anImp );
  • imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP

弱引用操作

1
2
3
4
// 加载弱引用指针引用的对象并返回
id objc_loadWeak ( id *location );
// 存储__weak变量的新值
id objc_storeWeak ( id *location, id obj );
  • objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retainautoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。

    objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。

宏定义

Runtime 中,还定义了一些宏定义供我们使用,有些值我们会经常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我们做一个简单的介绍。

布尔值

1
2
#define YES  (BOOL)1
#define NO (BOOL)0

这两个宏定义定义了表示布尔值的常量,需要注意的是YES的值是1,而不是非0值。

空值

1
2
#define nil  __DARWIN_NULL
#define Nil __DARWIN_NULL

其中nil用于空的实例对象,而Nil用于空类对象。

分发函数原型

1
#define OBJC_OLD_DISPATCH_PROTOTYPES  1

该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换

Objective-C根类

1
#define OBJC_ROOT_CLASS

如果我们定义了一个Objective-C根类,则编译器会报错,指明我们定义的类没有指定一个基类。这种情况下,我们就可以使用这个宏定义来避过这个编译错误。该宏在iOS 7.0后可用。

其实在NSObject的声明中,我们就可以看到这个宏的身影,如下所示:

1
2
3
4
5
6
7
8
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0)

OBJC_ROOT_CLASS
OBJC_EXPORT

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

我们可以参考这种方式来定义我们自己的根类。

局部变量存储时长

1
#define NS_VALID_UNTIL_END_OF_SCOPE

该宏表明存储在某些局部变量中的值在优化时不应该被编译器强制释放。

我们将局部变量标记为id类型或者是指向ObjC对象类型的指针,以便存储在这些局部变量中的值在优化时不会被编译器强制释放。相反,这些值会在变量再次被赋值之前或者局部变量的作用域结束之前都会被保存。

关联对象行为

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

Objective-C Runtime 实际应用

动态交换两个方法的实现

当第三方框架或系统原生方法功能不能满足我们的需求的时候,可以通过交换方法实现在保持原有方法功能的基础上,添加额外的功能。

1
2
3
4
5
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(originalMethodName));
Method targetMethod = class_getClassMethod(self, @selector(targetMethodName));
method_exchangeImplementations(originalMethod, targetMethod);
}

动态添加属性

属性赋值的本质就是让属性与一个对象产生关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface NSObject (AssociatedProperty)

@property NSString *propertyName;

@end

@implementation NSObject (AssociatedProperty)

- (void)setPropertyName:(NSString *)stringValue {
objc_setAssociatedObject(self, @"propertyName", stringValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)propertyName {
return objc_getAssociatedObject(self, @"propertyName");
}

@end

实现字典转模型的动态转换

利用 Runtime 遍历模型中所有属性,根据模型的属性名,去字典中查找 key,去除对应的值,给模型的属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 最简单实现,不考虑字典中含有数组或对象的情况
+ (instancetype)modelWithDict:(NSDictionary *)dict {
id objc = [[self alloc] init];
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *key = [ivarName substringFromIndex:1];
id value = dict[key];
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}

动态添加方法

如果一个类的方法很多,加载类到内存的时候比较耗费资源,使用动态给类添加方法的方式可以解决。

1
2
3
4
5
6
7
8
9
void methodAddWhenRun(id self, SEL _cmd, id argument ···) {}

+ (BOOL)resolveInstanceMethod:(SEL)self {
if (sel == NSSelectorFromString(@"methodAddWhenRun")) {
class_addMethod(self, sel, (IMP)methodAddWhenRun, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}

动态变量控制

1
2
3
4
5
6
7
8
9
10
11
12
// 假设要修改 `_propertyName`
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *varName = ivar_getName(ivar);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_propertyName"]) {
object_setIvar([self class], var, (id)newValue);
break;
}
}

实现 NSCoding 的自动归解档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)encodeWithCoder:(NSCoder *)encoder {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([TargetClass class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodObject:value forKey:key];
}
free(ivarList);
}

- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvaarList([TargetClass class], &count);
for (int index = 0; index < count; index++) {
Ivar ivar = ivarList[index];
const char *name = ivar_getName:(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList);
}
return self;
}

转载整理自 南风子的技术博客

参考 百度百科 动态语言

参考 白水ln的简书

内存管理理解与分析

iOS 内存管理

iOS 中的程序内存结构


在 iOS 程序中,内存可以粗略的分为五个区域:

Name Descroption
由操作系统自动分配和释放,常用来存放函数的参数值、局部变量的值等。优点是快速高效,缺点是有限制,数据
一般由程序员分配和释放,常用来存储对象
全局区 用来存储已经初始化的全局变量和静态变量,程序结束时才会被释放回收
常量区 用来存储常量的区域,程序结束时才会被释放回收
代码段 用来存放程序的执行代码,直到程序结束才会释放回首

在 iOS程序中,只有堆区中存放的数据是需要手动释放回收的,其它区域存储的数据的释放和回收都由系统进行管理。当一个 iOS 程序启动后,它的全局区、常量区和代码区就已经确定了。

  • 栈区 (stack) 是由编译器自动分配和释放,用来存放函数的参数值、局部变量等。栈是系统数据结构,对应进程/线程是唯一的。优点是快速高效,缺点是有限制,数据不灵活。

  • 堆区 (heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能由操作系统回收。优点是灵活方便,数据适应面广泛,但是效率有一定降低。

  • 全局区/静态区 (static) 存放全局变量和静态变量,初始话的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在另一块区域,程序结束后由系统自动释放。

  • 文字常量区,用来存储常量字符串,程序结束后由系统释放。

  • 代码区,存放函数的二进制代码

iOS 中的内存管理


因为 iOS 程序的内存分配中,只有堆区是有程序员进行管理的,所以 iOS 的内存管理大致上就是可以认为是堆区内存的管理。

在 Objective-C 中,使用引用计数来确定一个对象所占有的内存空间是否应该被回收。它的工作原理可以描述为:

Objective-C 中的每一个对象都有一个类型为 unsigned long 的 retainCount 的属性,这个属性由拥有它的对象进行维护。当我们新创建出这个对象的一个实例时,这个对象实例的 retainCount 值为1,每当一个新的引用指向对象,对象的 retainCount 值就会增加1,每当这个对象实例的引用减少一个,retainCount 的值就减少1。当着对象实例的 retainCount 的值为0时,代表这个对象实例没有被引用,系统会自动将这个对象实例的内存空间回收并同时调用这个实例对象的 dealloc 方法。

需要注意的几个问题:

  • 常量是没有引用计数的
  • 使用对象实例的属性值进行赋值,不会引用这个对象
  • 释放对象实例时会调用 dealloc 方法,如果没有调用则会造成内存泄漏
  • 对引用计数为1的对象实例发送 release 消息时,系统不会再对其进行 retainCount - 1 的操作。

MRC 和 ARC

使用对象实例的引用计数来进行 iOS的内存管理,分为两种方式:

  • MRC :手动引用计数,由程序员手动的管理对象实例的引用计数
  • ARC :自动引用计数,是基于 MRC 的,系统自动的管理对象实例的引用计数

实际上在 iOS 5 之后,Apple 就开始推荐使用 ARC 来进行 iOS 程序的内存管理工作,目前 MRC 已经非常少见。

ARC 中,编译器会在编译时在代码中插入合适的 retain 和 release 语句。

ARC 中的修饰符

ARC 中有四种修饰符

Name Description
__strong 强引用,默认值,持有所指向对象的所有权
__weak 弱引用,不持有所指向对象的所有权,所指向的对象销毁后,引用会自动置为 nil
__autoreleasing 自动释放对象的引用,一般用来传递参数
__unsafe_unretained 为兼容 MRC 出现的修饰符,可看成 MRC 下的 weak
属性的内存管理

常见的属性修饰符

Name Description
assign 直接赋值,一般用来修饰基本数据类型。修饰 Objc 对会造成野指针
retain 保留新值,再释放旧值,再设置新值
copy 拷贝新值,再释放旧值,再设置新值
weak ARC 新引入,可代替 assign,自动置 nil
strong ARC 新引入,可代替 retain
block 的内存管理

使用@property声明一个 block 时,使用 copy 来修饰。

block 会对内部使用的对象进行强应用,在使用时可能会造成循环引用,可通过添加弱引用标记来解决:

1
__weak typeof(self) weakSelf	=	self;

Autorelease & AutoreleasePool

在实际的情境中,经常会遇到不知道一个对象实例再什么时候不再使用,因而造成不知道应该何时才能将其释放的情况。Objective-C 中提供了 autorelease 方法来解决这个问题。

当给一个对象实例发送 autorelease 消息时,它会被添加到合适的自动释放池中,当自动释放池销毁时,会给自动释放池中的所有对象实例发送 release 消息。

autorelease 不会改变对象的引用计数。

创建自动释放池的两种方法:

1
2
3
4
5
6
7
// 1
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];
// 2
@autoreleasepool {

}

值得注意的是自动释放池实质上只是在销毁时给其中的所有对象发送了 release 消息,并不保证对象一定会被销毁。

内存管理问题和解决方案

僵尸对象和野指针

僵尸对象是指内存已经被回收的对象,而野指针是指向僵尸对象的指针。

向野指针发送消息会导致程序崩溃,就是经典的 : EXC_BAD_ACCESS 错误。

所以为了避免产生僵尸对象和野指针,在对象释放后,应将其指针置为 nil。

循环引用

当对象之间相互拥有彼此的强引用,形成闭环引用时,就称为循环引用。

循环引用会造成程序内存消耗过高、程序闪退等问题。

以下几种情况可能会造成循环引用:

  • 由于父类指针可以指向子类对象,当父类对象和子类对象相互引用时,就造成了循环引用

  • 作为对象属性的 block 中强引用了对象,造成循环引用,解决方法如下:

    1
    2
    3
    4
    5
    __weak typeof(self) weakSelf = self;
    self.testObject.testCircleBlock = ^{
    __strong typeof (weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
    };
  • 使用 strong 修饰符修饰代理属性,造成循环引用

  • 作为属性的 NSTimer,造成循环引用

循环中对象占用内存大

常见于循环次数较大,循环体生成的对象占用内存较大的情景。

可通过在循环中创建自己的 autoreleasePool 或及时释放占用内存大的 临时变量来解决。

RunLoop 探索与分析

RunLoop 基础

什么是RunLoop?

RunLoop 是一种让线程能随时处理事件但并不退出的机制,是一个用来调度工作的和协调接受的事件的循环。

iOS系统中,提供了 NSRunLoop 和 CFRunLoopRef 两个对象来实现 RunLoop。RunLoop 对象管理其需要处理的事件和消息,并提供了一个入口函数来执行事件循环的逻辑。线程执行了这个函数之后,就会一直处于这个函数内部的循环中,直到这个循环结束,函数返回。

CFRunLoopRef 是在 CoreFoundation 框架内的,提供了纯C函数的API,所有这些API都是线程安全的。

NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的API,但这些API不是线程安全的。

RunLoop与线程的关系

线程和 RunLoop 之间是一一对应的,其关系保存在一个全局的 Dictionary 中。线程刚创建是并没有 RunLoop,如果你不主动获取,那它一直不会有。RunLoop 的创建是在第一次获取时发生的,RunLoop 的销毁是在线程结束时发生的。

你只能在一个线程内部获取它的 RunLoop(主线程除外)。

RunLoop的对外接口

在 CoreFoundation 中关于 RunLoop 的类有以下几个:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTImerRef
  • CFRunLoopObserverRef

一个 RunLoop 包含若干个 Mode ,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode。如果需要切换 Mode ,只能退出 RunLoop,再重新指定一个 Mode 进入。这样做的目的是为了分隔开不同组的 Source/Timer/Observer,使其不能互相影响。

CFRunLoopSourceRef 是事件产生的地方,有两个版本 Source0 和 Source1:

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runLoop) 来唤醒 RunLoop ,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其它线程相互发送消息,它能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef 是基于时间的触发器,他和 NSTimer 是 toll-free bridge 的,可以混用。包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 中时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化。

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActiviry) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
};

Source/Timer/Observer 被统称为 Mode Item,一个 item 可以被同时加入多个 Mode。但是一个item被重复加入一个 Mode 不会产生效果。如果一个 Mode 中没有一个 item,则 RunLoop会直接退出,不进入循环。

RunLoop 的Mode

CFRunLoopMode 和 CFRunLoop 的结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
}

struce __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}

一个 Mode 可以将自己标记为 “Common” 属性(通过将其 ModeName 添加到 RunLoop 的 “CommonModes”中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItmes 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有 Mode 里。

CFRunLoop对外暴露的管理 Mode 的接口只有两个:

1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName);

CFRunLoopModeRef 暴露的管理 Mode Item 接口有:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef runloop, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef runloop, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef runloop, CFRunLoopTimerRef timer, CFStringRef modeName);
CFRunLoopRemoveSource(CFRunLoopRef runloop, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef runloop, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef runloop, CFRunLoopTimerRef timer, CFStringRef modeName);

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 时但 RunLoop内部没有对应的 mode 时,RunLoop 会自动的帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop ,其内部的 mode 只能添加不能删除。

Apple公开提供的 Mode 只有两个:kCFRunLoopDefaultMode(NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时 Apple 还提供了一个操作 Common 标记的字符串 : kCFRunLoopModes(NSDefaultRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时要注意区分这个字符串和其它 Mode Name。

RunLoop 的内部逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// DefaultMode 启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
}

// 用指定的 Mode 启动,允许设置 RunLoop 超时时间
int CFRunLoopRunInMode(CFStringRef ModeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopSpecific(CFRunLoopGetCurrent(), modeName , seconds ,returnAfterSourceHandle)
}

// RunLoop 的实现
int CFRunLoopRunSpecific(runloop, modeName ,seconds , stopeAfterHandle) {
// 根据 ModeName 找到对应 Mode
CFRunLoopModeRef currentMode = __CFRunLoopFinMode(runloop, modeName, false);
// 如果 Mode 里没有 source/timer/observer,直接返回
if (__CFRunLoopModeIsEmpty(currentMode)) {
return;
}

// 通知 Observer , RunLoop 即将进入 loop
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 进入 loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandle) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
// 通知 Observers : RunLoop 即将触发 Timer 回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 通知 Observers : RunLoop 即将触发 Source0 (非port) 回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行被加入的 block
__CFRunLoopDoBlocks(runloop, currentMode);
// RunLoop 触发 Source0(非port) 回调
sourceHandledThisLoop = __CFRunLoopDoSource0(runloop, currentMode, stopAfterHandele);
// 执行被加入的 block
__CFRunLoopDoBlocks(runloop, currentMode);

// 如果有 Source1 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg);
if (hasMsg)
{
goto handle_msg;
}
}

// 通知 Observers : RunLoop 的线程即将结束进入休眠(Sleep)
if (!sourceHandleThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

// 调用 mach_msg 等待接受 mach_port 的消息。线程进入休眠,直到被下面一个事件唤醒
// 1.一个基于 port 的 Source 的事件
// 2.一个 Timer 时间到了
// 3.RunLoop 自身的超时时间到了
// 4.被其它调用者手动唤醒了
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port);
}

// 通知 Observers : RunLoop 的线程刚刚被唤醒了
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

// 收到消息,处理消息
handle_msg;

// 如果一个 Timer 的时间到了,触发这个 Timer 的回调
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time());
} else if (msg_is_dispatch) {
// 如果有 dispatch 到 main_queue 的 block ,执行 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_(msg);
} else {
// 如果有一个 Source1 发出事件了, 处理这个事件
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandleThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandleThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
// 执行加入到 Loop 的 block
__CFRunLoopDoBlocks(runloop, currentMode);

if (sourceHandleThisLoop && stopAfterHandle) {
// 进入 loop 时参数说处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeOut) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimeOut;
} else if (__CFRunLoopIsStoped(runloop)) {
// 被外部调用者强行停止了
retVal = kCFRunLoopRunStoped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
// Source/Timer/Observer 一个都没有了
retVal = kCFRunLoopRunFinished;
}

// 如果没超时,mode 里没空, loop 也没有被停止,那就继续 loop
} while (retVal == 0)
}
}

RunLoop有什么用?

App 启动后 RunLoop 的状态 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}

common mode items = {

// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}

// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}

// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}

// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},

modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },

CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},

CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}

系统注册了五个默认的 Mode:

1.kCFRunLoopDefaultMode :App 的默认 Mode,通常主线程就是在这个 Mode 下运行的。

2.UITrackingRunLoopMode : 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其它 Mode 影响。

3.UIInitializationRunLoopMode :在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。

4.GSEventReceiveRunLoopMode :接受系统事件的内部 Mode,通常用不到。

5.kCFRunLoopCommonModes :这是一个占位的 Mode,没有实际作用。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中下断点时,通常能在调用栈中看到这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

AutoreleasePool

App 启动后,Apple 在主线程的 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,优先级最高,保证创建释放池发生在其它所有回调之前。

第二个 Observer 监视了两个事件:BeforeWaiting(准备进入休眠)时调用 _objc_autoreleasePoolPop() 和 _objc_auroreleasePoolPush() 释放旧的池并创建新池。Exit(即将退出Loop)时调用 _objc_autoreleasePoolPop() 来释放自动释放池,优先级最低,保证其释放发生在其它所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。

事件响应

Apple 注册了一个 Source1 (基于 mach port) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallBack()。

当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpingBoard 只接收按键、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后 Apple 注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转、发送给 UIWindow等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancle 事件都是在这个回调中完成的。

手势识别

当 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

Apple 注册了一个 Observer 检测 BeforeWaiting 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObsever(), 其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

PerformSelector

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

关于GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

1
2
3
4
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
  • CFSocket 是最底层的接口,只负责 socket 通信。
  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

RunLoop 怎么用?

AFNetWorking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

1
2
3
4
5
6
7
8
9
10
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

转载整理自 : ibireme 的博客 深入理解RunLoop

多线程编程基础

iOS 多线程

进程与线程

进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,是正在运行的程序的实例(An instance of a computer program that is being executed)。

进程的概念主要有两点:

  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(Text Region)、数据区域(Data Region)和堆栈(Stack Region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区存储着活动过程调用的指令和本地变量。
  • 进程是一个执行中的程序。

线程

线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分配的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其它线程共享进程所拥有的全部资源。

进程与线程的关系与区别

进程是资源分配的基本单位。所有与该进程有关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或正在使用它们。另外,进程也是抢占处理机的基本单位,它拥有一个完整的虚拟地址空间。当进程发生调度时,不同的进程拥有不同的虚拟地址空间,而同一进程内的不同线程共享同一地址空间。

与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。线程只由相关堆栈、寄存器和线程控制表TCB组成。寄存器可被用来存储线程内的局部变量,但不能存储其它线程的相关变量。

通常在一个进程中可以包含多个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更叫高效的提高系统内多个程序之间并发执行的程度。

线程和进程区别:

  • 地址空间和其它资源:进程之间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  • 通信:进程间通信主要方式有管道、系统IPC(消息队列、信号、共享存储)、套接字(Socket)。而线程间可以直接读写进程数据段来进行通信(需要进程同步和互斥手段的辅助,以保证数据的异质性)
  • 调度和切换:线程上下文切换比进程快的多。
  • 在多线程OS中,进程不是一个可执行的实体

线程安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其它线程不能进行访问直到该线程读取完,其它线程才可以使用。

iOS中的多线程

目前在iOS中有四种多线程解决方案:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

Pthreads

POSIX线程,简称Pthreads,是线程的POSIX标准。该标准定义了创建和操作线程的一整套API。在类Unix操作系统中,都使用Pthreads作为操作系统的线程。

NSThread

NSThread是经常Apple封装的完全面向对象的。你可以直观方便的操控线程对象,但是生命周期需要手动管理。

GCD

GCD是Apple为多核的并行运算提出的的解决方案,能够自动合理地利用更多的CPU内核,并自动管理线程的声明周期。GCD使用C进行编写,并使用了Block。

####任务

即代码所要完成的操作。

任务的执行方式有两种:同步执行和异步执行。

同步执行操作,它会阻塞当前线程并等待任务执行完毕,然后当前线程才会继续往下运行。

异步执行操作,则不会阻塞当前线程,当前线程会直接往下执行。

队列

用于存放要执行的任务。

串行队列中的任务,GCD会遵循FIFO原则来执行,串行队列的同步执行任务,会在当前线程一个一个执行,而异步执行任务,则会在它线程中一个一个执行。

并行队列中的任务执行顺序则要复杂一点,任务会根据同步或异步有不同的执行方式。并行队列中的同步执行任务会在当前线程中一个一个执行,而异步执行则会开很多线程一起执行。

如何创建队列?
  • 主队列:特殊的串行队列,主要用于刷新UI,任何需要刷新UI的工作都必须在主队列中执行。

dispatch_queue_t mainQueue = ispatch_get_main_queue();

  • 自己创建的队列:

    创建串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("CustomSerialQueue",DISPATCH_QUEUE_SERIAL);

​ 创建并行队列

dispatch_queue_t concurrentQueue = dispatch_queue_create("CustomConcurrentQueue",DISPATCH_QUEUE_CONCURRENT);

  • 全局队列:这是一个并行队列,并行任务一般都加入到这个队列。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)

如何创建任务?

  • 创建同步任务

    dispatch_sync(,^{// execute code));

  • 创建异步任务

    dispatch_async(,^{// execute code});

队列组

队列组可以将很多队列添加到一个组里,当组中的所有任务都执行完了,队列组将会通知给用户。

dispatch_group_enterdispatch_group_leave 分别表示一个任务追加到队列组和一个任务执行完毕离开了队列组。

只有当group中未执行完毕的任务数量为0时,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIOITY_DEFAULT,0);
dispatch_group_async(group, queue, ^{
// execute code
});
// 向group中添加在主队列中执行的任务
dispatch_group_async(group, dispatch_get_main_queue(),^{
// execute code
});
// 向group中追加任务
dispatch_group_enter(group);
dispatch_async(queue,^{
// execute code
dispatch_group_leave(group);
});

// 直到前面加入到group中的所有任务都执行完毕后,才会执行
dispatch_group_notify(group,dispatch_get_main_queue(),^{
// execute code
});
OR
// 直到前面加入到group中的所有任务都执行完成后,才会继续往下执行
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

GCD的信号量机制

并发队列可以分配多个线程,同时处理不同的任务,虽然提升了效率,但是多线程的并发是通过时间片轮转的方法实现的,线程的创建、销毁、上下文切换等会消耗资源。适当的并发可以提高效率,但是无节制的并发,则会抢占CPU资源,造成性能下降。此外,提交给并发队列的任务中,有些任务内部会有全局的锁,会导致线程休眠、阻塞,一旦这类任务过多,并发队列还需要创建新的线程来执行其它任务,会造成线程数量的增加。

因此控制并发队列中的线程数量就成了不能忽视的问题。

GCD并发线程数量控制

GCD中的信号量(dispatch_semaphore)是一个整形值,有初始计数值,可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1。如果信号量为0,线程会被阻塞,直到信号量大于0,才会继续执行。

使用信号量机制可以实现线程的同步,也可以控制最大并发数。

使用GCD信号量机制实现并发线程数量控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t	workConcurrentQueue	=	dispatch_queue_create(@"WORK_CONCURRENT_QUEUE",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t workSerialQueue = dispatch_queue_create(@"WORK_SERIAL_QUEUE",DISPATCH_QUEUE_SERIAL);
int maxConcurrent = 10;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(workSerialQueue, ^{
// 使信号量-1,当信号量为0时就一直等待,即阻塞所在线程
// 这里使信号量 maxConcurrent-1,表示最大并发数量已被占用一个位置
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(workConcurrentQueue, ^{
// 发送一个信号,让信号量+1
// 这里使信号量 maxConcurrent+1,表示任务被执行,释放了最大并发数量中的一个位置
dispatch_semaphore_signal(semaphore);
});
});
}
使用GCD信号量机制实现线程同步

有时候我们会遇到需要异步执行一些耗时任务,并在这些任务完成后进行一些额外的操作,相当于将异步执行任务转化为同步执行任务。

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_async(queue, ^{
// 执行耗时任务
···
// 任务完成后使信号量+1,被阻塞的线程继续执行
dispatch_semaphore_signal(semaphore);
});
// 如果信号量为0,则会阻塞当前线程,直到信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
使用GCD信号量机制实现线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其它变量的值也和预期的是一样的,就是线程安全的。

1
2
3
4
5
6
7
8
9
dispatch_semaphore_t semaphoreLock	=	dispatch_semaphore_create(1);
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
{
// 需要保证安全的执行代码
}
// 相当于解锁
dispatch_semaphore_singal(semaphoreLock);

GCD的一些使用场景

使用GCD实现延迟执行
1
2
3
4
5
dispatch_queue_t queue	=	dispatch_ger_gloabl_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double delay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW), (int64_t)(delay*NSEC_PER_SEC)), queue, ^{
// execute code
});
使用GCD实现单例模式
1
2
3
4
5
6
7
8
static id _instance;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// initial code
});
retur _instance;
}
任务同步

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。

dispatch_barrier_async 函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_queue_t queue = dispatch_queue_create("com.codelei.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
// Task 1
});
dispatch_async(queue, ^{
// Task 2
});

dispatch_barrier_async(queue, ^{
// 追加的任务 Task 3
});

dispatch_async(queue, ^{
// 追加的任务 Task 4
});

dispatch_async(queue, ^{
// 追加的任务 Task 5
});

直到 Task 1 和 Task 2 执行完成后,才会执行使用 dispatch_barrier_asynce 追加的任务 Task 3,然后在 Task 3 执行完成后,并行队列会正常执行。

NSOperation & NSOperationQueue

NSOperation是Apple对GCD的封装,完全面向对象。NSOperation和NSOperationQueue分别对应GCD的任务和队列。

添加任务

NSOperation只是一个抽象类,并不能直接封装任务。它有两个子类NSInvocationOperation和NSBlockOperation用来完成封装任务的操作。创建一个Operation后,需要调用start方法来启动任务,它默认在当前队列同步执行。如果需要在执行途中取消执行一个任务,调用cancel方法即可。

  • NSInvocationOperation

    1
    2
    NSInvocationOperation *invocationOperation = [[NSInvocation alloc] initWithTarget:self selector:@selector(executeMethod)];
    invocationOperation start];
  • NSBlockOperation

    1
    2
    3
    4
    5
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOperation start];

    上面提到,NSInvocationOperation和NSBlockOperation创建的任务默认在当前线程执行,但是NSBlockOperation可以通过addExecutionBlock:方法向Operation中添加多个可执行的Block。这样的Operation中的任务会并发执行,它会在主线程和其它多个线程执行这些任务。

    1
    2
    3
    4
    5
    6
    7
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }];
    [blockOpertation addExecutionBlock:^{
    // execute code
    }];
    [blockOperation start];

    NOTE:addExecutionBlock:方法必须在start方法之前执行,否则会报错。

  • 自定义的Operation

    自定义Operation类需要继承Operation类,并实现其main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。

创建队列

通过调用一个NSOperation类的start()方法来启动的任务,默认在当前线程同步执行。如果要避免占用当前线程,就需要使用到队列NSOperationQueue。只要将Operation添加到队列,就会自动调用任务的start()方法。

  • 主队列

    添加到主队列中的任务时串行执行的。

    1
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
  • 其它队列

    其它队列中的任务会在其它线程并行执行。

    1
    2
    3
    4
    5
    NSOperationQueue *queue = [NSOperatin alloc] init];
    NSBlockOperatin *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // execute code
    }]
    [queue addOperation:blockOperation];

    如果需要任务在队列中串行执行,可以通过设置NSOperationQueue的maxConcurrentOperationCount来实现。

    1
    2
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue setMaxConcurrentOperationCount:1];

    你还可以通过addOperationWithBlock:方法来向队列中添加新任务。

    1
    2
    3
    4
    NSOperationQueue *queue = [NSOperatin alloc] init];
    [queue addOperationWithBlock:^{
    // cxecute code
    }]

添加任务依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation	*blockOperationFirst  = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationSecond = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
NSBlockOperation *blockOperationThird = [NSBlockOperation blockOperationWithBlock:^{
// execute code
}];
[blockOperationSecond addDependency:blockOperationFirst];
[blockOperationThird addDependency:blockOperationSecond];
NSOperationQueue *queue = [NSOperatin alloc] init];
[queue addOperations:@[blockOperationFirst,blockOperationSecond,blockOperationThird]];

NOTE:

  • 添加相互依赖会造成死锁。
  • 使用removeDependency方法来移除依赖关系

其它方法

  • NSOperation类的一些其它方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 判断任务是否正在执行
    BOOL exccuting;
    // 判断任务是否完成
    BOOL finished;
    // 设置任务完成后的后续操作
    void (^completionBlock) (void);
    // 取消任务
    - (void)cancle;
    // 阻塞当前线程直到此任务执行完毕
    - (void)waitUntilFinished;
  • NSOperationQueue

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取队列的任务数量
    NSUInteger operationCount;
    // 取消队列中的所有任务
    - (void)cancelAllOperations;
    // 阻塞当前线程直到此队列中的所有任务执行完毕
    - (void)waitUntilAllOperationsAreFinished;
    // 暂停或继续队列
    BOOL suspended;

后台运行探索与解析

iOS 后台运行


当用户没有主动的使用你的应用程序时,系统会将它转换为后台运行状态。对于大多说应用程序来说,后台运行状态只是应用程序在被系统挂起前的一个阶段。挂起应用程序是一个改善电池寿命并让系统为前台应用程序释放重要资源的方式。

大多数应用程序都能很容易的挂起,但是拥有合理的理由在后台继续运行的应用程序也是有的。一个远足应用程序想要随着时间来定位用户的位置,让它能够在地图上显示用户的运动进程。一个音频应用程序可能需要在屏幕锁定时继续播放音乐。其它的应用程序可能想要在后台下载内容以便能够减少将内容呈献给用户的延迟。当你发现你的应用程序需要保持在后台继续运行的时候,iOS系统能够帮助你有效率的且尽可能少地占用系统资源的完成这个目的。

iOS提供的技术分为三类:

  • 在前台开始短任务的应用程序可以在进入后台状态时向系统请求完成这个任务的额外时间。
  • 在前台启动下载的应用程序可以将这些下载的管理移交给系统,从而允许在下载过程中暂停或终止该应用。
  • 需要在后台运行以支持特定类型任务的应用程序可以声明对一个或多个后台执行模式的支持。

尽可能避免在后台执行任何任务除非这样做能全面的提升用户的体验。应用程序可能会因为启动了另一个应用程序,锁定了屏幕或现在不使用它而被转入后台。在这些情况下,用户都表明你的应用程序现在不需要做任何有意义的工作。在这种情况下继续运行只会浪费设备的电量并可能导致用户强制的完全退出你的应用程序。所以你应该审慎的考虑在后台运行应用程序。

执行有限长度的任务

被转入后台的应用程序期待尽可能快的进入非活动状态以便它们能够被系统挂起。如果你的应用程序正在执行某个任务而且完成这个任务还需要一些时间的话,你可用调用 UIApplicationbeginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler:方法来请求一些额外的执行时间。调用这两个方法的任一个都会暂时的推迟你的应用程序被挂起,从而为完成你正在进行的任务赢得一些额外的时间。一旦你的应用程序完成了任务,你必须调用endBackgroundTask:方法让系统知道你的应用程序已经完成了任务,可以被挂起了。

每个调用beginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler: 方法的应用程序都会生成一个与相应的任务相关的标记。当你的应用程序完成任务时,它必须以这个标记来调用 endBackgroundTask:方法告知系统任务已经完成。调用 endBackgroundTask:方法失败会导致你的应用程序被终止。如果你在启动任务的时候提供了一个完成处理模块,系统会调用这个模块并给你最后一次结束任务避免程序被终止的机会。

你不需要一直到等到应用程序进入后台才指定后台任务。一个更有用的设计是在开始任务之前调用beginBackgroundTaskWithName:expirationHandler:beginBackgroundTaskWithExpirationHandler:方法,一旦完成就调用endBackgroundTask:方法.

下面的的代码展示了当你的应用程序进入后台时如何开始一个长时运行的任务。在这个例子中,开始后台任务的请求包含了一个完成处理模块以防这个任务耗时太长。这个任务稍后会被提交到一个异步执行的队列中以便applicationDidEnterBackground:方法能够正常返回。blocks的使用简化了维护一些重要变量引用所需要的代码。bgTask变量是指向存储当前任务标识符的指针的类的成员变量,它在调用这个方法之前被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)applicationDidEnterBackground:(UIApplication *)application {
bgTask = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];

// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// Do the work associated with the task, preferably in chunks.

[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
}

注意: 你总是要在开始一个任务的时候提供完成处理模块,但是如果你想知道你的应用程序还能运行多长时间,你可以通过UIApplication对象的backgroundTimeRemaining属性来获得。

在你的完成处理模块中,你可以加入一些必须的代码来关闭你的任务。但是,完成处理模块中的任何代码都不应该耗费太长的时间去执行,因为你的完成处理模块一旦被调用,就说明你的应用程序已经快要被关闭了。因此,只执行最少的清理状态信息并结束任务。

在后台执行下载任务

当下载文件时,应用程序应该使用NSURLSession对象来开始下载,这样系统就能控制下载的进程即使应用程序被挂起或终止。当你配置NSURLSession对象进行后台传输时,系统用一个单独的队列管理这些任务并以常规方式将传输的状态报告给你的应用程序。如果你的应用程序在传输正在进行时被终止,系统会在后台继续传输,并在传输完成或一个甚至多个任务需要你的应用程序时启动你的应用程序(若果使用)。

为了支持后台传输,你必须合适地配置你的NSURLSession对象。为了配置NSURLSession有必须先创建一个NSURLSessionConfiguration对象并给它的一些属性赋予合适的值。然后将这个NSURLSessionConfiguration对象在初始化NSURLSession对象时传递过去。

创建支持后台下载的NSURLSessionConfiguration对象的过程如下:

1.使用NSURLSessionConfigurationbackgroundSessionConfigurationWithIdentifier:方法创建配置对象。

2.将配置对象的sessionSendLaunchEvents属性的值设置为YES

3.如果你的应用程序在前台进行转移,建议你将sessionSendsLaunchEvents属性设置为YES

4.根据需要配置配置对象的任何其他属性。

5.使用配置对象创建您的NSURLSession对象。

一旦配置完成,你的NSURLSession对象会在合适的时间将上传和下载任务移交给系统。如果任务在你的应用程序仍在运行时完成(无论是在前台或是后台),NSURLSession对象都会以常规方式通知它的代理。如果任务还未完成时你的应用程序就被终止,系统会自动地在后台管理任务。如果用户终止了你的应用程序,系统会停止任何待处理的任务。

当所有与后台会话管理的任务完成时,系统会重启被终止的应用程序(假定sessionSendsLaunchEvents属性被设置为YES而且这个应用程序不是被用户强制终止的)并调用应用代理的application:handleEventsForBackgroundURLSession:completionHandler:方法。(系统还可以重新启动应用程序来处理身份验证或其他需要你的应用程序注意的任务的相关事件)在执行该委托方法时,请使用提供的标识符创建一个新的与之前相同的NSURLSessionConfigurationNSURLSession对象。 系统将你的新会话对象重新连接到先前的任务,并将其状态报告给会话对象的委托。

执行长时间运行的任务

对于需要更多后台执行时间的任务,你必须请求特定的权限才能在后台运行它们而不被挂起。iOS中,只有特定类型的应用程序被允许在后台运行:

  • 在后台播放音频内容给用户,比如音乐播放器
  • 在后台录制音频文件
  • 使用户随时了解其位置的应用程序,比如导航应用程序
  • 支持VoIP的应用程序
  • 需要经常性的下载和处理新内容的应用程序
  • 周期性的从外部配件接收更新的应用程序

使用这些服务的应用程序必须声明其支持的服务,并使用系统框架来实现这些服务的相关方面。

声明你的应用程序支持的后台模式

你必须在应用程序使用后台任务之前声明你要支持的后台任务类型。在Xcode 5和之后,你需要在你工程的Capabilities选项卡中声明你要支持哪些后台模式。启用后台模式选项将UIBackgroundModes键添加到应用程序的Info.plist文件中。

下表列出了你可以指定的后台模式的值:

Xcode后台模式 UIBackgroundModes 值 描述
音频和AirPlay audio 应用程序在后台播放或录制音频内容。用户必须在第一次使用前就授权使用麦克风。
位置更新 location 使用户随时知道他们的位置,即使应用在后台运行。
网络电话 voip 应用程序提供给用户通过网络连接进行通话的能力。
Newsstand下载 newsstand-content Newsstand类型的应用程序在后台下载并处理报纸或杂志内容。
外部附件通信 external-accessory 应用程序与需要通过外部附件框架定期提供更新的硬件配件配合使用。
使用蓝牙设备 bluetooth-central 应用程序使用需要通过Core Bluetooth框架定期发送更新的蓝牙设备。
作为蓝牙LE附件 bluetooth-peripheral 应用程序通过Core Bluetooth框架支持外设模式下的蓝牙通信。使用此模式需要用户授权
后台抓取 fetch 应用程序定期的从网路下载并处理少量数据。
远程推送 remote-notification 应用程序想要在接收到一个远程推送时开始下载内容。

以上每种模式都让系统知道应该在合适的时间唤醒或启动应用程序来响应相关的事件。

追踪用户位置

在后台追踪用户的位置的方式有好几种,大多数方式实际上并不需要你的应用程序在后台不断的运行。

  • 重大位置变更
  • 仅在前台定位服务
  • 后台定位服务

对于不需要高精度位置数据的应用程序来说,推荐使用重大位置变更定位服务。这个服务只有在用户的位置发生非常重大的变化时才会产生位置更新;它对社交类应用程序或者给用户提供不是很重要的位置相关信息的应用程序来说是非常理想的定位方式。如果当一个位置更新发生时应用程序被终止,系统会在后台唤醒它来处理这个更新。如果应用程序开始使用了这个服务然后被终止,当新的位置更新产生时,系统会自动重启它。

仅前台定位服务和后台定位服务都使用标准的Core Location服务获取位置数据。唯一的区别是,如果应用程序被挂起,则仅前台定位服务停止发送更新。 前台定位服务适用于只在前台需要位置数据的应用程序。

你可以在Xcode工程中的Capabilities选项卡中启用支持定位服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为location来启用这个服务)。启用这个服务并不会阻止系统挂起你的应用程序,但是它会告知系统无论何时新的位置更新被发送过来都应该唤醒应用程序来处理。

重要提示:鼓励你谨慎使用标准的定位服务或改用重要的位置更改服务。 定位服务需要经常使用iOS设备的板载无线电硬件。 连续运行这个硬件会消耗大量的电量。 如果你的应用程序不需要向用户提供精确且连续的位置信息,则最好尽量减少使用位置服务。

在后台播放或录制音频

一个需要连续播放或录制音频的应用程序(即使应用处在后台)可以注册后台服务,实现即使在后台也能执行这些任务。你可以在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为audio来启用这个服务)。在后台播放音频内容的应用程序必须播放听得见的内容而不能是无声的。

后台音频应用程序的典型例子包括:

  • 音乐播放程序
  • 音频录制程序
  • 支持通过AirPlay播放音频或视频的程序
  • VoIP程序

UIBackgroundModes键包含audio值时,系统的媒体框架会自动阻止相关的应用程序被挂起。只要应用程序还在播放视屏或音频内容,录制音频,它就还能在后台运行。然而,一旦录制或播放停止,就会被系统挂起。

你可以使用任何系统音频框架来处理后台音频播放,并且使用这些框架的过程和在前台使用相同(对于通过AriPlay播放视频内容来说,你可以使用Media Player或AVFoundation框架来实现)。因为应用程序在播放媒体文件时不会被挂起,所以即使应用程序在后台也能正常的进行回调操作。在你的回调中,你应该只做为播放提供数据的工作,不应该在回调中执行任何与播放无关的任务。

在任何给定的时刻,因为可能有不止一个应用程序支持音频服务,所以由系统来决定哪个应用程序能够播放或录制音频。前台应用程序总是有优先的音频操作权利。可能有不止一个应用程序被允许在后台播放音频,这个时候决定哪个应用程序能够播放音频就取决于每个应用程序的音频会话的配置。

实现一个VoIP应用程序

一个Voice over Internet Protocol应用程序允许用户通过互联网而不是设备的蜂窝网络设备进行语音通话。这样的一个应用程序需要为它的服务维持一个持续的网络连接,以便它能接收到打进来的电话和其它相关数据。系统允许VoIP应用程序被挂起并给它提供了监测它们的sockets的便利,而不是让它一直处于唤醒状态。当检测到传入流量时,系统唤醒VoIP程序并将socket的控制权交还给它。

为了配置VoIP应用程序,你必须:

  • 在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为voip来启用这个服务)。
  • 为VoIP应用程序配置一个socket。
  • 在移动到后台之前,调用setKeepAliveTimeout:handler:方法来安装一个定期执行的程序。你的应用程序可以使用这个处理程序来保持服务连接。
  • 配置你的音频会话来操作进入或退出活跃的使用状态的转换。

设置UIBackgroundModes的值为voip让系统知道,当VoIP应用程序需要管理它的网络会话时,系统应该允许它在后台运行。为了让VoIP应用程序总是可用,系统会在启动后立刻重新启动拥有这个键值的应用程序。

大多数的VoIP应用程序也需要配置后台音频模式,因为它也需要在后台发送音频内容。因此,你应该将UIBackgroundModes的值设置为audiovoip。如果你不这样做的话,你的应用程序将不能在后台播放或录制音频内容。

适时的获取少量内容

需要定期的检查新内容的应用程序可以请求系统唤醒它们,以便它们可以初始化一个获取内容的拉取操作。为了支持这种模式,你需要在Xcode工程中的Capabilities选项卡中启用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为fetch来启用这个服务)。启用这个服务并不能保证系统会给你的应用程序任何时间来执行后台拉取操作。系统必须在你的应用程序拉取内容的请求和其它应用程序以及系统自身之间做出平衡。在评估所有信息后,如果有很好的机会的话,系统会给请求拉取内容的应用程序一些执行时间。

当有好机会产生时,系统会唤醒或启动你的应用程序到后台并调用应用程序代理对象的application:performFetchWithCompletionHandler:方法。使用这个方法来检查新内容,如果新内容可用则开始下载操作。一旦新内容下载完成,你必须将新内容是否可用的结果传递给提供好的完成处理块。执行这个块告诉系统它可以将你的应用程序转换到挂起状态并评估其使用功率。可以快速下载少量内容并在它们有可用的下载内容时准确反应的应用程序,比起花费更长的下载时间或声称有可用的下载内容但没有下载任何东西的应用程序,更可能在未来获得执行时间。

在下载任何内容时,推荐你使用NSURLSession类来开始和管理你的下载。

使用推送通知来开始下载

如果你的服务器在有新的应用程序可用内容时向用户的设备发送了一个推送通知,你可以请求系统在后台运行你的应用程序并立即开始下载新的可用内容。这种后台模式意图在于尽可能减少从你的用户看见推送通知到你的应用程序可以展示相关内容之间的时间。应用程序通常会在用户看见推送通知的差不多相同的时间被唤醒,但是仍会给你更多的准备时间。

为了支持这种模式,你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为remote-notification来启用这个服务)。

对于一个使用推送通知来触发下载操作的应用程序来说,通知的有效内容必须包含值为1content-available的键。当这个键值对被检测到时,系统会启动或唤醒你的应用程序到后台同时调用应用程序的代理对象的application:didReceiveRemoteNotification:fetchCompletionHandler:方法。你应该在这个方法里实现对相关内容的下载并将下载好的内容在加入到你的应用程序中。

在下载任何内容时,推荐你使用NSURLSession类来开始和管理你的下载。

在后台下载新闻站内容

下载新闻和新的杂志文章的新闻站应用程序可以注册在后台进行这些下载。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为newsstand-content来启用这个服务)。当你提供这个关键字时,如果你的应用程序没有在运行,系统会启动你的应用程序以便它能开始下载新的文章。

当你使用Newsstand Kit框架来开始一个下载时,由系统为你的应用程序操作下载的进程。即使你的应用程序被挂起或终止,系统仍会继续下载文件。当下载操作完成后,系统将下载好的文件传输到你的应用程序的沙盒并向你的应用程序发送一个通知。如果应用程序没有在运行,这个通知会唤醒它并给它一个处理新的下载文件的机会。如果在下载过程中发生了错误,你的应用程序也会这样被唤醒来处理这个错误。

与外部附件通信

使用外部附件的应用程序可以请求在附件发送了一个更新时被唤醒,即使它处在挂起状态。这种支持对于某些定期发送数据的附件来说是非常重要的,比如心率监视器。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为external-accessory来启用这个服务)。当你启用这个模式时,外部附件框架不会关闭活跃的外部附件会话。当外部附件发送来新的内容时,这个框架唤醒你的应用程序以便它能处理这些数据内容。在外部附件建立连接或失去连接时,系统也会唤醒应用程序来进行处理。

任何支持附件更新后台处理的应用程序都必须遵循以下基本准则:

  • 应用程序必须提供一个界面,让用户来开始或者停止附件更新事件的发送。

  • 一旦被唤醒,应用程序大约有10S时间来处理数据。理想情况下,它应该尽可能快速地处理数据然后转换到挂起状态。但是,如果需要更多的时间,应用程序可以使用beginBackgroundTaskWithExpirationHandler:方法来申请额外的执行时间。

与蓝牙附件通信

使用蓝牙外设的应用程序可以请求在外设发送了一个更新时被唤醒,即使它处在挂起状态。这种支持对于需要定期发送数据的Bluetooth-LE外设来说是非常重要的。你需要在Xcode工程中的Capabilities选项卡中启这项用服务(你也可以通过在Info.plist文件中设置UIBackgroundModes的值为bluetooth-central来启用这个服务)。当你启用这个模式时,Core Bluetooth框架会保持相应外设的任何活跃会话连接。此外,当有新的数据从外设传来,系统会唤醒应用程序让他能够处理数据。在外设建立连接或失去连接时,系统也会唤醒应用程序来进行处理。

iOS 6中,一个使用蓝牙外设的应用程序也可以在外设模式下运行。要充当蓝牙外设,你需要在Xcode工程中的Capabilities选项卡中启这项用服务(您也可以通过在应用程序Info.plist文件中包含UIBackgroundModes键和bluetooth-peripheral值来启用此支持)。启用此模式可让Core Bluetooth框架在后台简单地唤醒应用程序,以便它可以处理外设的相关请求。

支持蓝牙数据后台处理的任何应用程序都必须基于会话,并遵循以下基本准则:

  • 应用程序必须提供一个界面,允许用户启动和停止蓝牙事件的传递。

  • 被唤醒后,应用程序大概需要10秒钟才能处理数据。理想情况下,它应该尽可能快地处理数据,并允许自己再次暂停。但是,如果需要更多的时间,应用程序可以使用beginBackgroundTaskWithExpirationHandler:方法来请求更多的时间,它应该只有在绝对必要的时候才这样做。

在后台获得用户的注意

通知是处在挂起,在后台运行或没有运行的应用程序获得用户的注意的一种方式。应用程序可以使用本地通知来显示提醒框,播放声音,标记应用程序的图标或者全部一起使用。比如,一个闹钟应用程序可能会使用本地通知来播放闹铃声并显示一个提醒框来使闹钟不可用。当一个通知被发送给用户,用户必须决定是否授权让应用程序来到前台。如果应用程序已经在前台,本地通知将会被静默的发送给你的应用程序而不是发送给用户。

为了安排本地通知的发送,需要创建一个配置了通知的各个参数的UILocalNotification类的实例并调用UIApplication类的方法。本地通知对象包含了发送通知的类型和在什么时间发送它的信息。UIApplication类的方法提供了是立即发送还是按时间表发送通知的选项。

下面的代码片段展示了如何安排一个由用户设置的使用了日期和时间的闹钟的例子。这个例子在一个时间只配置了一个闹钟并在安排它之前终止了前一个闹钟(你的应用程序在任何给定的时刻都不能拥有超过128个处在活跃状态的本地通知,它们每一个都能设置成以固定的时间间隔重复)。如果闹钟被触发时,应用程序没有运行或者处在后台,闹钟会弹出提醒框并在后台播放音频。如果应用程序是活动且处在前台,则会调用应用程序的代理对象的application:didReceiveLocalNotification:方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)scheduleAlarmForDate:(NSDate*)theDate {
UIApplication* app = [UIApplication sharedApplication];
NSArray* oldNotifications = [app scheduledLocalNotifications];

// Clear out the old notification before scheduling a new one.
if ([oldNotifications count] > 0)
[app cancelAllLocalNotifications];

// Create a new notification.
UILocalNotification* alarm = [[UILocalNotification alloc] init];
if (alarm)
{
alarm.fireDate = theDate;
alarm.timeZone = [NSTimeZone defaultTimeZone];
alarm.repeatInterval = 0;
alarm.soundName = @"alarmsound.caf";
alarm.alertBody = @"Time to wake up!";

[app scheduleLocalNotification:alarm];
}
}

本地通知使用的音频文件和推送通知使用的要求相同。自定义的音频文件必须位于你应用程序主要包中且必须是以下几种格式:Linear PCM,MA4,μ-Law或者a-Law。你也可以指定UILocalNotificationDefaultSoundName常量来使设备播放默认的提醒声音。当通知被发送且音频被播放时,系统也会触发设备震动来配合。

何时启动应用程序进入后台

支持后台执行的应用程序可能会被系统重启来处理发生的事件。如果应用程序不是被用户强制退出,系统会启动应用程序在下列事件发生时:

  • 对于定位应用程序
    • 系统收到一个满足发送给应用程序标准的位置更新。
    • 设备进入或退出一个已注册的区域。
  • 对于音频应用程序,音频框架需要应用程序来处理某些数据。
  • 对于蓝牙应用程序
    • 应用程序作为从连接的外设接收数据的核心角色。
    • 应用程序作为从连接核心接收命令的外设。
  • 对于后台下载应用程序
    • 应用程序收到一个包含 content-available 键并且值为 1的远程通知。
    • 系统在随机时间唤醒应用程序来开始新内容的下载。
    • 使用NSURLSession对象在后台下载内容的应用程序,当这个会话相关的任务全部成功完成或发生了一个错误。
    • Newsstand应用程序内容下载完成。

在大多数情况下,系统并不会重启被用户强制退出的应用程序。定位应用程序是个例外,在iOS8和之后。如果不是的话,用户必须显式的启动应用程序或者重启设备,系统才能自动的启动程序进入后台。当启用密码保护时,在用户第一次解锁设备之前,系统不会启动任何应用程序进入后台。

成为一个负责的后台运行应用程序

在使用系统资源和硬件时,前台应用程序始终比后台应用程序的优先级高。后台应用程序需要为这个差异做准备并调整它们的行为:

  • 不要在你的代码中进行任何OpenGL ES调用。在后台运行时,你绝不能创建EAGLContext对象或发出任何OpenGL ES绘图命令。使用这些会使你的应用程序被立即终止。应用程序必须保证任何先前提交的命令在移入后台之前完成。
  • 在挂起之前停止任何Bonjour-related服务。在你的应用程序移入后台但未被挂起之前,它应该从Bonjour注销并关闭任何与网络服务相关的监听sockets。一个挂起的应用程序无论如何不能响应任何传入的服务请求。如果你没有关闭Bonjour服务,在应用程序挂起时,系统会自动的关闭它们。
  • 准备好处理基于网络的sockets的连接失败。系统可能会销毁socket连接,当你的应用程序因为某些原因被挂起时。只要你的基于网络的socket代码准备好了如何处理其他类型的网络失败,比如使用失败信号或网络转化,socket被销毁就不会导致出现任何不寻常的问题。当你的应用程序重新开始运行时,如果使用socket遭遇故障,只需重新建立一个。
  • 在转入后台之前保存你的应用程序的状态。处于低内存的情况下,后台应用程序可能会被从内存中移除以释放空间。挂起的应用程序首先被移除,并且不会在移除之前通知应用程序。因此,应用程序应该利用iOS 6及之后提供的状态保存机制将应用程序状态信息保存到磁盘。
  • 在转入后台时移除对任何不需要的对象的强引用
  • 在挂起之前停止使用共享系统资源。与共享系统资源进行交互的应用程序应该在被挂起之前停止使用这些资源。前台应用程序总是拥有这些资源的优先使用权。当你的应用程序挂起时,如果被发现它在使用这些共享资源,则会被系统杀死进程。
  • 避免更新窗口和视图。因为你的应用程序的窗口和视图在后台是不可见的,你应该避免更新它们。如果你的应用程序需要在系统拍摄快照前更新窗口的内容的话则是一种例外情况。
  • 响应外部附件的连接和断开通知。对于和外部附件通信的应用程序来说,当应用程序转入后台时系统会自动的发送一个断开通知。应用程序必须注册这个通知并用它来关闭当前附件的会话。当应用程序转入前台时,一个匹配连接的通知被发送,给应用程序重新连接的机会。
  • 在转入后台时清理活动的提醒框的资源。为了在各个应用程序之间进行切换时保存上下文,在你的应用程序进入后台时,系统不会自动地释放上拉菜单或提醒视图。在应用程序转入后台之前,你需要提供合适的清理行为。
  • 在转入后台之前从视图中移除敏感信息。当应用程序转入后台时,系统会对应用程序的主窗口拍摄快照,当应用程序转入到前台时,这个快照会短暂的显示。在从applicationDidEnterBackground:方法返回之前,你应该隐藏或混淆可能被作为快照一部分拍摄的密码和其它敏感的个人信息
  • 在后台运行时尽可能的少做工作。给与后台应用程序的执行时间相比于前台应用程序来说有更多的限制。应用程序不应该在后台执行太长时间,这可能导致应用程序被终止。

选择退出后台执行

如果你不希望你的应用程序在后台运行,你可以通过将值为YESUIApplicationExitsOnSuspend键加入Info.plist来显式的退出后台模式。当一个应用程序选择退出后台,它的生命周期将在未运行,非活动和活动状态之间转换,永远不会进入后台或被挂起状态。当用户按下Home按钮来退出应用程序时,应用程序代理对象的applicationWillTerminate:方法将会被调用,应用程序将有大约5秒时间在它被终止或移入未运行状态之前来执行清理任务并退出。

强烈不鼓励退出后台执行,但在某些情况下可能是首选。 具体来说,如果后台执行的编码对你的应用程序来说增加了很大的复杂性,那么终止应用程序可能会更简单。 此外,如果你的应用程序消耗大量内存,并且无法轻松释放任何内存,系统可能会迅速杀死你的应用程序,为其他应用程序腾出空间。 因此,选择终止,而不是切换到后台,可能会产生相同的结果,并节省你的开发时间和精力。

应用程序生命周期和状态转换操作策略

iOS 应用程序生命周期


基本概念介绍

The Main Function - 程序的入口

main函数是每个基于C的应用程序的入口,iOS也是这样。在Xcode中开发iOS程序的话,Xcode会自动为你创建这个函数作为你工程的一部分。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

main函数控制UIKit framework的工作。UIApplicationMain函数通过创建应用程序的核心对象,从可用的storyboard文件加载应用程序的用户界面,调用自定义代码,以便让你可以在程序启动时进行一些初始设置,并将应用程序的运行循环启动。

The Structure of an App - 程序的结构

在启动过程中,UIApplicationMain函数设置了几个关键对象,启动应用程序并开始运行它。 每个iOS应用程序的核心都是UIApplication对象,其工作是为了方便系统与应用程序中的其他对象之间的交互。

一个iOS应用程序中包含的关键对象:

一个iOS应用程序中各个对象的所起到的作用:

Object Description
UIAppplication UIApplication对象管理着App的事件循环和一些高级App行为,并向它的委托对象报告App的状态转换和一些特殊事件。
Appdelegate Appdelegate是你编写的程序代码的核心 ,它与UIApplication对象一起工作,处理应用程序初始化,状态转换和许多高级App行为。 这个对象也是唯一一个保证在每个应用程序中都出现的对象,因此通常用于设置应用程序的初始数据结构。
ViewController View Controller对象管理着你的App内容在屏幕上是如何呈现的。一个View Controller对象管理着一个视图和这个视图的子视图。
UIWindow UIWindow对象协调屏幕上一个或多个视图的呈现。 大多数应用程序只有一个窗口,它在主屏幕上显示内容,但应用程序可能会在外部显示器上显示内容的附加窗口。要更改应用程序的内容,可以使用视图控制器更改相应窗口中显示的视图。 你永远不会更换窗口本身。除了托管视图之外,Windows还可以使用UIApplication对象将事件传递给的视图和控制器。
View,Control and Layer View和Control提供了应用内容的可视化表示。 除了包含View和Control之外,应用程序还可以将Core Animation图层纳入其视图和控制层次结构中。 层对象实际上才是表示可视内容的数据对象。

The Main Run Loop - 主运行循环

应用程序的主运行循环处理所有与用户相关的事件。 UIApplication对象在启动时设置主运行循环,并使用它来处理事件并处理基于视图的接口的更新。

顾名思义,主运行循环在应用程序的主线程上执行,这确保了与用户相关的事件按照接收的顺序连续处理。

当用户与设备进行交互时,与这些交互相关的事件由系统生成,并通过由UIKit设置的特殊端口传送到应用。 事件在应用程序内部进入队列,并逐个分派到主运行循环执行。UIApplication对象是接收事件的第一个对象,并且决定需要做什么来处理这个事件。 触摸事件通常被分派到主窗口对象,主窗口对象又将其发送到触摸发生的视图。

可以在iOS应用程式中分发许多类型的事件。 最常见的如下表所示。 这些事件大多是使用应用程序的主运行循环分发的,但也有些不是。 一些事件被分发到一个委托对象或被分发给你提供的一个block。

事件类型 分发对象 备注
触摸事件 事件发生的视图 视图是响应者对象。任何没有被当前视图处理的触摸事件都会被沿着响应链继续向下分发进行处理。
遥控器事件和摇动运动事件 首要响应对象 遥控器事件用于控制媒体播放,并由耳机和其他附件产生。
加速度计、磁力仪和陀螺仪 你指定的对象 与加速度计,磁力计和陀螺仪硬件相关的事件传递给你指定的对象。
定位 你指定的对象 你注册定位服务以接收使用Core Location框架位置的事件。
重绘 需要更新的视图 重绘事件不涉及事件对象,而是简单地调用视图来绘制自身。

一些事件,如触摸和遥控事件,由你的应用程序的响应者对象处理。响应者对象在你的应用程序中无处不在。 大多数事件都针对特定的响应者对象,但如果需要处理事件,则可以将其传递给其他响应方(通过响应者链)。 例如,不处理事件的视图可以将事件传递到其父视图或父视图控制器。

触摸控件(如按钮)中发生的事件的处理方式与在许多其他类型的视图中发生的触摸事件的处理方式不同。 通常只有有限数量的交互可能与控件相关联,因此这些交互被重新封装到动作消息中并传递到适当的目标对象。 这种目标动作(Target-Action)设计模式可以轻松地使用控件来触发你应用中自定义代码的执行。

Execution States for Apps - 应用程序的执行状态

在任何给定的时刻,你的应用程序都处在下表所列的状态之一。 系统会改变应用程序的状态以响应整个系统中发生的操作。

状态 描述
Not running 应用程序没有运行或被系统终止运行。
Inactive 应用程序正在前台运行,但当前没有收到事件 (它可能正在执行其他代码)。应用程序通常在转换到不同的状态时暂时保持在此状态。
Active 应用程序正在前台运行,并且正在接收事件(这是前台应用程序的正常模式)。
Background 应用程序在后台执行代码。 大多数应用程序短暂地进入此状态。 但是,请求额外执行时间的应用程序可能会保持此状态一段时间。
Suspended 应用程序是在后台,但不执行代码。 系统将应用程序自动转换到这个状态,在这样做之前并不会通知应用程序。 挂起时,应用程序保留在内存中,但不执行任何代码。当出现低内存条件时,系统可能会清除已挂起的应用程序,以为前台应用程序腾出更多空间。

下图展示了应用程序在各种状态之间的转换:

大多数状态转换伴随着对应用程序委托对象的方法的相应调用 ,这些方法是你以适当方式响应状态变更的地方。

  • application:willFinishLaunchingWithOptions:

    告诉代理对象启动过程已经开始,但还没有发生状态恢复。

  • application:didFinishLaunchingWithOptions:

    告诉代理对象启动过程快要完成,应用程序快要准备好运行。

  • applicationDidBecomeActive:

    告诉代理对象应用程序已经处于Active状态。

  • applicationWillResignActive:

    告诉代理对象应用程序即将进入Inactive状态。

  • applicationDidEnterBackground:

    告诉代理对象应用程序已经进入后台。

  • applicationWillEnterForeground:

    告诉代理对象应用程序即将进入前台。

  • applicationWillTerminate:

    告诉代理对象应用程序即将被终止。

App Termination - 应用程序的终止

应用程序必须随时准备终止运行,不应等待保存用户数据或执行其他关键任务。 System-initiated termination是应用程序生命周期的正常部分。该系统通常终止应用程序,以便它可以回收内存并为用户启动的其他应用程序腾出空间,但是系统也可能会终止行为不当或未及时响应事件的应用程序。

挂起的应用程序终止时不会收到通知,系统将杀死该进程并回收相应的内存。如果一个应用程序当前在后台运行并且没有挂起,系统会在终止之前调用应用程序委托的applicationWillTerminate:。当设备重新启动时,系统不会调用此方法。

除了系统终止你的应用程序,用户可以使用多任务UI显式地终止你的应用程序。System-initiated termination与终止挂起的应用程序具有相同的效果。该应用程序的进程被杀死,并且不会有通知发送到该应用程序。

Threads and Concurrency - 线程和并发

系统创建你的应用程序的主线程,你可以根据需要创建其他线程来执行其他任务。

对于iOS应用,首选技术是使用Grand Central Dispatch(GCD),操作对象和其它异步编程接口,而不是自己创建和管理线程。 GCD等技术可以让你定义想要执行的工作以及要执行的顺序,但让系统决定如何在可用的CPU上执行该工作。让系统处理线程管理简化了必须编写的代码,使得更容易确保代码的正确性,并提供更好的整体性能。

在考虑线程和并发时,请考虑以下几点:

  • 涉及视图,核心动画和许多其他UIKit类的工作通常必须发生在应用程序的主线程上。

  • 长时间的任务(或潜在的长度任务)应始终在后台线程上执行。任何涉及网络访问,文件访问或大量数据处理的任务都应使用GCD或操作对象异步执行。

  • 在启动时,尽可能将任务从主线程移出。在启动时,您的应用程序应尽可能快地设置用户界面。只有有助于设置用户界面的任务才能在主线程上执行。

    所有其他任务应该是异步执行的。


操作应用程序的状态转换


对于应用程序的任何一个可能的运行状态,系统都对你的应用程序有不同的期望。当应用程序的状态发生转换时,系统会通知应用对象,应用对象又通知其代理。你可以使用UIApplicationDelegate协议的状态转换方法来检测这些状态变化并进行适当的响应。

应用程序启动时应该做什么

当应用程序启动(进入前台货后台)时,你应该使用应用程序委托对象的application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法执行一些操作:

  • 检查启动选项字典的内容(launch options dictionary),了解应用程序启动的原因,并作出适当的响应。

  • 初始化应用程序的关键数据结构。

  • 准备应用程序的窗口和视图以进行内容显示。

    • 使用OpenGL ES进行绘图的应用程序不能使用这些方法来准备绘图环境,使用OpenGL ES绘图应该在applicationDidBecomeActive:方法中进行准备。

    • Show your app window from your application:willFinishLaunchingWithOptions: method. UIKit delays making the window visible until after the application:didFinishLaunchingWithOptions: method returns.

    • application:willFinishLaunchingWithOptions:方法中配置如何显示应用程序的Window。UIKit延迟Window的显示直到application:didFinishLaunchingWithOptions:方法进行了返回。

在启动时,系统会自动的加载应用程序的主要storyBoard文件,并加载初始视图控制器。对于支持状态恢复的应用程序,状态恢复机制会在调用application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法之间将应用程序恢复到以前的状态。使用application:willFinishLaunchingWithOptions:方法显示应用程序窗口,并确定状态恢复是否应该发生。使用application:didFinishLaunchingWithOptions:方法对应用程序的用户界面进行任何最终调整。

application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:方法应始终尽可能轻量级,以减少应用程序的启动时间。如果应用程序没有及时完成其启动周期,系统将使其无法响应。因此,任何可能减慢启动速度的任务(如访问网络)都应该在辅助线程上执行。

The Launch Cycle - 启动周期

当你的应用程序启动后,在短暂的停留在非活动状态(Inactive)后,将会从非运行状态(Not Running)转换到活动状态(Active)或后台状态(Background)。作为启动周期的一部分,系统会为你的应用程序创建一个进程和一个主线程,并在主线程中调用应用程序的main函数。

下图显示了应用程序启动到前台时发生的事件序列,包括调用的应用程序委派方法。

当你的应用程序启动到后台时,通常要处理一些后台事件,启动周期会有些许改变。主要的不同在于,在后台的应用程序不是处于活动状态,只是进入后台来处理一些事件,之后可能会被挂起。当启动到后台时,系统仍然会加载用户界面文件,但不会显示应用程序的窗口。

你可以通过在代理方法 application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:中检查UIApplicationapplicationState属性来决定应用程序是启动到前台还是后台。当你的应用程序启动到前台时,这个属性的值是UIApplicationStateInactive,当你的应用程序启动到后台时,这个属性的值是UIApplicationStateBackground

Launching in Landscape Mode - 以横屏模式启动

对于界面只使用横屏的应用程序来说,必须明确地请求系统以横屏模式启动。通常来说,应用程序以纵屏模式启动,只有在需要适应设备的方向的时候才旋转屏幕的方向。 对于支持纵向和横向方向的应用程序,应始终为纵向模式配置视图,然后让视图控制器处理任何旋转。
但是,如果您的应用程序支持横向而不是纵向方向,请执行以下任务,让使其以横向模式启动:

  • 像你应用程序中的Infl.plist文件中添加UIInterfaceOrientation字段,并将其值设为UIInterfaceOrientationLandscapeLeftUIInterfaceOrientationLandscapeRight
  • 在横屏模式下布局你的视图并确保视图的布局和自动调整大小选项设置正确。
  • 重写试图控制器的shouldAutorotateToInterfaceOrientation:方法并在左向横屏或右向横屏时返回YES,在纵屏模式下返回NO

重要提示: 应用程序应该总是用视图控制器来管理基于窗口(Window)的内容。

Info.plist文件中的UIInterfaceOrientation字段告诉iOS系统该如何配置应用程序状态栏的方向(如果显示的话)以及任何视图控制器管理的视图在启动时的方向。视图控制器根据这个字段设置它的视图的初始方向。 使用此字段相当于在执行你的applicationDidFinishLaunching:方法的早期调用应用程序的setStatusBarOrientation:animated:方法。

Installing App-Specific Data Files at First Launch - 配置应用程序数据文件

你可以使用应用程序的第一个启动周期来设置任何应用程序运行所需要的数据或配置文件。 应用程序特定的数据文件应在应用程序沙箱的Library/Application Support/<bundleID>/目录中创建,其中是应用程序的标识符。你可以根据需要细分目录来组织你的数据文件。

如果应用程序包中包含你打算进行修改的数据文件,你应该将要修改的数据文件从应用程序的数据包中复制出来进行修改。你不能在应用程序包中修改数据文件,因为iOS应用程序是代码签名的,在应用程序包中修改数据文件会使应用程序的签名失效,使得你的应用程序无法启动。将打算修改的数据文件复制到Application Support(或者其它可写的目录)目录下进行修改是唯一安全的方式。

应用程序临时被打断时应该做什么

基于警报的打断会导致你的应用程序临时性的失去控制。这种情况下,你的应用程序仍然运行在前台,但是不接收任何来自系统的触摸事件(但是会继续接收推送通知等其他类型的时间,比如加速度计时间等)。你应该在applicationWillResignActive:方法中进行如下操作来处理这种情况:

  • 保存数据以及任何相关的状态信息。
  • 停止计时器和其他周期性任务。
  • 停止任何进行的数据请求任务。
  • 不要创建任何新的任务。
  • 停止视屏播放(除了通过AriPlay播放的视屏)
  • 如果你的应用程序是游戏的话,是游戏进入暂停状态。
  • 提高OpenGL ES 帧速率。
  • 暂停执行非关键代码的任何调度队列或操作队列。 (可以在不活动的情况下继续处理网络请求和其他时间敏感的后台任务)

当你的应用程序返回活动状态时,在applicationDidBecomeActive:方法中应该进行与applicationWillResignActive:相反的操作步骤。因此,一但重新活动,你的应用程序应重新启动计时器,恢复调度队列,并再次调低OpenGL ES帧速率。 但是,游戏不应该自动恢复,它们应该保持暂停,直到用户选择恢复它们为止。

当用户按下睡眠或唤醒按钮时,含有NSFileProtectionComplete保护选项的文件的应用程序必须关闭这些被保护文件的任何引用。对于设置了相应密码的设备,按下睡眠或唤醒按钮时应该锁定屏幕并强制系统丢弃对拥有完全保护文件的解密密钥。当屏幕锁定时,任何尝试访问相关文件的操作都会失败。所以你如果有这样的文件,你应该在applicationWillResignActive:方法中关闭对它们的任何引用,在applicationDidBecomeActive:中打开新的引用。

重要提示: 你应该总是在适当的时候保存用户数据。虽然你可以使用应用程序状态转换来强制对象将为保存的变更写入磁盘,但是合适的做法是永远不要等到状态转换时才去保存数据。

Responding to Temporary Interruptions - 响应临时打断

当基于警报的打断发生时(比如打进电话),应用程序暂时进入非活动状态已便系统可以提示用户如和继续操作。应用程序保持在这个状态直到用户拒绝了这个警报,这时应用程序会返回前台进入活动状态或进入后台。

以横幅形式显示的通知不会像基于警报的通知那样是你的应用程序进入非活动状态。尽管横幅放置在你应用程序窗口的顶部边缘,但是你的应用程序还是能够继续接收触摸事件。然而如果用户下拉横幅以便查看通知中心,你的应用程序就会像基于警报的打断发生时那样进入非活动状态。你的应用程序保持在非活动状态直到用户退出通知中心或启动另一个应用程序,这时,你的应用程序相应的会进入非活动转台或后台。

按下睡眠或唤醒按钮是另一种类型的可以导致你的应用程序暂时进入非活动状态的打断。当用户按下这个按钮,系统会使触摸事件变得不可用并让应用程序进入后台,同时将applicationState的值设置为UIApplicationStateBackground并锁定屏幕。

应用程序在前台时做什么

当你的应用程序返回前台进入活动状态时,你可以重新启动在进入后台时停止的任务。移动到前台时出现的步骤如下图所示,applicationWillEnterForeground:方法应该撤消在applicationDidEnterBackground:方法中完成的任何内容,并且applicationDidBecomeActive:方法应该继续执行与启动时相同的激活任务。

Be Prepared to Process Queued Notifications - 准备处理通知队列

一个处于挂起状态的应用程序必须做好在返回前台或进入后台执行状态时处理任何在队列中的通知。处于挂起状态的应用程序不执行任何代码,因此不能处理通知引发的屏幕方向改变,时间改变,偏好改变以及其它会影响应用程序的外观和状态的改变。为了确保这些改变不会丢失,系统将相关的通知放入队列并在应用程序开始执行代码(无论在前台还是后台)时分发通知给它。为了防止应用程序在恢复通知时变得超负荷,系统会将事件合并,并提供一个单一的通知(每种相关类型),反映自您的应用程序被挂起以来的净变化。

下表列出了分发给你的应用程序的可以合并的通知。这些通知的大多数都直接分发给已经注册的观察者。

Event Notifications
An accessory is connected or disconnected. EAAccessoryDidConnectNotification EAAccessoryDidDisconnectNotification
设备方向改变 UIDeviceOrientationDidChangeNotification除了这个通知,视图控制器自动的改变自己的方向。
重要的时间改变 UIApplicationSignificantTimeChangeNotification
电池状态改变 UIDeviceBatteryLevelDidChangeNotification UIDeviceBatteryStateDidChangeNotification
The proximity state changes. UIDeviceProximityStateDidChangeNotification
被保护的文件的状态的改变 UIApplicationProtectedDataWillBecomeUnavailable UIApplicationProtectedDataDidBecomeAvailable
内建显示器连接或失去连接 UIScreenDidConnectNotification UIScreenDidDisconnectNotification
屏幕显示模式改变 UIScreenModeDidChangeNotification
通过设置应用程序改变了应用程序的设置 NSUserDefaultsDidChangeNotification
当前语言或本地化设置改变 NSCurrentLocaleDidChangeNotification
用户的iCloud账户状态改变 NSUbiquityIdentityDidChangeNotification

通常在任何触摸事件或用户输入事件之前,在主线程中分发队列化的通知。大多数的应用程序应该足够快地处理这些事件,避免在重启是导致可观察到的延迟。

应用程序返回前台时也会接收到要更新自从上次更新以来被标记为dirty的视图的通知。一个运行在后台的应用程序仍然能调用setNeedsDisplaysetNeedsDisplayInRect:方法来请求更新视图。然而,因为这些请求更新的视图不可见,系统合并了这些请求并在应用程序进入前台后才更新它们。

Handle iCloud Changes - 处理iCloud改变

当iCloud的状态发生改变时,系统会发送一个NSUbiquityIdentityDidChangeNotification通知给你的应用程序。用户登入或登出iCloud账号,关闭了文档和数据的同步,都会引起iCloud状态的改变。这个通知表示应用程序应该更新缓存和与iCloud有关的用户界面元素。

如果你的应用程序已经提示了用户是否要在iCloud存储文件的话,不要在iCloud状态发生改变时再次提醒。在第一次提醒了用户后,将用户的选择保存在本地的偏好设置中。

Handle Locale Changes - 处理本地化改变

如果用户在你的应用程序挂起时改变了当前的本地化方案,当你的应用程序返回前台时,你可以使用NSCurrentLocaleDidChangeNotification通知来强制更新包含了对本地化方案敏感的信息,比如日期,时间和数字。当然,最好的避免发生本地化相关的问题的方法是用能简单更新视图的方法来编写代码:

  • 使用autoupdatingCurrentLocale类方法当你检索到NSLocal对象。这个方法返回一个可以自动更新自己来响应本地化方案改变的本地化对象,所以你永远不需要重新创建它。然而,当本地化方案发生改变时,你仍然需要更新包含来自于当前本地化方案的信息的视图。
  • 重新创建任何缓存的日期和数字格式,无论当前本地化方案何时发生变化。

Handle Changes to Your App’s Settings - 处理偏好设置的改变

如果你的应用程序含有被设置应用程序管理的设置选项,你应该监测NSUserDefaultsDidChangeNotification通知,因为用户可以改变这些设置当你的应用程序被挂起或在后台。你可以使用这个通知来响应并处理在这些设置上的重要改变。

一旦收到NSUserDefaultsDidChangeNotification通知,你的应用程序应该重新加载任何相关的设置,如果需要,恰当地重置用户界面。

应用程序在后台时做什么

当应用程序从前台转为后台执行时,使用你的代理对象的applicationDidEnterBackground:方法来做:

  • 准备应用程序的照片。当你的applicationDidEnterBackground:返回时,系统会对你的应用程序的用户界面拍照并用这个照片来做过度动画。如果你的应用程序里的视图包含敏感信息,你应该在applicationDidEnterBackground:返回之前隐藏或更改这些视图。
  • 保存任何应用程序状态的相关信息。在进入后台之前,你的应用程序应该已经保存好了所有重要的用户数据。使用转换到后台来保存应用程序状态的最后一分钟更改。

你的应用程序代理对象的applicationDidEnterBackground:方法大约有5分钟的时间来结束任何任务并返回。实际上,这个方法应该可能快的返回。如果这个方法没有在时间耗尽前返回,你的应用程序将会被终止并被移出内存。如果你仍然需要更多的时间来执行任务,调用beginBackgroundTaskWithExpirationHandler:方法来请求后台执行时间,然后在一个次要线程里开始任何长时任务。不管你有没有开始一个后台任务,applicationDidEnterBackground:方法都会在5分钟内退出。

提示: 除了调用applicationDidEnterBackground:方法外,系统还会发送UIApplicationDidEnterBackgroundNotification通知。你可以使用这个通知将清理任务分发给应用程序里的其它对象。

The Background Transition Cycle - 后台转换周期

When the user presses the Home button, presses the Sleep/Wake button, or the system launches another app, the foreground app transitions to the inactive state and then to the background state. These transitions result in calls to the app delegate’s applicationWillResignActive: and applicationDidEnterBackground: methods, as shown in Figure 4-5. After returning from the applicationDidEnterBackground: method, most apps move to the suspended state shortly afterward. Apps that request specific background tasks (such as playing music) or that request a little extra execution time from the system may continue to run for a while longer.

当用户按下Home键,按下睡眠或唤醒按钮,或者系统启动了另一个应用程序,运行在前台的应用程序会转换为非活动状态然后进入后台。这些状态转换会导致调用applicationWillResignActive:applicationDidEnterBackground:方法,如下图所示。从applicationDidEnterBackground:方法返回后,大多数应用程序不久之后就会转换到挂起状态。 请求特定后台任务(如播放音乐)或从系统请求一点额外执行时间的应用程序可能会持续运行一段时间。

Prepare for the App Snapshot - 准备应用程序快照

系统在应用程序代理对象applicationDidEnterBackground:方法返回的不久之前,对应用程序的窗口进行快照。相似的,当应用程序被唤醒执行后台任务时,系统会对应用程序进行一次新的反应改变的快照。

如果在进入后台更改视图时,可以调用主视图的snapshotViewAfterScreenUpdates:方法来强制执行这些更改。在一个视图上调用setNeedsDisplay方法对快照是无效的,因为快照发生在下一个绘制周期之前。调用值为“YES”的snapshotViewAfterScreenUpdates:方法将立即使用机器的底层缓冲区更新快照。

Reduce Your Memory Footprint

每个应用程序应该在进入后台时释放尽可能多的内存。 系统尝试尽可能多地在内存中保留应用程序,但是当内存不足时,会终止挂起的应用程序以回收该内存。 在后台消耗大量内存的应用程序是第一个要终止的应用程序。

实际上,你的应用程序应该在不再需要的时候删除对对象的强引用。 删除强引用使编译器能够立即释放对象,以便可以回收对应的内存。 但是,如果要缓存某些对象以提高性能,则可以等到应用程序转换到后台再删除对它们的引用。

应该尽快删除强引用的对象的一些示例包括:

  • 你创建的图像对象。
  • 可以从磁盘重新加载的大型媒体或数据文件
  • 您的应用程序不需要的任何稍后可以重新创建其他对象。

为了帮助你减少应用程序的内存占用空间,系统会自动清除在应用程序移动到后台时代表应用程序分配的数据。

  • 系统清除所有Core Animation层的后备存储。 此功能不会从应用程序的图层对象中删除内存,也不会更改当前图层属性。 它只是防止这些图层的内容出现在屏幕上,这表明应用程序在后台应该不会发生。
  • 它删除任何系统对缓存图像的引用。
  • 它删除了对其他系统管理的数据高速缓存的强引用。

属性修饰符分析

基础概念


atomic & nonatomic

决定编译器生成的setter/getter是否为原子操作 。
atomic是Objc使用的一种线程保护技术,防止在某个操作未完成时被另外一个线程操作,造成数据错误。然而这种多线程安全机制对资源的占用很大,在iPhone等小型设备中,除非需要使用多线程之间通讯编程,采用这种技术需要慎重考量。
一般情况下,使用nonatomic来修饰成员变量,存取器直接返回一个值,而不需要管当不同的线程同时访问时会发生什么。

readwrite & readonly

任何属性都可以声明为readwrite或readonly,且默认设置为readwrite 。
将一个属性声明为readwrite时,编译器将会为这个属性生成setter和getter。
将一个属性生命为readonly时,编译器将只会生成getter。

assign

直接赋值,不更改引用计数,一般用于基础数据类型和C语言类型数据。

retain

释放旧对象,并使传入的新对象引用计数+1。
此属性只能用于NSObject及其子类,而不能用于Core Foundation(因为其没有使用引用计数,需要另外使用CFRetain和CFRelease来进行CF的内存管理)。

strong

释放旧对象,并使传入的新对象引用计数+1。
在ARC下,strong为对象类型属性声明时的默认值。

copy

创建一个引用计数为1的新对象,这个新对象是传入对象的拷贝。
会拷贝传入的对象(即创建一个引用计数为1的新对象,但是内容与传入对象相同),并把新对象赋值给实例变量。

weak

弱引用,要求不保留传入的属性(既不会使传入的对象引用计数+1)。
类似于assign,但与assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会。


进阶问题


strong & weak

这里采用 stack overflow 上的一个对强/弱引用的解释

如果将对象想象成一只想要逃跑(be deallocated)的狗的话。

强引用就像是拴住狗的绳子。只要你还还牵着拴住狗的绳子,狗就逃跑不了。如果有五个人都将绳子拴住了狗,那么除非五个人都解开了绳子,否则狗就不可能逃跑。

而弱引用,就像一个小孩子指着狗说:看,那里有一直狗。只要狗仍被狗绳拴着,小孩就还能看见狗,他们仍会指着它。然而一但狗绳被解开,狗就会逃跑而不管多少小孩子指着它。

copy & retain

copy实际上是建立了一个新的相同的对象,而retain是将传入对象的引用计数+1 。

深拷贝 & 浅拷贝

浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间 。浅拷贝只是对对象的简单拷贝,让几个对象共用一片内存,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。
在iOS中,使用retain修饰的属性变量,就是一种浅拷贝。它既让几个指针共用同一片内存空间,又可以在release时由于计数的存在,不会轻易的销毁内存,达到更加简单使用的目的。
深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。
值得注意的是,对可变对象如NSMutableArray等无论是copy还是mutableCopy都是进行了深拷贝。

@property & @synthesize

如果我们希望使用默认的实例变量命名方式,那么我们在.m文件中就不需要使用@synthesize声明,系统会帮我们自动完成。如果我们希望自己命名实例变量命,那么我们就使用@synthesize显式声明我们希望的实例变量名 。

block作为实例变量为何要用copy修饰?

因为block如果在栈上的话,其所属的变量作用域结束,该block就被释放掉,block中的__block变量也同时被释放掉。为了解决栈块在其变量作用域结束之后被释放掉的问题,我们就需要把block复制到堆中。

为什么NSString、NSDictionary、NSArray要使用copy修饰?

对于NSString、NSDictionary、NSArray等经常使用copy关键字,是因为它们有对应的可变类型:NSMutableString、NSMutableDictionary、NSMutableArray,它们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性时拷贝一份。

weak、copy、strong、assgin分别用在什么地方?

什么情况下会使用weak关键字?

  • 在ARC中,出现循环引用的时候,会使用weak关键字。
  • 自身已经对它进行了一次强引用,没有必要再强调引用一次。

assgin适用于基本的数据类型,比如NSInteger、BOOL等。

NSString、NSArray、NSDictionary等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;

除了上面的三种情况,剩下的就使用strong来进行修饰。