KVC — Key Value Coding 键值编码

KVC 是一种不用通过调用 setter、getter 方法而是直接通过属性字符串名称key来存取属性的机制。

KVC 和 属性访问器的对比如下:

  • KVC是通过在运行时动态的访问和修改对象的属性而访问器是在编译时确定,单就这一点增加了访问属性的灵活性,
    但是用访问器访问属性的时候编译器会做预编译处理,访问不存在的属性编译器会报错,使用KVC方式的时候如果有错误只能在运行的时候才能发现。

  • 相比访问器KVC 效率会低一点。

  • KVC 可以访问对象的私有属性,修改只读属性

  • KVC 在字典转模型,集合类操作方面有着普通访问器不能提供的功能

总而言之 KVC的特点是在运行时访问,可以访问对象私有属性和修改只读属性,在字典转模型,集合类操作方面十分便捷。

首先先列下关键的一些方法,后续都会对这些方法进行介绍:

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

//默认返回YES,表示如果没有找到会按照一定的规则进行搜索,设置成NO如果没有找到就当作没有,会抛出异常。
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。但是注意这个方法是不会自动调用的需要我们手动调用。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值,前面一个是取值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

// 容器类型相关
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

// 传入key数组, 返回一个成员变量名和变量值的键值对组成的字典(可用于模型转字典)
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
// 字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

下面是KVC 关键用法的总结:

1. 设置和访问策略及访问私有属性和可读属性
#import <Foundation/Foundation.h>

@interface Teacher : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyValue;
@end

#import "Teacher.h"

@interface Teacher ()

@property (nonatomic, assign ,readwrite) NSInteger age;
@property (nonatomic, strong ,readwrite) NSString *name;
@property (nonatomic, assign ,readwrite) BOOL male;
@property (nonatomic, assign ,readwrite) BOOL isTest;

@end

@implementation Teacher

- (NSString *)description {
return [NSString stringWithFormat:@"name = %@ \
age = %ld \
readOnlyValue = %@ \
is Male = %d",
self.name, (long)self.age, self.readonlyValue, self.male];
}

int main(int argc, const char * argv[]) {
@autoreleasepool {

Teacher *teacher = [Teacher new];
[teacher setValue:@"linxiaohai" forKey:@"name"];
NSLog(@"%@",teacher);
[teacher setValue:@"linxiaohai1" forKey:@"_name"];
NSLog(@"%@",teacher);

//修改NSInteger要转换成NSNumber*
[teacher setValue:@(29) forKey:@"age"];
NSLog(@"%@",teacher);

//修改BOOL要转换成NSNumber*
[teacher setValue:@(YES) forKey:@"male"];
NSLog(@"%@",teacher);
[teacher setValue:@(NO) forKey:@"_male"];
NSLog(@"%@",teacher);

//修改只读属性
[teacher setValue:@"This is a readOnly Value" forKey:@"readonlyValue"];
NSLog(@"%@",teacher);
[teacher setValue:@"This is a readOnly Value 2" forKey:@"_readonlyValue"];
NSLog(@"%@",teacher);

//如果没有找到将会按照_is<Key> isKey 方式继续查找
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
[teacher setValue:@(YES) forKey:@"test"];
NSLog(@"%@",teacher);
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
}
return 0;
}

2019-06-21 11:27:36.860598+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 0 readOnlyValue = (null)   is Male = 0
2019-06-21 11:27:36.860744+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.860916+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.861087+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 1
2019-06-21 11:27:36.861213+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value is Male = 1
2019-06-21 11:27:36.861348+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.861422+0800 KVC-Demo[42518:5605320] male: 1
2019-06-21 11:27:36.877241+0800 KVC-Demo[42518:5605320] ===================================================
2019-06-21 11:27:36.877501+0800 KVC-Demo[42518:5605320] Test: 0
2019-06-21 11:27:36.877765+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.877824+0800 KVC-Demo[42518:5605320] Test: 1
2019-06-21 11:27:36.877867+0800 KVC-Demo[42518:5605320] ===================================================

  • -(void)setValue:(id)value forKey:(NSString *)key 规则:
首先搜索 setter 方法,有就直接赋值。
如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
返回 NO,则执行setValue:forUNdefinedKey:
返回 YES,则按_<key>,_<isKey><key><isKey>的顺序搜索[实例变量]注意这里是搜索实例变量,不是属性。
还没有找到的话,就调用setValue:forUndefinedKey:
总结来说就是KVC设值的顺序如下: setKey -> _key -> _isKey -> key -> isKey
  • -(id)valueForKey:(NSString *)key 规则
* 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
* 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
* 返回 NO,则执行valueForUNdefinedKey:
* 返回 YES,则按_<key>,_is<Key>,<key>,is<Key>的顺序搜索实例变量。
* 如果没有则查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法,如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合
* 还没找到则查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法,如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合。
* 还没有找到的话,调用valueForUndefinedKey:
总结来说就是KVC取值的顺序如下: getKey -> _key -> _isKey -> key -> isKey
-> [countOf<Key> objectIn<Key>AtIndex <Key>AtIndexes] -> [countOf<Key>,enumeratorOf<Key>,memberOf<Key>]

