iOS runtime和runloop

runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, 所以大家有必要进行研究,有能力的童鞋可以和下面作者一样, 亲历实践一下。


在简书里发现了两篇非常好的文章介绍 runtime和runloop的,在这里合二为一了, 把原版作者的东西拿了过来, 为了尊重作者,在这里注明一下 @sam_lau 是runtime的作者, @tripleCC是runloop的作者 




RunTime

Objective-C是基于C语言加入了面向对象特性消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和转发。下面通过分析Apple开源的Runtime代码(我使用的版本是objc4-646.tar)来深入理解Objective-C的Runtime机制。

Runtime数据结构

在Objective-C中,使用[receiver message]语法并不会马上执行receiver对象的message方法的代码,而是向receiver发送一条message消息,这条消息可能由receiver来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。其实[receiver message]被编译器转化为:

idobjc_msgSend (idself, SEL op, ... );

下面从两个数据结构id和SEL来逐步分析和理解Runtime有哪些重要的数据结构。

SEL

SEL是函数objc_msgSend第二个参数的数据类型,表示方法选择器,按下面路径打开objc.h文件

SEL Data Structure

查看到SEL数据结构如下:

typedefstructobjc_selector *SEL;

其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。

如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)方法将SEL转化为字符串,再用NSLog打印。

id

接下来看objc_msgSend第一个参数的数据类型id,id是通用类型指针,能够表示任何对象。按下面路径打开objc.h文件

id Data Structure.png

查看到id数据结构如下:

/// Represents an instance of a class.structobjc_object {    Class isa  OBJC_ISA_AVAILABILITY;};/// A pointer to an instance of a class.typedefstructobjc_object *id;

id其实就是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类

注意:根据Apple的官方文档Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技术实现的,isa指针在运行时被修改,指向一个中间类而不是真正的类。所以,你不应该使用isa指针来确定类的关系,而是使用class方法来确定实例对象的类。

Class

isa指针的数据类型是Class,Class表示对象所属的类,按下面路径打开objc.h文件

Class Data Structure

/// An opaque type that represents an Objective-C class.typedefstructobjc_class *Class;

可以查看到Class其实就是一个objc_class结构体指针,但这个头文件找不到它的定义,需要在runtime.h才能找到objc_class结构体的定义。

按下面路径打开runtime.h文件

objc_class Data Structure

查看到objc_class结构体定义如下:

