iOS开发中Runloop和Runtime浅谈
Runloop
做了一年多的IOS开发,对IOS和Objective-C深层次的了解还十分有限,大多还停留在会用API的级别,这是件挺可悲的事情。想学好一门语言
还是需要深层次的了解它,这样才能在使用的时候得心应手,出现各种怪异的问题时不至于不知所措。废话少说,进入今天的正题。
不知道大家有没有想过这个问题,一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我
们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干
活的时候,它就能立刻响应。其实,这就是run loop的功劳。
一、线程与run loop
1.1 线程任务的类型
再来说说线程。有些线程执行的任务是一条直线,起点到终点;而另一些线程要干的活则是一个圆,不断循环,直到通过某种方式将它终止。直线线程如简单的
Hello
World,运行打印完,它的生命周期便结束了,像昙花一现那样;圆类型的如操作系统,一直运行直到你关机。在IOS中,圆型的线程就是通过run
loop不停的循环实现的。
1.2 线程与run loop的关系
Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run
loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run
loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run
loop(以下都已Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。
1.2.1 主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main() 函数:
intmain(intargc,char*argv[])
{
@autoreleasepool{
returnUIApplicationMain(argc, argv,nil,NSStringFromClass([appDelegateclass]));
}
}
重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
1.2.2 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
1.2.3 在任何一个Cocoa程序的线程中,都可以通过:
NSRunLoop*runloop = [NSRunLoopcurrentRunLoop];
来获取到当前线程的run loop。
1.3 关于run loop的几点说明
1.3.1 Cocoa中的NSRunLoop类并不是线程安全的
我们不能再一个线程中去操作另外一个线程的run
loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型
的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:
- (CFRunLoopRef)getCFRunLoop;
获取对应的CFRunLoopRef类,来达到线程安全的目的。
1.3.2 Run loop的管理并不完全是自动的。
我们仍必须设计线程代码以在适当的时候启动run loop并正确响应输入事件,当然前提是线程中需要用到run loop。而且,我们还需要使用while/for语句来驱动run loop能够循环运行,下面的代码就成功驱动了一个run loop:
BOOLisRunning =NO;
do{
isRunning = [[NSRunLoopcurrentRunLoop]runMode:NSDefaultRunLoopModebeforeDate:[NSDatedistantFuture]];
}while(isRunning);
1.3.3 Run loop同时也负责autorelease pool的创建和释放
在使用手动的内存管理方式的项目中,会经常用到很多自动释放的对象,如果这些对象不能够被即时释放掉,会造成内存占用量急剧增大。Run
loop就为我们做了这样的工作,每当一个运行循环结束的时候,它都会释放一次autorelease
pool,同时pool中的所有自动释放类型变量都会被释放掉。
1.3.4 Run loop的优点
一个run
loop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。如果仅仅是想实现这个功能,你可能会想一个简单的
while循环不就可以实现了吗,用得着费老大劲来做个那么复杂的机制?显然,苹果的架构设计师不是吃干饭的,你想到的他们早就想过了。
首先,NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的
具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source(见后文)中了。
其次,也是很重要的一点,使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。
二、Run loop相关知识点
2.1输入事件来源
Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。两种源都使用程序的某一特定的处理例程来处理到达的事件。图-1显示了run loop的概念结构以及各种源。
需要说明的是,当你创建输入源,你需要将其分配给run loop中的一个或多个模式(什么是模式,下文将会讲到)。模式只会在特定事件影响监听的源。大多数情况下,run loop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只在run loop运行在其关联的模式下才会被传递。
图-1 Runloop的结构和输入源类型
2.1.1输入源(input source)
传递异步事件,通常消息来自于其他线程或程序。输入源传递异步消息给相应的处理例程,并调用runUntilDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。
2.1.1.1基于端口的输入源
基于端口的输入源由内核自动发送。
Cocoa和Core
Foundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口
对象,并使用NSPort的方法把该端口添加到run loop。端口对象会自己处理创建和配置输入源。
在Core Foundation,你必须人工创建端口和它的run
loop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。
下面的例子展示了如何创建一个基于端口的输入源,将其添加到run loop并启动:
voidcreatePortSource()
{
CFMessagePortRefport =CFMessagePortCreateLocal(kCFAllocatorDefault,CFSTR("com.someport"),myCallbackFunc,NULL,NULL);
CFRunLoopSourceRefsource =CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port,0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopCommonModes);
while(pageStillLoading) {
NSAutoreleasePool*pool = [[NSAutoreleasePoolalloc]init];
CFRunLoopRun();
[poolrelease];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
CFRelease(source);
}
2.1.1.2自定义输入源
自定义的输入源需要人工从其他线程发送。
为了创建自定义输入源,必须使用Core
Foundation里面的CFRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Core
Fundation会在配置源的不同地方调用回调函数,处理输入事件,在源从run loop移除的时候清理它。
除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。创建并启动自定义输入源的示例如下:
voidcreateCustomSource()
{
CFRunLoopSourceContextcontext = {0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL};
CFRunLoopSourceRefsource =CFRunLoopSourceCreate(kCFAllocatorDefault,0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
while(pageStillLoading) {
NSAutoreleasePool*pool = [[NSAutoreleasePoolalloc]init];
CFRunLoopRun();
[poolrelease];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
CFRelease(source);
}
2.1.1.3Cocoa上的Selector源
除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector方法。和基于端口的源一样,执行selector请求会在目标线
程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从run loop里面移除。
当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。
NSObject类提供了类似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray*)array;
2.1.2定时源(timer source)
定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,不会立即退出run loop。
需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run
loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run
loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run
loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。
创建定时器源有两种方法,
方法一:
NSTimer *timer = [NSTimerscheduledTimerWithTimeInterval:4.0
target:self
selector:@selector(backgroundThreadFire:) userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timerforMode:NSDefaultRunLoopMode];
方法二:
[NSTimerscheduledTimerWithTimeInterval:10
target:self
selector:@selector(backgroundThreadFire:)
userInfo:nil
repeats:YES];
2.2 RunLoop观察者
源是在合适的同步或异步事件发生时触发,而run loop观察者则是在run loop本身运行的特定时候触发。你可以使用run loop观察者来为处理某一特定事件或是进入休眠的线程做准备。你可以将run loop观察者和以下事件关联:
1. Runloop入口
2. Runloop何时处理一个定时器
3. Runloop何时处理一个输入源
4. Runloop何时进入睡眠状态
5. Runloop何时被唤醒,但在唤醒之前要处理的事件
6. Runloop终止
和定时器类似,在创建的时候你可以指定run loop观察者可以只用一次或循环使用。若只用一次,那么在它启动后,会把它自己从run
loop里面移除,而循环的观察者则不会。定义观察者并把它添加到run loop,只能使用Core
Fundation。下面的例子演示了如何创建run loop的观察者:
- (void)addObserverToCurrentRunloop
{
// The application uses garbage collection, so noautorelease pool is needed.
NSRunLoop*myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the runloop.
CFRunLoopObserverContextcontext = {0,self,NULL,NULL,NULL};
CFRunLoopObserverRef observer =CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopBeforeTimers,YES,0, &myRunLoopObserver, &context);
if(observer)
{
CFRunLoopRefcfLoop = [myRunLoopgetCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
}
其中,kCFRunLoopBeforeTimers表示选择监听定时器触发前处理事件,后面的YES表示循环监听。
2.3 RunLoop的事件队列
每次运行run loop,你线程的run loop对会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:
通知观察者run loop已经启动
通知观察者任何即将要开始的定时器
通知观察者任何即将启动的非基于端口的源
启动任何准备好的非基于端口的源
如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9。
通知观察者线程进入休眠
将线程置于休眠直到任一下面的事件发生:
某一事件到达基于端口的源
定时器启动
Run loop设置的时间已经超时
run loop被显式唤醒
通知观察者线程将被唤醒。
处理未处理的事件
如果用户定义的定时器启动,处理定时器事件并重启run loop。进入步骤2
如果输入源启动,传递相应的消息
如果run loop被显式唤醒而且时间还没超时,重启run loop。进入步骤2
通知观察者run loop结束。
因为定时器和输入源的观察者是在相应的事件发生之前传递消息,所以通知的时间和实际事件发生的时间之间可能存在误差。如果需要精确时间控制,你可以使用休眠和唤醒通知来帮助你校对实际发生事件的时间。
因为当你运行run loop时定时器和其它周期性事件经常需要被传递,撤销run loop也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到消息而不是经由程序传递,因此活跃的定时器不会开始直到鼠标追踪结束并将控制权交给程序。
Run loop可以由run loop对象显式唤醒。其它消息也可以唤醒run loop。例如,添加新的非基于端口的源会唤醒run loop从而可以立即处理输入源而不需要等待其他事件发生后再处理。
从这个事件队列中可以看出:
①如果是事件到达,消息会被传递给相应的处理程序来处理, runloop处理完当次事件后,run loop会退出,而不管之前预定的时间到了没有。你可以重新启动run loop来等待下一事件。
②如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生。这就是为什么run loop可以做到让线程有工作的时候忙于工作,而没工作的时候处于休眠状态。
2.4什么时候使用run loop
仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个run loop。Run
loop是程序主线程基础设施的关键部分。所以,Cocoa和Carbon程序提供了代码运行主程序的循环并自动启动run
loop。IOS程序中UIApplication的run方法(或Mac OS
X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似
的,RunApplicationEventLoop函数为Carbon程序启动主循环。如果你使用xcode提供的模板创建你的程序,那你永远不需要自
己去显式的调用这些例程。
对于辅助线程,你需要判断一个run loop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的run
loop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动run loop。Run
loop在你要和线程有更多的交互时才需要,比如以下情况:
使用端口或自定义输入源来和其他线程通信
使用线程的定时器
Cocoa中使用任何performSelector…的方法
使线程周期性工作
如果你决定在程序中使用run loop,那么它的配置和启动都很简单。和所有线程编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。
-------- by wangzz
参考文档:
http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i
Runtime
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里
)。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编
译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找
到对应的函数来调用。
那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:
[obj makeText];
其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(makeText));
首先我们来看看obj这个对象,iOS中的obj都继承于NSObject。
@interface NSObject {
Class isa OBJC_ISA_AVAILABILITY;
}
在NSObjcet中存在一个Class的isa指针。然后我们看看Class:
typedef struct objc_class *Class;
struct objc_class {
Class isa;// 指向metaclass
Class super_class ;// 指向其父类
const char *name ;// 类名
long version ;// 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info;// 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size ;// 该类的实例变量大小(包括从父类继承下来的实例变量);
struct objc_ivar_list *ivars;// 用于存储每个成员变量的地址
struct objc_method_list **methodLists ;// 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache *cache;// 指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols;// 存储该类遵守的协议
}
我们可以看到,对于一个Class类中,存在很多东西,下面我来一一解释一下:
Class
isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对
象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方
法)。
Class super_class:指向父类,如果这个类是根类,则为NULL。
下面一张图片很好的描述了类和对象的继承关系:
注意:所有metaclass中isa指针都指向跟metaclass。而跟metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。
Class类中其他的成员这里就先不做过多解释了,下面我们来看看:
@selector (makeText):这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(makeText)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个Int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。
首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector
(makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
Runtime应用
1.什么是runtime?
runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime实现的。
2.runtime有什么作用?
1.能动态产生一个类,一个成员变量,一个方法
2.能动态修改一个类,一个成员变量,一个方法
3.能动态删除一个类,一个成员变量,一个方法
3.常用的头文件
#import包含对类、成员变量、属性、方法的操作#import包含消息机制
4.常用方法
class_copyIvarList()返回一个指向类的成员变量数组的指针
class_copyPropertyList()返回一个指向类的属性数组的指针
注意:根据Apple官方runtime.h文档所示,上面两个方法返回的指针,在使用完毕之后必须free()。
ivar_getName()获取成员变量名-->C类型的字符串property_getName()获取属性名-->C类型的字符串-------------------------------------typedef struct objc_method *Method;class_getInstanceMethod() class_getClassMethod()以上两个函数传入返回Method类型---------------------------------------------------method_exchangeImplementations()交换两个方法的实现
5.runtime在开发中的用途
1.动态的遍历一个类的所有成员变量,用于字典转模型,归档解档操作
代码如下:
- (void)viewDidLoad { [superviewDidLoad];/** 利用runtime遍历一个类的全部成员变量
1.导入头文件 */unsignedintcount =0;/** Ivar:表示成员变量类型 */Ivar *ivars = class_copyIvarList([BDPerson class], &count);//获得一个指向该类成员变量的指针for(inti =0; i < count; i ++) {//获得IvarIvar ivar = ivars[i];//根据ivar获得其成员变量的名称--->C语言的字符串constchar*name = ivar_getName(ivar);NSString*key = [NSStringstringWithUTF8String:name];NSLog(@"%d----%@",i,key);}}
运行结果如下:
成员变量遍历输出结果.png
获取一个类的全部属性:
获取类的属性的代码实现.png
结果如下:
输出结果.png
应用场景:
可以利用遍历类的属性,来快速的进行归档操作。
将从网络上下载的json数据进行字典转模型。
注意:归档解档需要遵守协议,实现以下两个方法
- (void)encodeWithCoder:(NSCoder*)encoder{
//归档存储自定义对象
unsignedintcount =0;
//获得指向该类所有属性的指针
objc_property_t *properties = class_copyPropertyList([BDPerson class], &count);
for(inti =0; i < count; i ++) {
//获得
objc_property_t property = properties[i];
//根据objc_property_t获得其属性的名称--->C语言的字符串
constchar*name = property_getName(property);
NSString*key = [NSStringstringWithUTF8String:name];
// 编码每个属性,利用kVC取出每个属性对应的数值
[encoder encodeObject:[selfvalueForKeyPath:key] forKey:key];
}
}
- (instancetype)initWithCoder:(NSCoder*)decoder{
//归档存储自定义对象
unsignedintcount =0;
//获得指向该类所有属性的指针
objc_property_t *properties = class_copyPropertyList([BDPerson class], &count);
for(inti =0; i < count; i ++) {
objc_property_t property = properties[i];
//根据objc_property_t获得其属性的名称--->C语言的字符串
constchar*name = property_getName(property);
NSString*key = [NSStringstringWithUTF8String:name];
//解码每个属性,利用kVC取出每个属性对应的数值
[selfsetValue:[decoder decodeObjectForKey:key] forKeyPath:key];
}
returnself;
}
二、交换方法
通过runtime的method_exchangeImplementations(Method m1, Method m2)方法,可以进行交换方法的实现;一般用自己写的方法(常用在自己写的框架中,添加某些防错措施)来替换系统的方法实现,常用的地方有:
在数组中,越界访问程序会崩,可以用自己的方法添加判断防止程序出现崩溃数组或字典中不能添加nil,如果添加程序会崩,用自己的方法替换系统防止系统崩溃。
代码实现如下:
运行程序崩溃.png
添加一个分类实现方法交换.png
再次运行刚才的程序:
最终运行结果图.png除了获取属性列表之外,还有方法调用,拦截调用,动态添加方法属性,关联对象(添加属性),方法交换,根据属性值获取属性名称(反射机制)等应用。
总结
runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, 所以大家有必要进行研究。
参考博客:
http://blog.csdn.net/ztp800201/article/details/9240913
http://www.jianshu.com/p/613916eea37f
http://www.jianshu.com/p/ebc6e20b84cf
http://www.cocoachina.com/ios/20141018/9960.html
http://www.jianshu.com/p/364eab29f4f5
http://www.jianshu.com/p/927c8384855a