keyPath 用法:

Teacher *teacher = [Teacher new];
Student *student = [Student new];
[student setValue:@"Jimmy" forKey:@"name"];
[teacher setValue:student forKey:@"student"];
NSString *studentName = [teacher valueForKeyPath:@"student.name"];
NSLog(@"Student name: %@",studentName);
[teacher setValue:@"linxiaohai" forKeyPath:@"student.name"];
2. 异常处理

如果都没找到对应的key就会调用setValue:forUndefinedKey: 和valueForUndefinedKey:
下面是一个例子:

增加
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"undefineKey"]) {
NSLog(@"=========> %@",value);
}
}

在调用[teacher setValue:@"valueForUndefineKey" forKey:@"undefineKey"] 的时候就会打出如下log

=========> valueForUndefineKey


增加:
- (id)valueForUndefinedKey:(NSString *)key {
return [NSString stringWithFormat:@"This is A default Value for %@",key];
}

在调用
NSLog(@"Test: %@", [teacher valueForKey:@"undefineKey"]); 就会打出如下Log:

Test: This is A default Value for undefineKey

其他API

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回

// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
3. 字典转模型

使用字典来初始化模型

NSDictionary *catogiresDic = @{
@"id" :@1,
@"image":@"@www.iPhone.com",
@"name":@"iPhone",
@"price":@"100.03"};
CategoryList *catogires = [[CategoryList alloc] init];
[catogires setValuesForKeysWithDictionary:catogiresDic];

通过key的集合来从一个model中取出值形成一个字典

NSDictionary *value = [catogires dictionaryWithValuesForKeys:@[@"name",@"image"]];
4. 集合操作

KVC对于数组而言最大的功能还是获取集合类的 count,max,min,avg,sum 这是一个很好用的功能

集合操作的keyPath整体格式如下图所示:

左边是当前对象中待操作的集合实例对象,如果是针对自身的某个属性做操作,则可以省略,”@”操作符右边的是需要操作的属性。如果操作符是count的话右边也可以省略,这时候相当于针对当前数量统计。

  • 简单运算符
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count"]);
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count.price"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.@min.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@max.price"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.@sum.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@avg.price"]);

如果自身是一NSNumber数组的话右边的可以为self,表示针对的是自身

NSLog(@"count of book price : %@",[student valueForKeyPath:@"@count"]);
NSLog(@"count of book price : %@",[student valueForKeyPath:@"@count.self"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.self"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.self"]);
  • 对象运算符

通过这个特性能够一个对象集合的所有某个特定字端的值组成一个数组

一共有两种:

@distinctUnionOfObjects
@unionOfObjects

它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。例子如下:

NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
  • 数组集合运算符

这类操作符的操作对象内对象必须是数组/集合,也就是元素为数组/集合的数组/集合:下面是一个简单例子:

IDLProductModel *productA = [[IDLProductModel alloc] init];
productA.price = 99.0;
productA.name = @"iPod";

IDLProductModel *productB = [[IDLProductModel alloc] init];
productB.price = 199000.0;
productB.name = @"iMac";

IDLProductModel *productC = [[IDLProductModel alloc] init];
productC.price = 2990.0;
productC.name = @"iPhone";

IDLProductModel *productD = [[IDLProductModel alloc] init];
productD.price = 1990.0;
productD.name = @"iPhone";

NSArray *product = @[productA, productB, productC, productD];
NSArray *distinctUnionOfArrays = [@[product, product] valueForKeyPath:@"@distinctUnionOfArrays.price"];
NSArray *unionOfArrays = [@[product, product] valueForKeyPath:@"@unionOfArrays.price"];

NSSet *setA = [NSSet setWithObjects:productA, productB, nil];
NSSet *setB = [NSSet setWithObjects:productC, productD, nil];
NSSet *set = [NSSet setWithObjects:setA, setB, nil];

NSSet *allSet = [set valueForKeyPath:@"@distinctUnionOfSets.name"];

这种其实就是将操作对象的所有数据取出来,展开铺平为一个数组后,再对数组或者集合做操作。

5. 键值验证

KVC提供了验证Key对应的Value是否可用的方法,但是这个方法不会自动调用,必须在使用的时候手动调用:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

如下例子:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue == 10) {
return NO;
}
return YES;
}

NSNumber *age = @10;
NSError* error;
NSString *key = @"age";
BOOL isValid = [test validateValue:&age forKey:key error:&error];

6.其他用途
  • 指定数组中每个元素的执行方法,然后将新元素放到新数组中返回
NSArray *arr = @[@"iPod", @"iPhone", @"iMac", @"iPhone8 Plus"];
NSArray *uppercaseStrArr = [arr valueForKeyPath:@"uppercaseString"];
  • 抽取字典数组中的指定key
