GCD
本篇内容
1. 死锁
2. 异步与同步概念
3. 串行与并发概念(重点)
4. dispatch_group
5. dispatch_barrier_async
6. 信号量
6. dispatch_source(重点)
简介
在单核的CPU里,采用分时的执行,来回在不同线程之间进行切换。在多核的CPU里可以同时多个线程,也可以切换线程。这样就实现多线程。
多线程带来的问题
数据竞争:多个线程更新相同的资源
死锁:线程之间相互等待
消耗大量内存:太多线程会消耗大量内存
死锁
在GCD中,以同步分发的方式非常容易出现死锁。死锁的本质资源的相互等待。异步调用不会产生死锁。
dispatch_queue_t queueA;
dispatch_sync(queueA, ^{
dispatch_sync(queueA, ^{
[self foo];
});
});
/*
一旦进入第二个dispatch_sync就会死锁
它们两个在同一个线程里执行外面的正在执行,第二个会等待外面的执行完,而第二个永远都不会执行完。
*/
队列
同步队列只是在执行任务时,顺序的从对列里去任务。在一个任务没有完时不会执行下一个任务。
并发队列是执行任务时,在一个任务没有执行完,可以去拿另一个任务去找线程执行。
异步
异步操作有开启线程的权利。
同步操作没有开启线程的权利。
不同执行方式在不同的队列里执行
同步异步在并发串行队列里的四种组合
同步与异步决定是否开启线程
串行与并发决定拿任务方式
任务时需要线程去执行的。也就时需要考虑,有线程没任务,有任务没线程去执行的情况。
- 同步执行 串行队列 因为是同步,不会从线程池里拿线程执行,会在当前线程里执行下一个任务(要在当前任务执行完后)。即若果在主线程里同步执行任务。会出现死锁。
/*
死锁
*/
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]);
});
- (void)gcdDemo1 {
//1.创建串行队列
dispatch_queue_t q = dispatch_queue_create("demo1_seral", DISPATCH_QUEUE_SERIAL);
//2.同步执行任务
for (int i =0; i< 20; i++) {
dispatch_sync(q, ^{
NSLog(@"demo1 %@",[NSThread currentThread]);
});
}
}
- 同步执行 并发队列 因为是同步,不会开启线程,因为是并发队列,拿任务可以并发,但没有线程执行,所有还是一个一个执行
//1.创建串行队列
dispatch_queue_t q = dispatch_queue_create("demo4_seral", DISPATCH_QUEUE_CONCURRENT);
//2.同步执行任务
for (int i =0; i< 20; i++) {
dispatch_sync(q, ^{
NSLog(@"demo3+%@, %@",@(i),[NSThread currentThread]);
});
}
NSLog(@"come here");
- 异步执行 串行队列 因为是异步,可以去线程池里拿线程执行。当前是串行队列,要等一个任务执行完后才去拿下一个任务。所以还是一个一个执行
- (void)gcdDemo2 {
//1.创建串行队列
dispatch_queue_t q = dispatch_queue_create("demo2_seral", DISPATCH_QUEUE_SERIAL);
//2.同步执行任务
for (int i =0; i< 20; i++) {
dispatch_async(q, ^{
NSLog(@"demo2+%@, %@",@(i),[NSThread currentThread]);
});
}
NSLog(@"come here");
}
- 异步执行 并发队列 因为是异步,可以去线程池中拿线程执行。当前是并发队列。当前任务没有执行完可以去拿下一个任务执行,因为是并发,可以从线程池里拿新的线程去执行这个任务。这时就真正实现了幷发执行。
- (void)gcdDemo3 {
//1.创建串行队列
dispatch_queue_t q = dispatch_queue_create("demo3_seral", DISPATCH_QUEUE_CONCURRENT);
//2.同步执行任务
for (int i =0; i< 20; i++) {
dispatch_async(q, ^{
NSLog(@"demo3+%@, %@",@(i),[NSThread currentThread]);
});
}
NSLog(@"come here");
}
- 应用(如果将这个例子理解了,就真正理解的同步异步执行与串行并发对列) 指定一个同步任务,让所有异步任务等待同步任务执行完后才执行。 解决方法: 在一个并发队列里先执行加入这个同步任务,在加入后面两个异步任务。因为是同步任务在前,不会有开启多余的线程去执行后面的任务。当第一个执行完,执行到第二个时候,是异步的,可以开启线程执行其他的,因为是并发队列,第三个任务有线程可以用来执行。
- (void)gcdDemo6 {
dispatch_queue_t loginQueue = dispatch_queue_create("rao-login-queue", DISPATCH_QUEUE_CONCURRENT);
void (^task)(void) = ^(){
dispatch_sync(loginQueue, ^{
NSLog(@"用户登录了%@",[NSThread currentThread]);
});
dispatch_async(loginQueue, ^{
NSLog(@"用户支付了%@",[NSThread currentThread]);
});
dispatch_async(loginQueue, ^{
NSLog(@"用户下载了%@",[NSThread currentThread]);
});
};
dispatch_async(loginQueue, task);
}
dispatch group
使用场景:在一个对列里并发执行完后,想执行一个操作,就可以用dispatch_group
dispatch_queue_t queue = dispatch_queue_create("rao-queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"down load1%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"down load2%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"down load3%@",[NSThread currentThread]);
});
//当前面的所有
dispatch_group_notify(group, queue, ^{
NSLog(@"所有的都执行完%@",[NSThread currentThread]);
});
//在主队列里进行更新
dispatch_group_notify(group,dispatch_get_main_queue(), ^{
NSLog(@"所有的都执行完%@",[NSThread currentThread]);
});
dispatch_barrier_async
dispatch_barrier_async等待之前追加的任务执行完后,就会执行这个任务,并且不会执行下一个任务,要等这个任务执行完后,才会并发执行下一个任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("label", DISPATCH_QUEUE_CONCURRENT);
static NSInteger readCount = 0;
void(^read)(void) = ^() {
NSLog(@"这是read%@",@(readCount));
};
void(^write)(void) = ^(){
readCount++;
NSLog(@"这是write%@",@(readCount));
};
dispatch_async(queue, read);
dispatch_async(queue, read);
dispatch_async(queue, read);
dispatch_async(queue, read);
dispatch_barrier_async(queue, write);
dispatch_async(queue, read);
dispatch_async(queue, read);
}
信号量
NSMutableArray是线程不安全的,当有多个线程同时对数组进行操作的时候可能导致崩溃或数据错误。这里采用的就时信号量,所谓信号量,可以理解成一个数,占有空间时+1,走开始-1;
- (void)viewDidLoad {
[super viewDidLoad];
//并发写入数据
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);//创建信号量
NSMutableArray *array = [[NSMutableArray alloc]init];
for (int i = 0 ; i<1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//减少信号量
[array addObject:@(i)];
});
dispatch_semaphore_signal(semaphore);//提高信号量
}
锁
锁的部分参考深入理解iOS开发中的锁
本图来自ibireme
- OSSpinLock
自旋锁的实现原理比较简单,就是死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。如果a线程在临界区的执行时间过长,则b线程会消耗大量的cpu时间,不太划算。所以,自旋锁用在临界区执行时间比较短的环境性能会很高。
dispatch_semaphore
dispatch_semaphore实现的原理和自旋锁有点不一样。首先会先将信号量减一,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作。pthread_mutex
pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切换。NSLock
NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。NSCondition
NSCondition封装了一个互斥锁和条件变量。互斥锁保证线程安全,条件变量保证执行顺序。NSRecursiveLock
递归锁,pthread_mutex(recursive)的封装。@synchronized:
一个对象层面的锁,锁住了整个对象,底层使用了互斥递归锁来实现。
事件源
Runloop里我们说过source_t的概念,其实与队列的源是相同的。Runloop提供对源的监控。队列也可以实现对源的监控。且都可以创建自定义源。
理解:dispatch都是主动添加任务到队列中,然而当系统事件发生时,我们希望做一定的工作当监听到系统事件后就会触发一个任务,并自动将其加入队列执行,这里与之前手动添加任务的模式不同,一旦将Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中。(这个概念在监听系统事件时做一定的操作时是很有用处的!!!哈哈)
监听事件类型
Dispatch Source一共可以监听六类事件,分为11个类型,我们来看看都是什么:
DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据,下文中会有详细的演示。
DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
DISPATCH_SOURCE_TYPE_READ:读文件事件。
DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。
- timer_source 示例
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
if(time){
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 1, 1);
dispatch_source_set_event_handler(timer, ^{
});
dispatch_resume(timer);
}
- 监听度文件读事件
dispatch_source_t processContentsOfFile(const char *fileName) {
//prepare the file for reading
int fd = open(fileName,O_RDONLY);
if(fd == -1){
return NULL;
}
fcntl(fd,F_SETFL,O_NONBLOCK);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
if(!readSource){
close(fd);
return NULL;
}
dispatch_source_set_event_handler(readSource, ^{
});
return nil;
}
取消dispatch_source
当设置了Dispatch source对象将一直保持有效状态,除非手动调用dispatch_source_cancel函数来取消它。但取消了dispatch source对象后,将不能再接收到新的事件
暂停与恢复dispatch_source
可以通过使用dispatch_suspend和 dispatch_resume函数来暂停和恢复事件传递给dispatch source对象
Runtime系列一 Objective-C对象模型
- 一:前言
- 二:NSObject类的实现
- 三:NSObject对象的表示objc_object
- 四:元类(实例对象,类对象,元类之间的关系)
- 六:Runtime几个术语的结构分析
- 七:相关的API
- 八:API示例Demo
一:前言
Objective-c的编译器将OC代码编译成可执行二进制文件。操作系统在装载后,会在运行时运行时系统下运行该程序(此时运行时系统就是Runtime实现Objective-C机制的一个运行库,可以理解成专门运行Object-C的一个小系统)。
Runtime库主要做下面几件事:
封装
在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。找出方法的最终执行代码
当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
二:NSObject类的实现
//定义NSObject
@interface NSObject <NSObject> {
Class isa;
}
//定义Class类型
typedef struct objc_class *Class;
//定义objc_class
struct objc_class {
Class isa;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
};
由此可见NSObject类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
objc_class字段说明
isa:所有类的自身也是一个对象,这个对象的Class里面也有各个ias指针,它指向metaClass
super_class:指向该类的父类
cache:在我们每次调用过一个方法后,这个方法就会被缓存到cached列表中,
下次调用的时候就会在缓存中找,如果cache中没有,就会在methodlist中找。
举例说明
NSArray *array = [NSArray alloc]init];
alloc先执行,发现NSArray没有相应的方法,然后去父类查找。父类发现有,就会根据所需要的内存空间大小开始分配内存空间。alloc同意会加入cache里面。
接着执行init方法,如果NSArray响应该方法,则直接将其加入cache中。不响应就去父类查找。
三:NSObject对象的表示objc_object
表一个类的实例的结构体
这个实例只有指向其所属类的一个指针。当我们向一个Object-C对象发送消息时,运行库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime会根据isa指向所属的类的方法列表及父类方法列表中寻找与消息对应指向的方法。
NSObject是如何根据类创建对象的?这里有很多疑问。????
当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构
然后是类的实例变量数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。
struct objc_object {
Class isa;
}
type struct objc_object *id;
四:元类(实例对象,类对象,元类之间的关系)
从NSObject的定义了可以看出,每个类都是一个对象(类对象)。
我们在NSArray *array = [NSArray array]
使用类方法时幷没有创建对象。
那么类对象的isa指向哪里?(我能达成的共识是对象由isa指向”对象“定义),那么在object里类对象的isa指向叫meta-class.它是类对象的类。
发送实例方法时,会在类对象方法列表里找,幷缓存
发送类方法时,会在元类对象的方法列表里找,幷缓存
那么元类的的isa指针又指向哪里呢?
Object-c的设计者为了不让其无限循环下去,将元类的isa指向了其最基类。而最基类的meta-class的isa指向自己。这样就形成一个闭环。
类图结构如下(这个图画的真完美)
六:Runtime几个术语的结构分析
SEL
这个术语在头Runtime的头文件里没有看到相关的定义(
这里我们给出推测
type struct objc_selector *SEL
struct objc_selector{
void *sel_id;
const char *sel_types;
}
所以从这里可以将SEL理解成一个char *类型的字符串。这个字符串在method_list里映射了真正的函数实现。
id
typedef struct objc_object *id;
struct objc_object { Class isa; };
id 重新定义为objc_object * 类型(指向对象的指针)。objc_object结构体包含一个isa指针,根据isa指针可以找到所属的类。
注意 isa指针在代码运行时并不总是指向实例对象所属于的类。所以不能依靠它来确定类型,要想确定类型还是需要用对象的class方法。kvo实现原理就是被观察对象的isa指针指向一个中间类而不是真实的类型。
Method
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
构建Method时需要SEL(方法名),method_types存储参数与返回值的类型编码,IMP函数指针(真正的函数实现)
Ivar
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
}
ivar_name 变量名,ivvar_type变量类型编码
IMP
typedef id (*IMP)(id, SEL, ...);
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型
Cache
在objc_class的结构体中的cache字段,它用于缓存调用过的方法。cahce是指向objc_cache结构体的指针。
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
occupied 缓存的方法数目
buckets 指向Method数据结构的指针的数组。也就是缓存函数的地方。
Property
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
类型编码
编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。
//类型编码
float a[] = {1,2,3};
NSLog(@"array encoding type:%s",@encode(typeof(a)));
NSString *str = @"你好";
NSLog(@"string encoding type:%s",@encode(typeof(str)));
CGFloat ff = 2.0;
NSLog(@"float encoding type:%s",@encode(typeof(ff)));
//结果
array encoding type:[3f]
string encoding type:@
float encoding type:d
七:相关的API
将上述的实例对象,类对象,元类对象概念理解清楚后,理解实例变量,方法列表等信息。使用下列的相关API将易如反掌。
类相关API
const char * class_getName(Class cls);//取类名
Class class_getSuperclass(Class cls);//取父类
BOOL class_isMetaClass(Class cls);//是否是元类
size_t class_getInstanceSize(Class cls);//获取实例变量的大小
成员变量相关API
// 获取类中指定名称实例成员变量的信息
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 );
Objectice-c不支持往已经存在的类中添加实例变量。不管是系统提供的库,还是自己定义的类,都无法动态添加成员变量。(这里联想到了category的实现)但如果我们是通过运行时来创建一个类的话,在该类没注册前是可以添加实例变量的。且只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。(This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.)
class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量(注意咯)。使用时,还要注意内存的释放。
属性相关API
// 获取指定的属性
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 )
Method相关API
// 添加方法
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。一个Object-c方法是一个简单的C函数,它至少包含两个参数self,_cmd,所以我们在实现函数替换是或者交换时,要添加这两个参数
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。
添加方法时,class_addMethod函数后面有一个类型编码。这个类型编码在基本概念里已经做了解释。
八:API示例Demo
Demo源码RunTime相关实践
下面是类的一些基本API使用示例
- (void)viewDidLoad {
[super viewDidLoad];
//与类相关的API
//获得类名
self.view.backgroundColor = [UIColor whiteColor];
const char * resutl0 = class_getName([self class]);//获取类名
NSString *resutlStr = [NSString stringWithUTF8String:resutl0];
NSLog(@"这是resultStr:%@",resutlStr);
//获取父类
Class superClass = class_getSuperclass([self class]);
NSLog(@"这是父类:%@",superClass);
//获取变量大小
size_t classSize = class_getInstanceSize([self class]);
NSLog(@"这是classSize:%zu",classSize);
//是否是元类
if(class_isMetaClass([self class])){
NSLog(@"self class 是元类");
}
if( class_isMetaClass(superClass)){
NSLog(@"superClass是元类");
};
const char * className = object_getClassName([self class]);
Class metaClass = objc_getMetaClass(className);
if(class_isMetaClass(metaClass)){
NSLog(@"是元类");
}
//获取实例变量
const char * dataArrayIvarCstring = [@"_dataArray" UTF8String];
Ivar dataArrayIvar = class_getInstanceVariable([self class], dataArrayIvarCstring);
NSLog(@"这是Ivar:%@", [NSString stringWithUTF8String:ivar_getName(dataArrayIvar)]);
//获取属性(属性会自动生成实例变量)
const char * dataArrayPropertyCstring = [@"dataArray" UTF8String];
objc_property_t dataArrayPropertyr = class_getProperty([self class], dataArrayPropertyCstring);
NSLog(@"这是property:%@", [NSString stringWithUTF8String:property_getName(dataArrayPropertyr)]);
//通过SEL找到Method,并找到相应的实现
IMP doFuncMethodImp = class_getMethodImplementation([self class], @selector(doFunc));
doFuncMethodImp(self,@selector(doFunc));
//获取类方法
Method dofun2Method = class_getClassMethod([self class], @selector(dofunc2));
IMP dofun2MethodImp = method_getImplementation(dofun2Method);
dofun2MethodImp(self,@selector(dofun2Method));
}
下面是动态添加类的实践
#import "Runtime3ViewController.h"
#import <objc/runtime.h>
/*
添加实例变量
添加属性
添加方法
添加协议
*/
@interface Runtime3ViewController ()
@property (nonatomic, strong) UIColor *property2;
@end
@implementation Runtime3ViewController {
NSArray *_property0;
NSArray *property1;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//动态创建类,从参数里可以看出,可以指定父列
Class CreatClass0 = objc_allocateClassPair([NSObject class], "CreatedClass0", 0);
//动态添加实例变量
class_addIvar(CreatClass0, "_attribute0", sizeof(NSString *), log(sizeof(NSString *)), "i");
Ivar ivar = class_getInstanceVariable(CreatClass0, "_attribute0");
objc_registerClassPair(CreatClass0);
//(添加实例变量后再动态添加属性
objc_property_attribute_t type2 = {"T","@\"NSString\""};//T,属性类型
objc_property_attribute_t ownership2 = {"C",""};//C,属性的修饰符 copy
objc_property_attribute_t backingivar2 ={"V","_attribute0"};//V,属性对应的实例变量值
objc_property_attribute_t attrs2[] = { type2, ownership2, backingivar2 };//属性数组
class_addProperty(CreatClass0, "_attribute0", attrs2, 3);//为一个类添加上面设置的属性
SEL getter = NSSelectorFromString(@"attribute0");
SEL setter= NSSelectorFromString(@"setAttribute0:");
//(添加属性后,再为属性添加get,set的方法,将SEL,与实现绑定,并添加到类里
BOOL suc0 = class_addMethod(CreatClass0, getter, (IMP)attribute0Getter, "@@:");
BOOL suc1 = class_addMethod(CreatClass0, setter, (IMP)attribute0Setter, "v@:@");
NSLog(@"这是suc0:%@,suc1:%@",@(suc0),@(suc1));
//根据动态创建的类,创建对象
id idClass = [[CreatClass0 alloc]init];
//执行动态创建的类的方法
[idClass performSelector:setter withObject:@"你好"];
NSLog(@"这是被设置的:%@",[idClass performSelector:getter withObject:nil]);
//获取整理实例犯法
unsigned int copycopyMethodListCount = 0;
Method *methods = class_copyMethodList(CreatClass0, ©copyMethodListCount);
for (int i = 0; i< copycopyMethodListCount; i++) {
Method tmpMethod = methods[i];
SEL tmpName = method_getName(tmpMethod);
NSLog(@"这是所有函数的名字:%@", NSStringFromSelector(tmpName));
}
//添加协议
class_addProtocol(CreatClass0, NSProtocolFromString(@"UITableViewDelegate"));
}
//get方法
NSString *attribute0Getter(id classInstance, SEL _cmd) {
Ivar ivar = class_getInstanceVariable([classInstance class], "_attribute0");//获取变量,如果没获取到说明不存在
return object_getIvar(classInstance, ivar);
}
//set方法
void attribute0Setter(id classInstance, SEL _cmd, NSString *newName) {
Ivar ivar = class_getInstanceVariable([classInstance class], "_attribute0");//获取变量,如果没获取到说明不存在
id oldName = object_getIvar(classInstance, ivar);
if (oldName != newName) object_setIvar(classInstance, ivar, [newName copy]);
}
总结,其实理解了对象模型,在理解消息的发送机制,就理解了Rumtime。将在系列二里进行消息的发送机制的梳理。
深入理解Object-C的Category
深入理解Objective-C:Category
是我看过的最完整的一个对Category的描述。本篇文章只是一个实践后结论性的总结,并对其中的一些发生关联的概念做说明。
1. 前言
Category实际上就是将这些方法添加到主类的方法列表里的头部。
apple推荐的使用场景
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的category里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的category
- 将私有方法提前声明
不过除了apple推荐的使用场景,category的其他几个使用场景:
- 模拟多继承
- 把framework的私有方法公开
补充说明
什么是私有方法提前声明?
Cocoa没有任何真正的私有方法。只要知道对象支持的某个方法的名称,即使该对象所在的类的接口中没有该方法的声明,你也可以调用该方法。不过这么做编译器会报错,但是只要新建一个该类的类别,在类别.h文件中写上原始类该方法的声明,类别.m文件中什么也不写,就可以正常调用私有方法了。这就是传说中的私有方法前向引用。 所以说cocoa没有真正的私有方法。
我们还知道,即使没有引入 Category 的头文件,Category 的方法也会被添加进主类的方法列表里,可以通过 performSelector 的方式使用,导入头文件只是为了通过编译器的静态检查(将私有方法提前声明)。category如何模拟多继承?
模拟多继承主要是利用可以添加方法,添加属性。既然可以添加属性,也可以添加方法,那么我要的方法与东西,都可以在里面实现。
2. Category与extension对比
extension
在编译器决议,它是类的一部分。在编译期和头文件的@interface以及实现文件里的@implement一起形成一个完整的类。它伴随类的产生而产生。extension一般用于隐藏类的私有信息。category
在运行期决议,就category和extension的区别来看,可以推导出,extension可以添加实例变量,而category是无法添加实例变量的。因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部结构,这对编译型语言来说是灾难性的。
3. Category的结构与编译
我们知道所有的OC类和对象,在runtime层都是用struct表示的,category也不例外。category结构体如下
typedef struct category_t *Category;
typedef struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
} category_t;
类的名字,类,实例方法列表,类方法列表,协议列表,属性
从category的定义可以看出category可以添加实例方法,类方法,甚至可以实现协议,添加属性。但无法添加实例变量。
从category的结构体表示可以看出
在编译时,编译器在DATA段下(静态区)的objc_catlist_section里保存了category的数组。实际上这是个全局数组,所有的编译的category都会放在里面,这个数组供在运行时的加载
如下是这个全局数组
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_MyClass_$_MyAddition,
};
4. Category的加载
我们知道,Objective-C的运行是依赖OC的runtime的,而OC的runtime和其他系统库一样,是OS X和iOS通过dyld动态加载的。
当动态加载了OC的runtime时,就会将上面编译的那个全局数组做整理,添加到类的方法列表里。具体结论是
- 把category的实例方法,协议以及属性添加到类上
- 把category的类方法和协议添加到类的metaclass上
具体怎么添加的,有兴趣的可以自行查看源码
需要特别注意的是(特别重要)
category的方法没有”完全替换掉“原来已经有的方法,也就是说category和原来类都有的methoda,那么category附加完成后,类的方法列表里会有两个methodA;
category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。还有多个category时,越后的会越在数组的前面。
5. Category的+load方法
思考如下两个问题
在类的+load方法调用的时候,我们可以调用category中声明的方法吗?(其实是在思考,category将方法加到主类与load执行的顺序问题)
这些+load方法执行的顺序?(其实是在思考主类与category的执行load的顺序)
//Dog+DogCategory.h
@interface Dog (DogCategory)
+ (void)dogJiao;
@end
//Dog+DogCategory.m
#import "Dog+DogCategory.h"
@implementation Dog (DogCategory)
+ (void)load {
NSLog(@"%s",__FUNCTION__);
}
+ (void)dogJiao {
NSLog(@"%s",__FUNCTION__);
}
@end
//Dog+DogCategory2.h
#import "Dog+DogCategory.h"
@interface Dog (DogCategory2)
@end
//Dog+DogCategory2
#import "Dog+DogCategory2.h"
@implementation Dog (DogCategory2)
+ (void)load {
NSLog(@"%s",__FUNCTION__);
[self dogJiao];
}
@end
我们在DogCategory2中调用了DogCategory中的方法,由此可见,category的方法是在+load执行之前。
+load的执行顺序是先是 类,然后是category,而category的+load方法是按编译顺序。编译顺序在后的+load后执行。因为编译在后,在形成全局的数组时,会被加在数组最前面,这也是为什么,在category有相同的方法时,在执行时会选择后面文件在后面编译的文件。哈哈。
6. 怎么调用原来类中被category覆盖掉的方法?
我们已经知道category其实并不是完全替换掉原理的类的同名方法。只是category在方法列表的前面而已。所以我嫩只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方方法。
7. category与关联对象
其实category与关联对象没有关系。只是category只能给原有类添加方法,不能添加实例变量,可以用关联对象去解决这个问题罢了。
那么关联对象又是存在什么地方?如何存储?对象销毁的时候如何处理关联对象?
有兴趣的可以去看源码
所有的关联对象都由AssociationsManager管理,而AssociationsManager里面由一个静态AsscociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象存在一个全局的map里面。而map的key就是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv值。在对象销毁时,会查看这个对象有没有关联对象。如果有就会完成关联对象的移除工作。
RunLoop思考与总结
本篇是对这篇文章深入理解Runloop的学习与实践的理解,文字绝大部分是出自这篇文章,可以说是我自己学习总结吧。
- 一:程序是如何保持不退出的?
- 二:Runloop的基础概念与相关API
- 三:RunLoop的内部逻辑
- 四:RunLoop如何在睡眠中等待?
- 六:RunLoop与AutoreleasePool的关系
- 七:事件响应
- 八:手势识别
- 九:界面更新
- 十:动画
- 十一:定时器
- 十二:PerformSelecter
- 十三:GCD(还没具体的研究证实)
- 十四:观察RunLoop的实际调用(很重要)
一:程序是如何保持不退出的?
一个应用就是一段可执行的指令。一段段的指令按照一定的顺序执行,而这样一条执行流顺序就是一个抽象的概念叫“线程”。而提供给执行所需要的内存,计数器,栈,寄存器等独立空间就是进程空间,整个执行流(包括多个子执行流(线程))就是一个抽象概念”进程“。。
在下面的main函数指令执行完后并没有退出程序。那么是如何保证应用程序处于不退出呢?
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
上面的代码是一个iOS应用的main.m应用入口。明显在main执行完后就按理应当退出。但并没有退出。我们可以推测,UIApplicationMain()有一个死循环,而这个死循环要能时刻监听事件并执行事件发生后的程序指令,这个过程就是"EventLoop",这个循环的关键点:如何管理消息的到来,如何让线程在没有处理消息时休眠以避免资源的浪费,在有消息来时如何立刻被唤醒。所以线程执行完,就一直处于这个函数的内部“接受消息--》处理消息-->等待-->c处理”,直到收到退出的标识。
苹果不循序直接创建Runloop,它只提荣CFRunloopGetMain()与CFRunloopGetCurrent()函数,用懒加载的方式。线程与Runloop是一一对应的。
二:Runloop的基础概念与相关API
iOS里有关于Runloop的类既有OC的API也有CoreFoundation的API.我在查看NSRunloop时API内容简单到已经忽略了很多概念。CoreFoundation里的CFRunLoop对Runloop概念的诠释更加全面。
如下几个核心概念
CFRunLoop,
CFRunLoopMode,
CFRunLoopSourceRef,
CFRunLoopObserverRef,
CFRunLoopTimerRef
CFRunLoopMode
一个Runloop包含若干个Mode,每个Mode包含若干个Source/Timer/Observer。每次调用RunLoop时,只能指定在一个Mode下运行。切换Mode可以切换在不同Mode下的Source/Timer/Observer(NSTimer 这就是为什么要加入指定的Mode才能运行,并且只能在指定的Mode下)。Source/Timer/Observer将其统一称为item ,一个item可以加入到多个Mode,如果一个Mode里一个 item都没有,就不进入循环。typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM; typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef; typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef; typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef; typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
CFRunLoopMode 和 CFRunLoop 的结构大致如下(此部分在开源的Runloop的源码里有,在苹果的库里并没有开放):
struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set };
特别强调(我以前没有理解将NSTimer添加到kCFRunLoopCommonModes模式的真正含义,哈哈,现在终于弄清楚了)
CFRunLoop里有commonModes,一个Mode可以将自己标记为"Common”属性。CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
。每当RunLoop的模式发生变化时,都会将_commonModeItems里的Source/Observer/Timer同步到具有“Common”标记的Mode里。
例如
主线程的Runloop里有两个预置的Mode:kCFRunLoopDefaultMode,UITrackingRunLoopMode,这两个Mode都已经被标记为"Common"属性。有时你需要一个Timer,在两个Mode中都能回调,一种办法是将这个Timer分别添加到这两个Mode里。还有一种方式,就是将其添加到RunLoop的”commonModelItems"中,“commonModeItems”被RunLoop自动更新到所有具有“Common”属性的Mode里去。还可以自己创建Model,通过
CFRunLoopAddCommonMode(runloop, yourFriendlyCFString);
添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中
//CFRunLoop对外暴露的管理Mode接口只有如下 CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);//将一个mode添加common标记 CFRunLoopRunInMode(CFStringRef modeName, ...); //Mode 暴露的管理 mode item的接口有下面几个: //在RunLoop里添加Source,Observer,Timer。并指定Mode。 Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode); void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode); void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode); Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode); void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode); void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode); Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode); void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode); void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
CFRunLoopSourceRef 事件源。分为Source0,Source1
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef是基于时间的触发器,它和NSTimer是toll-frebridged的。当加入到Runloop时,Runloop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回到。
CFRunLoopObserverRef是观察者,每个Observer都包含一个回调。当RunLoop的状态发生改变时,观察者就能通过回调接受这个变化。如下是RunLoop的状态
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0),//进入Runllp kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入睡眠 kCFRunLoopAfterWaiting = (1UL << 6),//刚从睡眠职工唤醒 kCFRunLoopExit = (1UL << 7),//退出runloop kCFRunLoopAllActivities = 0x0FFFFFFFU };
三:RunLoop的内部逻辑
此段代码出自最前面提到的文章,我将它贴到此处,便于我自己的理解。
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
从以上可以看出RunLoop就是一个一个有do while的函数。大致思路
用一张图能很好的说明
- 在这里区别一些source0与source1 source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。 source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调
source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档。
- 关键还有将现行睡眠,等待消息。那么是如何睡眠的呢?
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
四:RunLoop如何在睡眠中等待?
其实要搞清楚Runloop如何停下循环进入睡眠(在我们程序的概念里,大家从Runloop内部实现可以看出,是一个do while循环,循环的过程其实也是消耗资源的,所以才去睡眠来防止消耗资源)
休眠的函数调用的函数时mach_msg()。
在微内核Mach中,所有的东西都是通过自己的对象实现的,进程,线程,虚拟内存都被称作"对象“。和其他架构不同,Mach的对象间不能直接调用,只能通过消息传递的方式,这就是Mach的IPC(进程通信)的核心。
为了实现消息的发送和接收,mach_msg()函数实际上是调用了一个Mach陷阱(trap),即函数mach_msg_trap(),陷阱这个概念在Mach中等同与系统调用。当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态,内核态中内核实现的mach_msg()函数完成实际的工作。若果么有别人发送port消息过来,内核会将线程置于等待状态。例如,当APP静止时点击暂停,会看到主线程调用栈停留在mach_msg_trap()这里。
六:RunLoop与AutoreleasePool的关系
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 objcautoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 objcautoreleasePoolPush() 释放旧的池并创建新池 ;Exit(即将退出Loop) 时调用 objcautoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
kCFRunLoopEntry; // 进入runloop之前,创建一个自动释放池
kCFRunLoopBeforeWaiting; // 休眠之前,销毁自动释放池,创建一个新的自动释放池
kCFRunLoopExit; // 退出runloop之前,销毁自动释放池
七:事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
总结下:屏幕硬件接收事件--->给SprindBoard(桌面)应用程序-->通过mach_port进程间通信给本应用(进程)--->进入主线程的runloop---》处理事件进入事件队列处理执行
八:手势识别
(这里可以深入的思考,手势识别与touch事件向冲突时如何处理的)
九:界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,会去调用执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
说明:总结就是在进入睡眠前,会计算好,绘制好界面。等待垂直时钟从缓冲区取帧数据。
这个函数内部的调用栈大概是这样的:
_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 drawRe_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];
十:动画
Runloop与动画其实与绘制差不多。当要提交动画时,UIKit或者是CoreAnimation会向Runloop注册通知,并将其提交到渲染系统,包括动画结束的通知。也就是当动画结束后,runloop会被这些observer唤醒。(runloop的唤醒其实都是timer,source0,source1,observer都可以唤醒,特别需要说明的是performSeletor是通过timer唤醒)
十一:定时器
- NSTimer
其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
GCD Timer
GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。CADisplayLink
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。
十二:PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
十三:GCD(还没具体的研究证实)
十四:观察RunLoop的实际调用(很重要)
这个等有机会在实践吧
实践思路,操作一个事件,看runloop的反应
如。Time事件,手动添加一个事件源source0,触摸事件的事件源(基于端口的事件源)
Copyright © 2015 Powered by MWeb, Theme used GitHub CSS.