标签:
RunLoop用中文来翻译就是“跑圈”,“运行循环”。
RunLoop的基本作用:
如果没有RunLoop:
程序运行后的现象:刚点击运行,就立刻结束了!
如果有了RunLoop:
有了RunLoop后,整个程序启动后并不会马上退出,而是保持持续运行状态!
但是真实情况下的main函数长这样:
其实在执行UIApplicationMain函数时,其内部就启动了一个RunLoop,而且该函数一直没有返回,保持了程序的持续运行!当我们点击UIApplicationMain函数里面去,会发现该函数是有返回值(int类型),只不过一直没有返回而已,接下来可以测试下!
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"begin---");
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"end---");
return result;
}
}
当我们运行程序后,打印如下:
这就表明,确实是在UIApplicationMain函数中执行了RunLoop,而且这个默认启动的RunLoop是跟主线程相关联的!
在iOS系统中,提供了两个对象:NSRunLoop和CFRunLoopRef。
因此如果想深入学习RunLoop,就需要多了解CFRunLoopRef层面的API(Core Foundation),可以参考:
官方源代码
官方文档
1> 每条线程都有唯一的与之对应的RunLoop对象
在RunLoop源代码中,有这样的一个函数:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
这个函数的作用就是:传一条线程来创建一个RunLoop对象,这就对应了上面的这句话
2> 主线程的Runloop已经自动创建好了,子线程的RunLoop需要主动创建
苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数:
Foundation:
[NSRunLoop mainRunLoop];
[NSRunLoop currentRunLoop];
Core Foundation:
CFRunLoopGetMain();
CFRunLoopGetCurrent();
3> RunLoop在第一次获取时创建,在线程结束时销毁
以下是源代码的解读:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { // 这个函数就是传一条线程进来,就创建一个RunLoop
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 判断有没有RunLoop对象
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
// 创建可变Dictionary
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 获取主线程对应的RunLoop对象
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 把mainLoop放到Dictionary中,key就是主线程(也就是说在访问其他线程的时候,它会首先把主线程的RunLoop创建好)
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 根据传进来的线程获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果没有RunLoop,就创建
if (!loop) {
// 根据传进来的线程创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 把newLoop存进Dictionary
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)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
上述代码中就可以看出,RunLoop和线程是一一对应的,其关系会保存在一个Dictionary中。线程刚创建的时候并没有RunLoop,因为它是懒加载的,需要主动获取(在子线程内部通过currentRunLoop获取),第一次获取当前线程对应的RunLoop时就会创建,以后再不会创建!
Core Foundation中关于RunLoop的5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
如果RunLoop中没有上面的几个类,RunLoop就会马上结束了。
这些类的关系如下:
一个RunLoop包含很多Mode,Mode中又包含很多Source、Observer、Timer
CFRunLoopModeRef:
CFRunLoopModeRef代表着RunLoop的运行模式。
在获取RunLoop后还需要传入一种运行模式,让其跑起来。每次RunLoop启动时,需要指定其中一种Mode,这个Mode被称为CurrentMode,从上面的图看出,一个RunLoop有很多种模式。如果需要切换Mode,只能退出Loop,然后再重新指定一个Mode进入,这样做的目的就是为了分隔开不同组的Source/Observer/Timer,让其互不影响。
系统默认注册了5个Mode:
kCFRunLoopDefaultMode:App的默认Mode,通常主线程都是在这个Mode下运行
UITrackingRunLoopDefaultMode:界面跟踪Mode,用于scrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响,这样做可以使其更加流畅
UIInitializationRunLoopMode:在刚启动App时的第一个Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode:接受系统内部事件的Mode,通常用不到
NSRunLoopCommonMode:这是一个占位用的Mode,不是一种真正的Mode(也就是说启动的时候,是不可以传这种模式的)
它是基于时间的触发器(时间到了就触发一个事件),基本上就是我们平常所知道的NSTimer。它包含一个时间长度和一个回调(函数指针),当加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调
实用场景案例:
现在屏幕上有一个可滚动的UITextView,此时我们假如我们需要添加一个定时器来执行一些其他操作
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
其实这句代码做了很多事情,那就是首先创建一个NSTimer,添加到Mode中,接着RunLoop启动Mode,取出里面的Timer拿出来用。等价代码如下:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
这时每过2秒就会回调一次doSomeThing方法,但是当我们滚动UITextView时,doSomeThing方法就不再会执行了。因为该NSTimer是添加到NSDefaultRunLoopMode中的,而滚动UITextView是属于UITrackingRunLoopDefaultMode。那么如果想在defaultMode和TrackingMode下都能执行该怎么办呢?
当我们点进头文件,苹果只给我们提供了两种Mode:NSDefaultRunLoopMode/NSRunLoopCommonModes,并没有TrackingMode。因此得用到NSRunLoopCommonMode了,这就表示定时器会跑在标记为common modes的模式下。在我们打印currentRunLoop时:
带有common modes的Mode有两个,分别是kCFRunLoopDefaultMode和UITrackingRunLoopDefaultMode,这就表明这两种Mode下,定时器都可以用。
它是事件源(输入源),意味着一些点击事件、触摸事件都是由它来处理的。
按照函数调用栈,scorce的分类:
至于什么是函数栈:
比如触发一个点击事件,会按照上图所示,从下到上依次执行函数。很明显,里面就有Source0这个函数,说明点击事件在Source0中进行。虽然函数栈中并没有Source1,但是当触摸屏幕时会被包装成一个Event,首先会被Source1接受,然后把这个事件分发到Source0。
它是观察者,能够监听RunLoop状态的改变。每一个Observer都包含着一个回调,当RunLoop状态发生改变的时候,Observer就会通过回调接受这个变化。
// 添加observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // 这里就可以监听状态的改变,然后做出一些操作
});
// 添加观察着,监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer
CFRelease(observer);
RunLoop的状态有以下几个时间点:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1,即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2,即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4,即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64,从休眠中被唤醒
kCFRunLoopExit = (1UL << 7), // 128,退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包括以上所有状态
};
一幅图很好的说明了这个实现逻辑的过程
内部主要就是一个do-while循环,直到超时或者手动停止才会结束循环,该函数才会有返回值。
1> imageView延迟显示
当我们在滚动tableView时,就会从网络下载图片进行展示,但是图片显示的过程中会有渲染的问题,导致tableView拖动非常卡,这个时候就可以让imageView延迟显示。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"zly"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
也就是让imageView的显示只在NSDefaultRunLoopMode模式下才能进行,所以拖动tableView时是处于TrackingMode!
2> 常驻线程(让线程不死)
想要让当前线程不死,其实就可以用到RunLoop,在当前线程中开启一个RunLoop,但是需要注意的是此时的RunLoop的Mode中并没有Mode item,所以RunLoop就会立即退出,因此得在里面添加Mode item。
@autoreleasepool {
// 添加Source
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// run
[[NSRunLoop currentRunLoop] run];
}
标签:
原文地址:http://blog.csdn.net/ldszw/article/details/51321280