NSArray *array = @[@{@"name" : @"iPod",   @"price" : @99 },
@{@"name" : @"iPhone", @"price" : @199},
@{@"name" : @"iPhone", @"price" : @299},
@{@"name" : @"iPhone", @"price" : @299},
];
NSLog(@"%@", [array valueForKeyPath:@"name"]);//iPod,iPhone,iPhone,iPhone
KVO — Key Value Observer 键值观察
1. KVO 的基本使用

KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应

为某个对象的属性添加观察者:

[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior
context:nil];

回调方法

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"%@对象的%@属性改变了:%@",object,keyPath,change);
}

移除观察者

[self.person removeObserver:self forKeyPath:@"age"];

这里比较重要的是option这个参数:

NSKeyValueObservingOptionNew = 0x01 change字典包括改变后的值
NSKeyValueObservingOptionOld = 0x02 change字典包括改变前的值
NSKeyValueObservingOptionInitial = 0x04 注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior = 0x08
如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,
这与-willChangeValueForKey:被触发的时间是相对应的。
这样,在每次修改属性时,实际上是会发送两条通知。
2. KVO 触发规则:
  • 通过成员变量的setter方法/或者通过KVC都可以触发KVO
  • 直接修改成员变量是不会触发KVC的,需要手动添加KVC才能触发。如何手动触发见下面部分。
3. 设置相互关联的属性

假如有个 Person 类,类里有三个属性,fullName、firstName、lastName。这种情况如果需要观察名字的变化,就要分别添加 fullName、firstName、lastName 三次观察,非常麻烦。通过设置相互关联的属性就会在关联的属性发生变化的时候,另外的属性也受到通知。

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *fullName;

@end

@implementation Person

//在这里设置与fullName相关的依赖属性,外面一旦设置了这些依赖属性,fullName的观察者就会被触发
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

@end

[_person addObserver:self
forKeyPath:@"fullName"
options:NSKeyValueObservingOptionNew
context:nil];

[_person setValue:@"FistName" forKey:@"fistName"];

上面的例子中我们不论设置Person类的firstName或者lastName 都会触发 fullName 的观察。

4. 手动KVO (禁用自动KVO)
@interface Target : NSObject {
int age;
}

- (int)age;
- (void)setAge:(int)theAge;

@end

@implementation Target

- (id)init {
self = [super init];
if (nil != self) {
age = 10;
}
return self;
}

- (int)age {
return age;
}

- (void)setAge:(int)theAge {
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}

@end
  1. 需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法
  2. 实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
5. 查看某个对象有哪些属性被监听

如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性

id info = object.observationInfo;
NSLog(@"%@", [info description]);
6.KVO 原理:
KVO 是通过 isa-swizzling 实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中做如下的几方面工作:

1. 重写基类中任何被观察属性的 setter 方法,而通过重写就获得了 KVO 需要的通知机制

- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; // KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO在调用存取方法之后总调用
}

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。

2. 让这个重写的类成为原来类的派生类:

除了完成上面提到的第一步操作的同时派生类还重写了class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制

3. 重写派生类的dealloc 方法释放资源

扩展阅读

*KVO进阶——KVO实现探究

**** 一.概述 ****

一般来讲,开一个线程执行某项任务,在任务执行完成后线程就会退出。如果我们需要让线程能不退出一直常驻,随时处理事件这就需要消息循环来实现了。
Runloop 是 iOS中的消息循环,Android也有Looper/Handler一套消息循环机制,这到底是啥,以前玩过单片机的小伙伴肯定记得很清楚,我们在配置完硬件,对各个部件进行初始化完毕后,都会在main中执行一个死循环,然后等待硬件中断,软件中断,在对应的中断函数中处理外部事件,其实这个死循环就是我们今天要介绍的Runloop,它会保持整个应用进程处于存活状态,然后在每个消息循环中处理各个事件。Runloop是我们平常看不见摸不着,但是又比不可少的一个系统组件。

Runloop 的思想可以用下面的伪代码来表示:

int main(void) {
初始化();
while (不满足退出条件) {
休眠前处理待处理事件(message);
睡眠ZZzzzz... 等待被事件唤醒
message = 获取需要处理的事件();
处理事件(message)
}
return 0;
}

依赖NSRunloop的类和框架