structobjc_class {    Class isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class super_class                                        OBJC2_UNAVAILABLE;constchar*name                                        OBJC2_UNAVAILABLE;longversion                                            OBJC2_UNAVAILABLE;longinfo                                                OBJC2_UNAVAILABLE;longinstance_size                                      OBJC2_UNAVAILABLE;structobjc_ivar_list *ivars                            OBJC2_UNAVAILABLE;structobjc_method_list **methodLists                    OBJC2_UNAVAILABLE;structobjc_cache *cache                                OBJC2_UNAVAILABLE;structobjc_protocol_list *protocols                    OBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;/* Use `Class` instead of `struct objc_class *` */

注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本,但我们仍能从中获取一些有用信息。

让我们分析一些重要的成员变量表示什么意思和对应使用哪些数据结构。

isa表示一个Class对象的Class,也就是Meta Class。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。我们会在objc-runtime-new.h文件找到证据,发现objc_class有以下定义:

structobjc_class : objc_object {// Class ISA;Class superclass;cache_tcache;// formerly cache pointer and vtableclass_data_bits_tbits;// class_rw_t * plus custom rr/alloc flags......}

由此可见,结构体objc_class也是继承objc_object,说明Class在设计中本身也是一个对象

其实Meta Class也是一个Class,那么它也跟其他Class一样有自己的isa和super_class指针,关系如下:

Class isa and superclass relationship from Google

上图实线是super_class指针,虚线是isa指针。有几个关键点需要解释以下:

Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。

每个Class都有一个isa指针指向唯一的Meta class

Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

每个Meta class的isa指针都指向Root class (meta)。

super_class表示实例对象对应的父类

name表示类名

ivars表示多个成员变量,它指向objc_ivar_list结构体。在runtime.h可以看到它的定义:

structobjc_ivar_list {intivar_count                                          OBJC2_UNAVAILABLE;#ifdef __LP64__intspace                                                OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;}

objc_ivar_list其实就是一个链表,存储多个objc_ivar,而objc_ivar结构体存储类的单个成员变量信息。

methodLists表示方法列表,它指向objc_method_list结构体的二级指针,可以动态修改*methodLists的值来添加成员方法,也是Category实现原理,同样也解释Category不能添加实例变量的原因。在runtime.h可以看到它的定义:

structobjc_method_list {structobjc_method_list *obsolete                        OBJC2_UNAVAILABLE;intmethod_count                                        OBJC2_UNAVAILABLE;#ifdef __LP64__intspace                                                OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_method method_list[1]                        OBJC2_UNAVAILABLE;}

同理,objc_method_list也是一个链表,存储多个objc_method,而objc_method结构体存储类的某个方法的信息。

cache用来缓存经常访问的方法,它指向objc_cache结构体,后面会重点讲到。

protocols表示类遵循哪些协议

Method

Method表示类中的某个方法,在runtime.h文件中找到它的定义:

/// An opaque type that represents a method in a class definition.typedefstructobjc_method *Method;structobjc_method {    SEL method_name                                          OBJC2_UNAVAILABLE;char*method_types                                      OBJC2_UNAVAILABLE;    IMP method_imp                                          OBJC2_UNAVAILABLE;}

其实Method就是一个指向objc_method结构体指针,它存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。

Ivar

Ivar表示类中的实例变量,在runtime.h文件中找到它的定义:

/// An opaque type that represents an instance variable.typedefstructobjc_ivar *Ivar;structobjc_ivar {char*ivar_name                                          OBJC2_UNAVAILABLE;char*ivar_type                                          OBJC2_UNAVAILABLE;intivar_offset                                          OBJC2_UNAVAILABLE;#ifdef __LP64__intspace                                                OBJC2_UNAVAILABLE;#endif}

Ivar其实就是一个指向objc_ivar结构体指针,它包含了变量名(ivar_name)、变量类型(ivar_type)等信息。

IMP

在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现,在objc.h找到它的定义:

/// A pointer to the function of a method implementation.#if!OBJC_OLD_DISPATCH_PROTOTYPEStypedefvoid(*IMP)(void/* id, SEL, ... */);#elsetypedefid(*IMP)(id, SEL, ...);#endif

当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。

Cache

顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义:

typedefstructobjc_cache *Cache                            OBJC2_UNAVAILABLE;structobjc_cache {unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;unsignedintoccupied                                    OBJC2_UNAVAILABLE;    Method buckets[1]                                        OBJC2_UNAVAILABLE;};

Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

消息发送

前面从objc_msgSend作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入消息发送这个正题。

objc_msgSend函数

在前面已经提过,当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:

idobjc_msgSend (idself, SEL op, ... );

现在让我们看一下objc_msgSend它具体是如何发送消息:

首先根据receiver对象的isa指针获取它对应的class

优先在class的cache查找message方法,如果找不到,再到methodLists查找

如果没有在class找到,再到super_class查找

一旦找到message这个方法,就执行它实现的IMP。

Objc Message.gif

self与super

为了让大家更好地理解self和super,借用sunnyxx博客的ios程序员6级考试一道题目:下面的代码分别输出什么?

@implementationSon:Father- (id)init{self= [superinit];if(self)    {NSLog(@"%@",NSStringFromClass([selfclass]));NSLog(@"%@",NSStringFromClass([superclass]));    }returnself;}@end

self表示当前这个类的对象,而super是一个编译器标示符,和self指向同一个消息接受者。在本例中,无论是[self class]还是[super class],接受消息者都是Son对象,但super与self不同的是,self调用class方法时,是在子类Son中查找方法,而super调用class方法时,是在父类Father中查找方法。

当调用[self class]方法时,会转化为objc_msgSend函数,这个函数定义如下:

idobjc_msgSend(idself, SEL op, ...)

这时会从当前Son类的方法列表中查找,如果没有,就到Father类查找,还是没有,最后在NSObject类查找到。我们可以从NSObject.mm文件中看到- (Class)class的实现:

- (Class)class{returnobject_getClass(self);}

所以NSLog(@"%@", NSStringFromClass([self class]));会输出Son

当调用[super class]方法时,会转化为objc_msgSendSuper,这个函数定义如下:

idobjc_msgSendSuper(structobjc_super *super, SEL op, ...)

objc_msgSendSuper函数第一个参数super的数据类型是一个指向objc_super的结构体,从message.h文件中查看它的定义:

///Specifies the superclass of an instance.structobjc_super {///Specifies an instance of a class.__unsafe_unretained id receiver;///Specifies the particular superclass of the instance to message.#if!defined(__cplusplus)  &&  !__OBJC2__/* For compatibility with old objc-runtime.h header */__unsafe_unretained Classclass;#else__unsafe_unretained Class super_class;#endif/* super_class is the first class to search */};#endif

结构体包含两个成员,第一个是receiver,表示某个类的实例。第二个是super_class表示当前类的父类。

这时首先会构造出objc_super结构体,这个结构体第一个成员是self,第二个成员是(id)class_getSuperclass(objc_getClass("Son")),实际上该函数会输出Father。然后在Father类查找class方法,查找不到,最后在NSObject查到。此时,内部使用objc_msgSend(objc_super->receiver, @selector(class))去调用,与[self class]调用相同,所以结果还是Son

隐藏参数self和_cmd

当[receiver message]调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self和_cmd,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。至于对于self的描述,上面已经解释非常清楚了,下面我们重点讲解_cmd。

_cmd表示当前调用方法,其实它就是一个方法选择器SEL。一般用于判断方法名或在Associated Objects中唯一标识键名,后面在Associated Objects会讲到。

方法解析与消息转发

[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。

Method Resolution

Fast Forwarding

Normal Forwarding

Message Forward from Google

Method Resolution

首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:

@interfaceMessage:NSObject- (void)sendMessage:(NSString*)word;@end

@implementationMessage- (void)sendMessage:(NSString*)word{NSLog(@"normal way : send message = %@", word);}@end

如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:

- (void)viewDidLoad {    [superviewDidLoad];    Message *message = [Messagenew];    [message sendMessage:@"Sam Lau"];}

控制台会打印以下信息:

normal way :sendmessage = Sam Lau

但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:

#pragma mark - Method Resolution/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation+ (BOOL)resolveInstanceMethod:(SEL)sel{if(sel ==@selector(sendMessage:)) {        class_addMethod([selfclass], sel, imp_implementationWithBlock(^(idself,NSString*word) {NSLog(@"method resolution way : send message = %@", word);        }),"v@*");    }returnYES;}

控制台就会打印以下信息:

method resolution way :sendmessage = Sam Lau

注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings

如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)

Fast Forwarding

如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding

继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:

#pragma mark - Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector{if(aSelector ==@selector(sendMessage:)) {return[MessageForwarding new];    }returnnil;}

此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:

@interfaceMessageForwarding:NSObject- (void)sendMessage:(NSString*)word;@end

@implementationMessageForwarding- (void)sendMessage:(NSString*)word{NSLog(@"fast forwarding way : send message = %@", word);}@end

此时,控制台会打印以下信息:

fast forwarding way :sendmessage = Sam Lau

这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

Normal Forwarding

如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。

继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:

#pragma mark - Normal Forwarding-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

{

NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];if(!methodSignature){        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];}    return methodSignature;}-(void)forwardInvocation:(NSInvocation*)anInvocation

{

MessageForwarding *messageForwarding = [MessageForwarding new];if([messageForwarding respondsToSelector:anInvocation.selector]){        [anInvocation invokeWithTarget:messageForwarding];}}

关于这个例子的示例代码请到github下载。

三种方法的选择

Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?

Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。

Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。

Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

Associated Objects

Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class. (Programming with Objective-C)

当想使用Category对已存在的类进行扩展时,一般只能添加实例方法或类方法,而不适合添加额外的属性。虽然可以在Category头文件中声明property属性,但在实现文件中编译器是无法synthesize任何实例变量和属性访问方法。这时需要自定义属性访问方法并且使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个API来向对象添加、获取和删除关联值:

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 )

其中objc_AssociationPolicy是个枚举类型,它可以指定Objc内存管理的引用计数机制。

typedefOBJC_ENUM(uintptr_t, objc_AssociationPolicy){    OBJC_ASSOCIATION_ASSIGN =0,/**< Specifies a weak reference to the associated object. */OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,/**< Specifies a strong reference to the associated object.

*  The association is not made atomically. */OBJC_ASSOCIATION_COPY_NONATOMIC =3,/**< Specifies that the associated object is copied.

*  The association is not made atomically. */OBJC_ASSOCIATION_RETAIN =01401,/**< Specifies a strong reference to the associated object.

*  The association is made atomically. */OBJC_ASSOCIATION_COPY =01403/**< Specifies that the associated object is copied.

*  The association is made atomically. */};

下面有个关于NSObject+AssociatedObjectCategory添加属性associatedObject的示例代码:

NSObject+AssociatedObject.h

@interfaceNSObject(AssociatedObject)@property(strong,nonatomic)idassociatedObject;@end

NSObject+AssociatedObject.m

@implementationNSObject(AssociatedObject)- (void)setAssociatedObject:(id)associatedObject{    objc_setAssociatedObject(self,@selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (id)associatedObject{returnobjc_getAssociatedObject(self, _cmd);}@end

Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。

Method Swizzling

Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。如果能够利用好这个技巧,可以写出简洁、有效且维护性更好的代码。可以参考两篇关于Method Swizzling技巧的文章:

nshipster Method Swizzling

Method Swizzling 和 AOP 实践

Aspect-Oriented Programming(AOP)

类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern)AOP作用就是分离横向关注点(Cross-cutting concern)来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。

危险性

Method Swizzling就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。在stackoverflow上有人已经提出这样一个问题:What are the Dangers of Method Swizzling in Objective C?,它的危险性主要体现以下几个方面:

Method swizzling is not atomic

Changes behavior of un-owned code

Possible naming conflicts

Swizzling changes the method's arguments

The order of swizzles matters

Difficult to understand (looks recursive)

Difficult to debug

总结

虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。

扩展阅读

玉令天下博客的Objective-C Runtime

顾鹏博客的Objective-C Runtime

Associated Objects

Method Swizzling

Method Swizzling 和 AOP 实践

Objective-C Runtime Reference

What are the Dangers of Method Swizzling in Objective C?

ios程序员6级考试(答案和解释)




RunLoop

深入理解RunLoop这篇文章写的很好!

简介

RunLoop顾名思义,就是运行循环的意思。

基本作用:

保持程序的持续运行

处理App中的各类事件(触摸事件、定时器事件、Selector事件)

节省CPU资源,提高程序性能:没有事件时就进行睡眠状态

内部实现:

do-while循环,在这个循环内部不断地处理各种任务(Source\Timeer\Observer)

注意点:

一个线程对应一个RunLoop(采用字典存储,线程号为key,RunLoop为value)

主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动

RunLoop只能选择一个Mode启动,如果当前Mode没有任何Source、Timer、Observer,那么就不会进入RunLoop

RunLoop的主要函数调用顺序为:CFRunLoopRun->CFRunLoopRunSpecific->__CFRunLoopRun

注意特殊情况,事实上,在只有Observer的情况,也不一定会进入循环,因为源代码里面只会显式地检测两个东西:Source和Timer(这两个是主动向RunLoop发送消息的);Observer是被动接收消息的

RunLoop在第一次获取时创建,在线程结束时销毁

RunLoop循环示意图:(针对上面的__CFRunLoopRun函数,Mode已经判断非空前提)

图1

RunLoop循环示意图

图2

接触过微处理器编程的基本上都知道,在编写微处理器程序时,我通常会在main函数中写一个无限循环,然后在这个循环里面对外部事件进行监听,比如外部中断,一些传感器的数据等,在没有外部中断时,就让CPU进入低功耗模式。如果接收到了外部中断,就恢复到正常模式,对中断进行处理。

while(1) {// 根据中断决定是否切换模式执行任务}// 或者for(;;) {}

RunLoop和这个相似,也是在线程的main中增加了一个循环:

intmain(intargc,char* argv[]) {BOOLrunning =YES;do{// 执行各种任务,处理各种事件// ......}while(running);return0;}

所以线程在这种情况下,便不会退出。

关于MainRunLoop:

intmain(intargc,char* argv[]) {@autoreleasepool{returnUIApplicationMain(argc, argv,nil,NSStringFromClass([AppDelegate class]));    }}

在viewDidLoad中设置断电,然后得到以下主线程栈信息:

可以看到,UIApplicationMain内部启动了一个和主线程相关联的RunLoop(_CFRunLoopRun)。在这里也可以推断,程序进入UIApplicationMain就不会退出了。我稍微对主函数进行了如下修改,并在return语句上打印了断点:

运行程序后,并不会在断点处停下,证实了上面的推断。

上面涉及了一个_CFRunLoopRun函数,接下来说明下iOS中访问和使用RunLoop的API:

Foundation--NSRunLoop

Core Foundation--CFRunLoopRef(开源)

因为后者是开源的,且前者是在后者上针对OC的封装,所以一般是对CFRunLoopRef进行研究。

两套API对应获取RunLoop对象的方式:

Foundation

[NSRunLoop currentRunLoop]; // 当前runloop

[NSRunLoop mainRunLoop];// 主线程runloop

Core Foundation

CFRunLoopGetCurrent();// 当前runloop

CFRunLoopGetMain();// 主线程runloop

值得注意的是,获取当前RunLoop都是进行懒加载的,也就是调用时自动创建线程对应的RunLoop。

RunLoop相关类:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

类之间的关系

以上图片说明了各个类之间的关系。

CFRunLoopModeRef说明:

代表RunLoop的运行模式,一个RunLoop可以包含多个Mode,每个Mode可以包含多个Source、Timer、Observer

每次RunLoop启动时,只能指定其中一个Mode,这个Mode就变成了CurrentMode

当启动RunLoop时,如果所在Mode中没有Source、Timer、Observer,那么将不会进入RunLoop,会直接结束

如果要切换Mode,只能退出Loop,再重新制定一个Mode进入

系统默认注册了5个Mode:

NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行

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

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

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

NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

关于NSRunLoopCommonModes:

一个Mode可以将自己标记为“Common”属性,每当 RunLoop 的内容发生变化时,RunLoop会对标记有“Common”属性的Mode进行相适应的切换,并同步Source/Observer/Timer

在主线程中,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode这两个Mode都是被默认标记为“Common”属性的,从输出的主线程RunLoop可以查看。

- 结合上面两点,当使用NSRunLoopCommonModes占位时,会表明使用标记为“Common”属性的Mode,在一定层度上,可以说是“拥有了两个Mode”,可以在这两个Mode中的其中任意一个进行工作

CFRunLoopTimerRef说明:

CFRunLoopTimerRef是基于时间的触发器,它包含了一个时间长度和一个回调函数指针。当它加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

CFRunLoopTimerRef大部分指的是NSTimer,它受RunLoop的Mode影响

由于NSTimer在RunLoop中处理,所以受其影响较大,有时可能会不准确。还有一种定时器是GCD定时器,它并不在RunLoop中,所以不受其影响,也就比较精确

接下来说明各种Mode下,NSTimer的工作情况:

情况1

在对创建的定时器进行模式修改前,scheduledTimerWithTimeInterval创建的定时器只在NSDefaultRunLoopMode模式下可以正常运行,当滚动UIScroolView时,模式转换成UITrackingRunLoopMode,定时器就失效了。

修改成NSRunLoopCommonModes后,定时器在两个模式下都可以正常运行

// 创建的定时器默认添加到当前的RunLoop中(没有就创建),而且是NSDefaultRunLoopMode模式NSTimer*timer = [NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 可以通过以下方法对模型进行修改[[NSRunLoopmainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

情况2

timerWithTimeInterval创建的定时器并没有手动添加进RunLoop,所以需要手动进行添加。当添加为以下模式时,定时器只在UITrackingRunLoopMode模式下进行工作,也就是滑动UIScrollView时就会工作,停止滑动时就不工作

如果把UITrackingRunLoopMode换成NSDefaultRunLoopMode,那么效果就和情况1没修改Mode前的效果一样

NSTimer*timer = [NSTimertimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 在UITrackingRunLoopMode模式下定时器才会运行[[NSRunLoopmainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

CFRunLoopSourceRef说明:

Source分类

按官方文档

Port-Based Sources

Custom Input Sources

Cocoa Perform Selector Sources

按照函数调用栈

Source0:非基于Port的

Source0本身不能主动触发事件,只包含了一个回调函数指针

Source1:基于Port的,通过内核和其他线程通信,接收、分发系统事件

包含了mach_port和一个回调函数指针,接收到相关消息后,会分发给Source0进行处理

CFRunLoopObserverRef说明:

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

能够监听的状态

typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity){        kCFRunLoopEntry = (1UL <<0),// 进入RunLoopkCFRunLoopBeforeTimers = (1UL <<1),//即将处理timerkCFRunLoopBeforeSources = (1UL <<2),//即将处理SourceskCFRunLoopBeforeWaiting = (1UL <<5),//即将进入休眠kCFRunLoopAfterWaiting = (1UL <<6),//即将唤醒kCFRunLoopExit = (1UL <<7),//即将退出RunLoopkCFRunLoopAllActivities =0x0FFFFFFFU//所有活动};

添加监听者步骤

// 创建监听着CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {NSLog(@"%ld", activity);    });//    [[NSRunLoop currentRunLoop] getCFRunLoop]// 向当前runloop添加监听者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 释放内存CFRelease(observer);

CF的内存管理(Core Foundation):

1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release

比如CFRunLoopObserverCreate

2.release函数:CFRelease(对象);

自动释放池释放的时间和RunLoop的关系:

注意,这里的自动释放池指的是主线程的自动释放池,我们看不见它的创建和销毁。自己手动创建@autoreleasepool {}是根据代码块来的,出了这个代码块就释放了。

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

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

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

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

在自己创建线程时,需要手动创建自动释放池AutoreleasePool

综合上面,可以得到以下结论:

@autoreleasepool {}内部实现

有以下代码:

intmain(intargc,constchar* argv[]){@autoreleasepool{    }return0;}

查看编译转换后的代码:

intmain(intargc,constchar* argv[]){/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool;    }return0;}

__AtAutoreleasePool是什么呢?找到其定义:

struct__AtAutoreleasePool {  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void* atautoreleasepoolobj;};

可以看到__AtAutoreleasePool是一个类:

其构造函数使用objc_autoreleasePoolPush创建了一个线程池,并保存给成员变量atautoreleasepoolobj。

其析构函数使用objc_autoreleasePoolPop销毁了线程池

结合以上信息,main函数里面的__autoreleasepool是一个局部变量。当其创建时,会调用构造函数创建线程池,出了{}代码块时,局部变量被销毁,调用其析构函数销毁线程池。

RunLoop实际应用

常驻线程

当创建一个线程,并且希望它一直存在时,就需要使用到RunLoop,否则线程一执行完任务就会停止。

要向线程存在,需要有强指针引用他,其他的代码如下:

// 属性@property(strong,nonatomic)NSThread*thread;// 创建线程_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];[_thread start];- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{// 点击时使线程_thread执行test方法[selfperformSelector:@selector(test) onThread:_thread withObject:nilwaitUntilDone:NO];}//- (void)test{NSLog(@"__test__");}

就单单以上代码,是不起效果的,因为线程没有RunLoop,执行完test后就停止了,无法再让其执行任务(强制start会崩溃)。

通过在子线程中给RunLoop添加监听者,可以了解下performSelector:onThread:内部做的事情:

调用performSelector:onThread: 时,实际上它会创建一个Source0加到对应线程的RunLoop里去,所以,如果对应的线程没有RunLoop,这个方法就会失效

// 这句在主线程中调用// _thread就是下面的线程[selfperformSelector:@selector(run) onThread:_thread withObject:nilwaitUntilDone:NO];

performSelecter:afterDelay:也是一样的内部操作方法,只是创建的Timer添加到当前线程的RunLoop中了

// 创建RunLoop即将唤醒监听者CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {// 打印唤醒前的RunLoopNSLog(@"%ld--%@", activity, [NSRunLoopcurrentRunLoop]);    });// 向当前runloop添加监听者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 释放内存CFRelease(observer);    [selfperformSelector:@selector(setView:) withObject:nilafterDelay:2.0];// 使model不为空[[NSRunLoopcurrentRunLoop] addPort:[NSPortport] forMode:NSDefaultRunLoopMode];    [[NSRunLoopcurrentRunLoop] run];

综合上面的解释,可以知道performSelector:onThread:没有起作用,是因为_thread线程内部没有RunLoop,所以需要在线程内部创建RunLoop。

创建RunLoop并使对应线程成为常驻线程的常见方式有2:

方式1

向创建的RunLoop添加NSPort(Sources),让Mode不为空,RunLoop能进入循环不会退出

[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];

方式2

让RunLoop一直尝试运行,判断Mode是否为空,不是为空就进入RunLoop循环

while(1) {    [[NSRunLoopcurrentRunLoop] run];}

AFNetWorking就使用到了常驻线程:

创建常驻线程

+ (void)networkRequestThreadEntryPoint:(id)__unused object {@autoreleasepool{        [[NSThreadcurrentThread] setName:@"AFNetworking"];// 创建RunLoop并向Mode添加NSMachPort,使RunLoop不会退出NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop];        [runLoop addPort:[NSMachPortport] forMode:NSDefaultRunLoopMode];        [runLoop run];    }}+ (NSThread*)networkRequestThread {staticNSThread*_networkRequestThread =nil;staticdispatch_once_toncePredicate;dispatch_once(&oncePredicate, ^{        _networkRequestThread = [[NSThreadalloc] initWithTarget:selfselector:@selector(networkRequestThreadEntryPoint:) object:nil];        [_networkRequestThread start];    });return_networkRequestThread;}

使用常驻线程

- (void)start {    [self.locklock];if([selfisCancelled]) {        [selfperformSelector:@selector(cancelConnection) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]];    }elseif([selfisReady]) {self.state= AFOperationExecutingState;        [selfperformSelector:@selector(operationDidStart) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]];    }    [self.lockunlock];}

给子线程开启定时器

_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];[_thread start];// 子线程添加定时器- (void)subTimer{// 默认创建RunLoop并向其model添加timer,所以后续只需要让RunLoop run起来即可[NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 貌似source1不为空,source0就不为空//    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoopcurrentRunLoop] run];}

让某些事件(行为、任务)在特定模式下执行

比如图片的设置,在UIScrollView滚动的情况下,我不希望设置图片,等停止滚动了再设置图片,可以用以下代码:

// 图片只在NSDefaultRunLoopMode模式下会进行设置显示[self.imageViewperformSelector:@selector(setImage:) withObject:[UIImageimageNamed:@"Snip20150712_39"] afterDelay:2.0inModes:@[NSDefaultRunLoopMode]];

先设置任务在NSDefaultRunLoopMode模式在执行,这样,在滚动使RunLoop进入UITrackingRunLoopMode时,就不会进行图片的设置了。

控制定时器在特定模式下执行

上文的《CFRunLoopTimerRef说明:》中已经指出

添加Observer监听RunLoop的状态

监听点击事件的处理(在所有点击事件之前做一些事情)

具体步骤在《CFRunLoopObserverRef说明:》中已写明

GCD定时器

注意:

dispatch_source_t是个类,这点比较特殊

//    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());dispatch_source_ttimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0, dispatch_get_global_queue(0,0));    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW,1.0* NSEC_PER_SEC,0* NSEC_PER_SEC);    dispatch_source_set_event_handler(timer, ^{        NSLog(@"__");        NSLog(@"%@", [NSThread currentThread]);staticNSInteger count =0;if(count++ ==3) {// 为什么dispatch_cancel不能用_timer?/// Controlling expression type '__strong dispatch_source_t' (aka 'NSObject *__strong') not compatible with any generic association type// 类型错误,可能dispatch_cancel是宏定义,需要的就是方法调用,而不是变量//            dispatch_cancel(self.timer);dispatch_source_cancel(_timer);        }    });// 定时器默认是停止的,需要手动恢复dispatch_resume(timer);// 需要一个强引用保证timer不被释放_timer = timer;

最后一点需要说明的是,SDWebImage框架的下载图片业务中也使用到了RunLoop,老确保图片下载成功后才关闭任务子线程。

参考文档

深入理解RunLoop

如果你喜欢这里的专题, 请直接添加关注哦, 如果你喜欢这里的总结, 可以打赏作者哦

一块钱是你小小的心意,也是作者无悔的付出, 这些总结的价值讲一直持续给你,每天

三篇文章,欢迎你来关注哦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,184评论 0 6
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 719评论 0 1
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,674评论 7 64
  • 妹妹中考完了天天在家待着,一次视频聊天的过程中,我突然想到让她锻炼一下演讲和写作,再做做卷子复习之前学过的东西。一...
    新生千寻阅读 507评论 8 3