NSTimer
UIEvent
autorelease
NSObject(NSDelaydPerforming)
NSObject(NSThreadPerformAddtion)
CADisplayLink
CATransition
CAAnimation
dispatch_get_main_queue()
**** 二.RunLoop 需要注意的点:****
  • CFRunLoopRef基于C线程安全,NSRunLoop基于CFRunLoopRef面向对象的API是不安全的.

  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中,

  • 主线程的 Runloop 会在应用启动的时候启动,其他线程的 Runloop需要我们手动创建并启动,RunLoop在第一次获取时创建,在线程结束时销毁,我们只能在一个线程的内部获取其RunLoop,并且苹果系统不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:

    CFRunLoopRef CFRunLoopGetCurrent(void)
    CFRunLoopRef CFRunLoopGetMain(void)
    +(NSRunLoop *)currentRunLoop
    +(NSRunLoop *)mainRunLoop
  • Runloop Mode是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer隔绝开。Runloop 在某个时刻只能跑在一个Mode下,处理这一个Mode 当中的 Source,Timer 和 Observer,这也是为啥在滑动屏幕的时候有时候定时器会失效的原因,这个会在后续介绍。

  • Source0是内部事件 UIEvent、CFSocket都是source0,它区别于source1,它不是基于Port的,它不能主动触发事件。我们需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop处理这个事件。

  • Source1由RunLoop和内核管理,source1带有mach_port_t和一个回调,可以接收内核消息并触发回调,它能主动唤醒 RunLoop 的线程。

  • source0 有公开的 API 可供开发者调用,source1 却只能供系统使用

  • Timmer 包含一个时间长度和一个回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
    NSTimer的创建通常有两种方式,一种是以timer开头,另一种是以schedued开头,第一种是没有添加到Runloop中的,需要我们自己去手动调用代码添加,第二种会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中。

  • 一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环

三. RunLoop 关键源码解析:
3.1 RunLoop 关键数据结构
  • __CFRunLoop
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; // 用来锁对于 mode 列表的访问
__CFPort _wakeUpPort; // 用来唤醒 run loop 的 mach port
Boolean _unused;
volatile _per_run_data *_perRunData;
pthread_t _pthread; //对应的 pthread
uint32_t _winthread; //Windows 下对应线程
CFMutableSetRef _commonModes; //[存放 common mode 的集合]
CFMutableSetRef _commonModeItems; //[每个 common mode 都有的 item (source, timer and observer) 集合]
CFRunLoopModeRef _currentMode; //当前 run 的 mode
CFMutableSetRef _modes; //这个 run loop 所有的 mode 集合
struct _block_item *_blocks_head; //存放 CFRunLoopPerformBlock 函数添加的 block 的双向链表的头指针
struct _block_item *_blocks_tail; //存放 CFRunLoopPerformBlock 函数添加的 block 的双向链表的尾指针
CFAbsoluteTime _runTime; //总共的运行时间
CFAbsoluteTime _sleepTime; //总共的睡眠时间
CFTypeRef _counterpart;
};

每个__CFRunLoop 会存储有:

  • 当前RunLoop所对应的线程****_pthread****

  • 当前RunLoop所能运行的所有模式****_modes以及它的锁_lock****

  • 当前RunLoop当前运行模式****_modes****

  • 当前RunLoop当前运行模式****_currentMode****

  • 当前RunLoop的公用模式信息****_commonModes_commonModeItems****

  • 当前RunLoop唤醒端口****_wakeUpPort****

  • 添加到当前Runloop的Block信息****_blocks_head,_blocks_tail****

  • 当前RunLoop相关的统计信息****_runTime,_sleepTime****

  • __CFRunLoopMode

struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* 一个mutex,锁mode里的各种操作。根据注释,需要runloop 的锁先锁上才能锁这个锁。 */
CFStringRef _name; /* 模式名称 */
Boolean _stopped; //是否停止了
char _padding[3];
CFMutableSetRef _sources0; //Source0 集合,也就是非 port 的 source
CFMutableSetRef _sources1; //Source1 集合,也就是基于 port 的 source
CFMutableArrayRef _observers; //Observer 集合
CFMutableArrayRef _timers; //Timer 集合
CFMutableDictionaryRef _portToV1SourceMap; //Key 是 port,value 是对应 source1 的字典
__CFPortSet _portSet; //所有 port 的集合
CFIndex _observerMask; //需要 observe 的事件的 mask
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource; //用来实现 timer 的 GCD timer
dispatch_queue_t _queue; //放 _timerSource 的队列
Boolean _timerFired; // _timerSource 是否被启动
Boolean _dispatchTimerArmed; //timer是否开启了
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort; //使用 MK timer 时的端口
Boolean _mkTimerArmed; //timer 是否被开启
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* //下一个计划启动的时间 */
uint64_t _timerHardDeadline; /* 下一个最迟启动的时间(计划加上容忍延迟的时间)*/
};

__CFRunLoopMode 里面最关键的元素包括:****_sources0****,_sources1,_observers,_timers,_portSet

iOS 系统中定义了如下几种Mode:

  • NSDefaultRunLoopMode:
    NSDefaultRunLoopMode是Runloop的默认的运行模式,主线程在启动的时候就是运行在这个模式下的,NSTimer与NSURLConnection默认运行该模式下。
  • UITrackingRunLoopMode:在触摸滑动界面的时候会切换到这个模式用于保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • NSRunLoopCommonModes:这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode
  • __CFRunLoopSource
    struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order; /* immutable */
    CFMutableBagRef _runLoops;
    union {
    CFRunLoopSourceContext version0; /* immutable, except invalidation */
    CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
    };

Source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,标记信息位于****_bits****

  • __CFRunLoopObserver
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};

__CFRunLoopObserver 里面最关键的字段是****_callout****,它会在通知发出的时候被回调。

  • __CFRunLoopTimer
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

__CFRunLoopTimer 里面主要包含了触发的时机,以及对应的回调方法。

在针对RunLoop源码进行解析的时候大家可以针对下面这张图进行理解。下面的解析也是围绕者这张图来分析的:

3.2 RunLoop 起点

前面提到除了主线程外,其他我们自己创建的线程是没有RunLoop的,我们可以通过currentRunLoop来创建,而主线程的RunLoop是通过mainRunLoop创建的,最终会调用CFRunLoopRef 下的CFRunLoopGetCurrentCFRunLoopGetMain

CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

上面的这两个方法都会归到一处入口,****_CFRunLoopGet0****传入的是当前所在的线程:

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//检查当前传入的线程对象,如果传入的不合法,就使用主线程替代t
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
//如果存储RunLoop的字典不存在
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//创建主线程的RunLoop
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//创建一个主线程的runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//将主线程 以及 主线程runloop 加入到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
//第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
}
//通过传入的线程去获取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//如果字典中没有则创建新的一个runloop后加入字典
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
//如果传入线程就是当前线程
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
//注册一个回调,当线程销毁时,销毁对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

只要是第一次调用****_CFRunLoopGet0,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建,创建出来的RunLoop会被添加到__CFRunLoops这个全局字典里面。这个字典以线程指针作为key,Runloop作为value进行存储的,后续创建的一系列的Runloop都会添加到这里。每次调用_CFRunLoopGet会先从这里看下是否有已经创建好的RunLoop如果有就直接返回,如果没有则新建一个,然后存到__CFRunLoops****作为缓存。在最后的时候会判断如果传入线程就是当前线程,则会注册一个回调,当线程销毁时,销毁对应的RunLoop。

3.3 RunLoop 的创建
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}

这里没啥亮点。我们继续看下一步:

3.4 启动RunLoop

在什么都没有指定的时候,CFRunLoopRun默认是跑在kCFRunLoopDefaultMode模式下的

void CFRunLoopRun(void) {	/* DOES CALLOUT */
int32_t result;
do {
//运行当前线程的Runloop在默认模式
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
//CFRunLoopRun 函数不主动调用 CFRunLoopStop 函数(kCFRunLoopRunStopped 的情况)或者将所有事件源移除(kCFRunLoopRunFinished 的情况)是没有办法退出的
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

后续的各种run最终的入口都是归结到CFRunLoopRunSpecific

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
CHECK_FOR_FORK();
// 检查 run loop 是否正在销毁,如果销毁返回kCFRunLoopRunFinished,表示Runloop运行结束
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根据modeName找到本次运行的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// 没有找到 mode 或者 mode 里面没有任何事件源的话,返回 kCFRunLoopRunFinished
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// 因为可以嵌套调用,所以在进入当前Mode之前保存一下之前的状态
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;

// 通知 observers: kCFRunLoopEntry, 进入 run loop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 observers: `kCFRunLoopExit`, 退出 run loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

// 恢复之前的状态
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}

在CFRunLoopRunSpecific中会通过传入的modeName,找到对应的CFRunLoopModeRef,并将其传入****__CFRunLoopRun作为当前RunLoop要跑的模式。进入和退出__CFRunLoopRun会发出kCFRunLoopEntry,和kCFRunLoopExit****通知外部的Observer Runloop将要开始,以及Runloop已经退出。

__CFRunLoopRun方法有点长,我省略了一些不必要的代码:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//.....
do {
//......

// 通知 observers: kCFRunLoopBeforeTimers, 即将处理 timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 observers: kCFRunLoopBeforeSources, 即将处理 sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
// 处理 sources 0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 如果实际处理了 sources 0,再一次处理blocks
__CFRunLoopDoBlocks(rl, rlm);
}
//.....
// 通知 observers: kCFRunLoopBeforeWaiting, 即将进入等待(睡眠)
// 注意到如果实际处理了 source 0 或者超时了,不会进入睡眠,所以不会通知
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 设置标志位,正在睡眠
__CFRunLoopSetSleeping(rl);

__CFRunLoopSetIgnoreWakeUps(rl);

// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);

// 通知 observers: kCFRunLoopAfterWaiting, 即停止等待(被唤醒)
// 注意实际处理过 source 0 或者已经超时的话,不会通知(因为没有睡)
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 被什么唤醒就处理什么:
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);

//// 不知道哪个端口唤醒的(或者根本没睡),啥也不干
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//// 被 CFRunLoopWakeUp 函数弄醒的,啥也不干
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被 GCD 唤醒处理 GCD
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
//.....
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
//....
} else {
/// 被 sources 1 唤醒,处理 sources 1
CFRUNLOOP_WAKEUP_FOR_SOURCE();
//.......
}
//....
} while (0 == retVal);
//....
return retVal;
}

大体流程大家可以结合代码注释看下图中所示。

  1. 通知将要处理Timmer事件
  2. 通知将要处理Source事件
  3. 处理Source0事件,在处理Source0事件之前都会调用****__CFRunLoopDoBlocks****处理下block。
  4. 通知将要进入睡眠状态
  5. Runloop进入睡眠状态
  6. 当有事件源给Runloop发送消息,表示有事件处理的时候,就会唤醒Runloop。
  7. 唤醒Runloop。
  8. 通知Runloop已经唤醒。
  9. 根据事件源来调用不同的方法来执行处理,这里的事件源可以是Timmer,Source1,GCD事件。
  10. 继续上面循环。
四 RunLoop 应用相关

  • AutoreleasePool
    RunLoop的进入的时候会调用objc_autoreleasePoolPush()创建新的自动释放池。
    RunLoop的进入休眠的时候会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 销毁自动释放池,创建一个新的自动释放池。
    RunLoop即将退出时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。

  • GCD

当调用了dispatch_async(dispatch_get_main_queue())时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。需要注意的是dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。

  • 支持异步方法调用

我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中

  • NSTimer, GCD Timer,CADisplayLink

NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。而GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。CADisplayLink是一个执行频率和屏幕刷新相同的定时器,它也需要加入到RunLoop才能执行,通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅。

子线程启动定时器需要按照如下方式启动Runloop

dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
});

  • 滑动屏幕导致定时器失效

一般而言

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

等效于

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];

当我们滚动ScrollView或者滑动屏幕的时候,RunLoop会切换到UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式的RunLoop,所以导致defaultMode下的定时器失效,所以这种情况的解决方式就是将定时器放到commonMode中,这样即使切换到UITrackingRunLoopMode也会被触发。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

或者使用GCD定时器,它不会受RunLoop的影响:

// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();

// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 109次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);

// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
});

// 启动定时器
dispatch_resume(self.timer);
  • 触摸事件

当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,

  • 屏幕刷新

在我们修改了View frame、或者调整了UI层级,或者手动设置了setNeedsDisplay/setNeedsLayout之后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去,_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 这个Observer会监听RunLoop即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。facebook推出的AsyncDisplayKit的机制和它也类似,它将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程,在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务更新界面。

  • 常驻线程

我们有时候需要创建一个在后台一直存在的程序,来做一些需要频繁处理的任务

- (void)run {
@autoreleasepool{
/*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
下面的方法给runloop添加一个NSPort,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

[[NSRunLoop currentRunLoop] run];
}
}

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

AFNetworking 就是通过这种方式接收 Delegate 回调


+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 这里主要是监听某个 port,目的是让RunLoop不会退出,确保该 Thread 不会被回收
[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状态
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);

Block源码地址

1. Block实质:

Block从C语言角度实质上是能够捕获上下文变量的匿名函数,在创建的时候会捕获所需要的上下文局部自动变量到闭包内部。Block的底层是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。Block和__block 最终都会转换为一个C语言的结构体对象。

Block 在OC中的实现如下

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

Block 需要了解如下几个方面:

(1) 我们怎么把带有Block的 Objective C 代码转化为 C 代码
(2) 转换后的代码结构是怎样的
(3) Block对局部自动变量,局部静态变量,全局变量,对象,__block变量的捕获
(4) Block 与 __block 的存储特性
(5) Block的循环引用
(6) Block的内存布局

我们使用的转换block的命令行如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

一个简单常见的例子:

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic) int testProperty;
@end

@implementation Person
@end

int globleIntValue = 2;
static int staticGlobleIntValue = 5;
int main(int argc, char * argv[]) {
int intValue = 5;
static int staticIntValue = 3;
__block int blockInt = 5;
Person *person = [Person new];
void (^blockTest)(int) = ^ (int value){
printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",staticIntValue);
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",person.testProperty);
blockInt = 34;
};
blockTest(2222);
return 0;
}

转换后的代码:

int globleIntValue = 2;
static int staticGlobleIntValue = 5;

struct __Block_byref_blockInt_0 {
void *__isa;
__Block_byref_blockInt_0 *__forwarding;
int __flags;
int __size;
int blockInt;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intValue;
int *staticIntValue;
Person *person;
__Block_byref_blockInt_0 *blockInt; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int *_staticIntValue, Person *_person, __Block_byref_blockInt_0 *_blockInt, int flags=0) : intValue(_intValue), staticIntValue(_staticIntValue), person(_person), blockInt(_blockInt->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int value) {
__Block_byref_blockInt_0 *blockInt = __cself->blockInt; // bound by ref
int intValue = __cself->intValue; // bound by copy
int *staticIntValue = __cself->staticIntValue; // bound by copy
Person *person = __cself->person; // bound by copy

printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",(*staticIntValue));
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("testProperty")));
(blockInt->__forwarding->blockInt) = 34;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->blockInt, (void*)src->blockInt, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->blockInt, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {

int intValue = 5;
static int staticIntValue = 3;

__attribute__((__blocks__(byref))) __Block_byref_blockInt_0 blockInt = {(void*)0,(__Block_byref_blockInt_0 *)&blockInt, 0, sizeof(__Block_byref_blockInt_0), 5};

Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));

void (*blockTest)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue, &staticIntValue, person, (__Block_byref_blockInt_0 *)&blockInt, 570425344));
((void (*)(__block_impl *, int))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest, 2222);

return 0;
}

2. Block定义

Block类型变量,一般结合用typedef定义:

typedef int (^blockType)(int,int)
@property (nonatomic, asign, readonly) blockType block;

Block 变量可以作为自动变量,函数参数,函数返回值,静态局部变量,静态全局变量,全局变量
Block 定义:
block定义和普通的C语言函数定义类似,只不过多了一个^省去函数名称

^返回值类型 (参数列表) {
表达式
}

3. __block_impl 结构体

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
void (^blk)(void) = ^ {

}

转换成:

void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,auto_peram1,auto_peram2);

blk() 实际上执行的是

(*blk->impl.FuncPtr)(blk);

一个block的组成:

struct __block_impl impl;
struct __main_block_desc_0 *Desc;
---->
捕获的自动变量和局部静态变量指针

4. Block的变量捕获

  • block接受的参数属于值传递,可以在block内修改,不对其进行捕获

  • 局部变量的捕获: 当定义block的时候,Block中所使用的自动变量值被保存到Block的结构体实例中,也就是Block自身中,并且保存后就不能改写该值。但是还是可以调用变更该对象的方法。比如NSMutableArray 的 addObject

  • static局部变量的捕获: 局部静态变量 作用域在block之外,这种情况下block中存储的是静态局部变量的地址,所以当FuncPtr指向的函数调用的时候会通过取地址中存储的值

  • 全局变量的捕获
    全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取全局变量的值使用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。

  • 对象类型的捕获:
    在没有调用copy的情况下,还没有调用block()的时候对象就已经释放了,说明在栈上的block并没有对所使用的对象强引用,对block 进行一次copy操作会发现在block没有release之前,所引用的对象没有被释放,所以堆上的block强引用了所使用的对象,对对象执行一次release之后,对象的引用计数依然没有成为0,因为block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的__main_block_copy_0方法。__main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。(栈上block访问了对象类型的auto变量的时候不会对其发生强引用)

block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。

简而言之:
局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。当Block从栈复制到堆上时,block会对id类型的变量产生强引用

5. __block 说明符

在block中允许修改静态局部变量,静态全局变量和全局变量这几种类型,但是对于局部自动变量如果在block里面修改编译器会发出警告,这时候需要在局部自动变量之前添加__block 说明符,__block 可以指定任何类型的自动变量,通过__block修饰的变量将会变成一个结构体实例,只有这样这个值才能被block共享、并且不受栈帧生命周期的限制、在block被copy后,能够随着block复制到堆上

这时候被捕获的对象会转换为

struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 * __forwarding;
int __flags;
int __size;
// 这后面是__block变量
int val;
}

__main_block_impl_0中也会多出一个

__Block_byref_val_0 *val;

也就是说局部自动变量添加了__block后该变量将会以__Block_byref_val_0 形式被捕获

有时候__block变量配置在堆上的情况下,也可以访问栈上的__block变量,在这种情况下只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从堆上的__block变量还是从栈上的__block变量都能正确得访问。简而言之:
__block 变量结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是在堆上都能正确得访问__block变量


__block id __strong obj = [[NSObject alloc] init]; 和 id __strong obj = [[NSObject alloc] init] 一样在block没有copy的时候是不持有的,copy后block会持有对象引用

id array = [[NSMutableArray alloc] init];
id __weak array2 = array;

array2 是弱引用,当变量作用域结束,array 所指向的对象内存被释放,array2 指向 nil

如果 __weak 再改成 __unsafe_unretained ,__unsafe_unretained 修饰的对象变量指针就相当于一个普通指针。使用这个修饰符有点需要注意的地方是,当指针所指向的对象内存被释放时,指针变量不会被置为 nil。所以当使用这个修饰符时,一定要注意不要通过悬挂指针(指向被废弃内存的指针)来访问已经被废弃的对象内存,否则程序就会崩溃。

如果 __unsafe_unretained 再改成 __autoreleasing 会怎样呢?会报错,编译器并不允许你这么干!如果你这么写
__block id __autoreleasing obj = [[NSObject alloc] init];
编译器就会报下面的错误,意思就是 __block 和 __autoreleasing 不能同时使用。

6. Block 存储属性

Block 有如下几种存储类型:

_NSConcreteGlobalBlock 程序的数据区域

  • Block当作全局变量使用时(block 字面量写在全局作用域时)
  • 当 block 字面量不获取任何外部变量时(只使用全局变量以及block参数的时候)
    _NSConcreteGlobalBlock不持有对象

_NSConcreteStackBlock 栈

  • 除了上述两中情况下Block配置在程序的数据区中以外(换种说法如果只用到外部局部变量、成员属性变量,且没有强指针引用的block就是StackBlock),Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。配置在栈上的Block,如果其所属的变量作用域结束,该Block就被自动废弃。_NSConcreteStackBlock不持有对象

_NSConcreteMallocBlock 堆

  • 有强指针引用或copy修饰的成员属性引用的block,配置在全局变量上的Block,从变量作用域外也可以通过指针访问。但是设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;并且__block变量的也是配置在栈上的,如果其所属的变量作用域结束,则该__block变量也会被废弃。那么这时需要将Block和__block变量复制到堆上,才能让其不受变量域作用结束的影响。_NSConcreteMallocBlock持有对象。

整个存储区域如下图所示:

通过上述结果我们可以看出当block访问了auto变量的时候会变成__NSStackBlock__类型。而其他情况下是__NSGlobalBlock__类型,比较典型的是Block声明在全局区域,以及Block虽然不声明在全局区域但是Block不截获自动变量。
__NSGlobalBlock__类型存在于数据区,__NSStackBlock__存在于栈区。

而在ARC环境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__类型,而原本是__NSStackBlock__却变成了__NSMallocBlock__存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法这时候需要手动调用copy,但是比如Cocoa框架的方法且方法名中包含usingBlock等,或者GCD API的情况下,这些在方法或者函数中对传递过来的参数做了复制操作所以不需要copy

copy 的结果:

如果原先存储域处于_NSConcreteGlobalBlock 那么什么都不做
如果原先存储域处于 _NSConcreteMallocBlock 那么将会导致引用计数增加
如果原先存储域处于_NSConcreteStackBlock 那么会将block从栈复制到堆

下面是Block复制和释放的源码:

Block 复制

 void *_Block_copy(const void *arg) {

//1. 声明一个Block_layout结构体类型的指针,如果传入的Block为NULL就直接返回。
struct Block_layout *aBlock;
if (!arg) return NULL;

// 如果Block有值就强转成Block_layout的指针类型。
aBlock = (struct Block_layout *)arg;
//如果Block的flags表明该Block为堆Block
if (aBlock->flags & BLOCK_NEEDS_FREE) {
//对block的引用计数递增后返回Block
latching_incr_int(&aBlock->flags);
return aBlock;
}
//如果Block为全局Block就不做其他处理直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 如果是堆栈类型就对block执行一次copy
else {
//分配空间,这里的控件大小为descriptor->size
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;

//将Block从栈上复制到堆上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
//将新Block的引用计数置零。
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
//将堆Block的isa指针置为_NSConcreteMallocBlock,返回新Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}

Block 释放

void _Block_release(const void *arg) {

struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;

//如果是Globle类型的 Block 就不做任何操作
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//如果Block flags 标志位指示 block 不需要释放 就直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

//判断aBlock的引用计数是否需要释放内存
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//释放block
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}

简而言之:
访问了auto变量的block是__NSStackBlock__类型,没有访问auto变量的block是__NSGlobalBlock__类型。而对__NSStackBlock__类型进行copy操作就会变为__NSMallocBlock__类型。

在如下情况下栈上的block会被复制到堆上:

在调用Block 的copy 实例方法的时候
Block 作为函数返回值返回时
将Block赋给带有__strong修饰符id 类型的类或者Block类型成员变量时(block作为强指针引用的时候也会自动调用copy)
在方法名中含有usingBlock的Cocoa框架方法,或者GCD API中传递Block时候

在谁都不持有Block的时候block将会被释放

copy 函数会持有截获的对象,以及所使用的__block变量,dispose函数会释放截获的对象以及__block变量,
所以Block中使用的赋给赋有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。

再简而言之:也就是block 被copy到堆上的时候,它所使用的strong类型的对象以及__block变量都会超出作用域而存在。

  1. __block 变量存储属性

当Block从栈复制到堆上的时候,它所使用的所有__block变量也会被复制到堆上,并被Block持有。在多个Block中使用__block变量的时候,因为最先会将所有的Block配置在栈上,所以__block变量最初也会配置到栈上,在任何一个Block从栈上复制到堆上的时候,__block变量也会一起从栈复制到堆上,并被该Block 持有,当剩下的Block从栈复制到堆的时候,被复制的Block持有__block变量,并增加__block变量的引用计数。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也会被释放。





  1. 避免循环引用:

如果用self引用了block,block又捕获了self,这样就会有循环引用。因此,需要用weak来声明self,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,同样也要避免。

使用__weak来声明self
- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"%@", weakSelf.name);
};
}
return self;
}

- (void)configureBlock {
id tmpIvar = _ivar; //临时变量,避免了self引用
self.block = ^{
[tmpIvar msg];
}
}

当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。