iOS 记录runLoop与线程,runLoop与autorel
网上有很多关于runLoop的文章,但是看过了就忘记了,为了加深印象,不妨自己动手写写,很多理论都是网上学习到的,即便写完这篇记录,我也不是很理解runLoop。
一:线程与runLoop
看过面试题的人都知道runLoop,简单的理解就是跑圈,runLoop其实和线程是一一对应的,我们都知道主线程(UI线程),为什么主线程不会像子线程一样,执行完一段代码就被销毁掉,因为在主线程下有一个runLoop,这个runLoop不断循环接收各种事件,保证主线程不会因为无事儿可做而被销毁
那么子线程就没有runLoop吗?
默认情况下,子线程没有创建runLoop,但是可以手动创建,当我们在子线程中获取当前线程的runLoop时,就会自动创建一个runLopp,系统内部创建的代码如下,了解即可
// 拿到当前Runloop 调用_CFRunLoopGet0
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) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典里面拿,将线程作为key从字典里获取一个loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
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)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
二:看一下主线程的runLoop是怎样工作的
1:runLoop的活动周期
public static var entry: CFRunLoopActivity { get }
public static var beforeTimers: CFRunLoopActivity { get }
public static var beforeSources: CFRunLoopActivity { get }
public static var beforeWaiting: CFRunLoopActivity { get }
public static var afterWaiting: CFRunLoopActivity { get }
public static var exit: CFRunLoopActivity { get }
但大体来说有两种状态,工作和睡觉,当我们触发一些事件,这些事件包括source0、source1、timer其实我也不太清楚具体哪些事件
source0应该是各种点击事件、函数调用之内的,比如我们点击按钮
image.png
source1貌似是线程之间的通信,可以添加个断点了解下
image.png
image.png
timer是定时任务,我们启动一个定时器
image.png
这时候runLoop就是开始工作,当事件处理完成,runLoop就会去睡觉,等待下一个电话把它叫醒。
我们可以监听一下主线程下的runLoop的状态
// 1: 给主线程的runloop添加监听
func mainObserver() {
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFOptionFlags.max, true, 0) { (observer, activity) in
switch activity {
case CFRunLoopActivity.entry:
print("即将进入runloop")
break
case CFRunLoopActivity.beforeTimers:
print("即将处理timer")
break
case CFRunLoopActivity.beforeSources:
print("即将处理input Sources")
break
case CFRunLoopActivity.beforeWaiting:
print("即将睡眠")
break
case CFRunLoopActivity.afterWaiting:
print("从睡眠中唤醒,处理完唤醒源之前")
break
case CFRunLoopActivity.exit:
print("退出")
break
default:
print("other")
break
}
}
// 添加监听到当前的runloop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, CFRunLoopMode.defaultMode)
// 主线程的runloop已经运行了
// CFRunLoopRun()
}
然后我们就可以看到很多状态,大致在这个周期内,不断循环,永远不会切换到CFRunLoopActivity.exit状态
从睡眠中唤醒,处理完唤醒源之前
即将处理timer
即将处理input Sources
即将处理timer
即将处理input Sources
即将睡眠
从睡眠中唤醒,处理完唤醒源之前
即将处理timer
即将处理input Sources
即将处理timer
即将处理input Sources
即将睡眠
我们在OC项目的main.m中可以修改一下代码
int main(int argc, char * argv[]) {
@autoreleasepool {
int x = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"打印 %d", x);
return x;
}
}
这里添加的打印不会被执行,我们看看UIApplicationMain函数里面会执行什么
image.png
从图中可以看到,主线程中会启动runLoop,而主线程的runLoop会保证主线程不被销毁,也就是从UIApplicationMain函数进入,主线程会一直运行下去,不会调用return返回一个值
三:看一下子线程的runLoop
其实主线程的runLoop我们几乎不可用,也干不了什么事儿,主要是了解一下
但是我们可以在子线程中去使用runLoop干一些事儿
👇在子线程中获取了当前线程的runLoop,其实为我们创建了一个子线程的runLoop,如果我们注释掉
// RunLoop.current.add(Port.init(), forMode: RunLoop.Mode.default)
// RunLoop.current.run(until: Date.init(timeIntervalSinceNow: 10))
这两行代码,下面的程序不会打印任何信息,因为runLoop中没有监听任何输入源,直接就结束了
// 2: 给子线程的runLoop添加监听
func subObserver() {
// 全局队列获取一个子线程
subThread = Thread.init(block: {
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFOptionFlags.max, true, 0) { (observer, activity) in
switch activity {
case CFRunLoopActivity.entry:
print("子线程即将进入runloop")
break
case CFRunLoopActivity.beforeTimers:
print("子线程即将处理timer")
break
case CFRunLoopActivity.beforeSources:
print("子线程即将处理input Sources")
break
case CFRunLoopActivity.beforeWaiting:
print("子线程即将睡眠")
break
case CFRunLoopActivity.afterWaiting:
print("子线程从睡眠中唤醒,处理完唤醒源之前")
break
case CFRunLoopActivity.exit:
print("子线程退出")
break
default:
print("子线程other")
break
}
}
self.subRunLoop = RunLoop.current
// 添加监听到当前的runloop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, CFRunLoopMode.defaultMode)
// RunLoop.current.add(Port.init(), forMode: RunLoop.Mode.default)
// RunLoop.current.run(until: Date.init(timeIntervalSinceNow: 10))
CFRunLoopRun()
})
subThread?.start()
}
此时,我们来试试,指定一个方法到subThread线程上去执行
@IBAction func subLoopAction(_ sender: Any) {
// waitUntilDone: true会阻塞主线程
perform(#selector(subLoop), on: subThread!, with: nil, waitUntilDone: false)
}
@objc func subLoop() {
for i in 0...10 {
sleep(1)
print("sub \(i)")
}
}
程序会出现崩溃,因为此时的subThread子线程已经被销毁掉了,为了让子线程不会被快速销毁,我们就需要让子线程的runLoop运行下去
我们可以给runLoop添加一个输入源,比如timer,或者监听一个port
RunLoop.current.add(Port.init(), forMode: RunLoop.Mode.default)
这样子线程就不会被销毁,然后我们再运行试试看
image.png
好了,我们已经可以让子线程能够保持下去了,AFNetWorking里面就用到了runLoop去保存子线程不被销毁
我们也可以让runLoop在限定的时间内运行,超过这个时间,子线程就会被销毁
RunLoop.current.run(until: Date.init(timeIntervalSinceNow: 10))
让runLoop在接下来的10秒内有效,修改上面的代码
func subObserver() {
// 全局队列获取一个子线程
subThread = Thread.init(block: {
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFOptionFlags.max, true, 0) { (observer, activity) in
switch activity {
case CFRunLoopActivity.entry:
print("子线程即将进入runloop")
break
case CFRunLoopActivity.beforeTimers:
print("子线程即将处理timer")
break
case CFRunLoopActivity.beforeSources:
print("子线程即将处理input Sources")
break
case CFRunLoopActivity.beforeWaiting:
print("子线程即将睡眠")
break
case CFRunLoopActivity.afterWaiting:
print("子线程从睡眠中唤醒,处理完唤醒源之前")
break
case CFRunLoopActivity.exit:
print("子线程退出")
break
default:
print("子线程other")
break
}
}
self.subRunLoop = RunLoop.current
// 添加监听到当前的runloop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, CFRunLoopMode.defaultMode)
// RunLoop.current.add(Port.init(), forMode: RunLoop.Mode.default)
// RunLoop.current.run(mode: RunLoop.Mode.default, before: Date.init(timeIntervalSinceNow: 10))
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (_) in
print("xxxxx")
})
// 需要放到任务下面
RunLoop.current.run(until: Date.init(timeIntervalSinceNow: 5))
// CFRunLoopRun()
})
subThread?.start()
}
image.png
5秒钟后子线程销毁,内部的定时任务也不会继续执行,也可以添加延时执行任务
self.perform(#selector(self.subLoop), with: nil, afterDelay: 12)
超过5秒,也不会执行
四:runLoop和autoreleasepool
关于autoreleasepool和runLoop网上也有很多文章进行了讲解,这里我只是简单的把自己的理解写出来,如果有错,欢迎指正
先看看系统下的autoreleasepool是在什么时候开启的,打一个断点
我们启动程序,会立即停在断点处,因为我们都知道在main.m中就有使用到autoreleasepool image.png 我们跳过断点,让程序正常运行,然后放着不动等待一段时间,或者点击屏幕,我们发现程序自动又断到了这个断点 image.png
这时候,我们可以通过之前添加的主线程runLoop状态监听发现当runLoop状态切换到beforeWaiting时,断点就会断下
通过上面的图,可以发现,runLoop状态改变会执行回调方法,CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION,这个回调后有两个关于autoreleasepool的方法名 image.png image.png
这里有一个pop()方法,和push()的方法
由此我们可以大概的推测,runLoop在进入睡眠之前,会将当前创建的释放池进行销毁,随即立马创建一个新的释放池,等待下一个周期到来释放
然后我们回到main.m中,修改一下代码
int main(int argc, char * argv[]) {
@autoreleasepool {
}
}
使用编译命令clang -rewrite-objc main.m,将main.m文件编译成main.cpp文件
image.png
我们可以看到一个结构体__AtAutoreleasePool,这个结构体中有 objc_autoreleasePoolPush()方法,这个方法返回一个pool对象atautoreleasepoolobj,objc_autoreleasePoolPop(atautoreleasepoolobj)方法,接收一个pool对象为参数。
关于autoreleasepool的使用场景,网上很多文章都有介绍,这里摘抄一下
1.写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时
2.写循环,循环里面包含了大量临时创建的对象
3.创建了新的线程(非Cocoa程序创建线程时才需要,所以平时我们创建子线程的时候并没有特意的去添加autoreleasepool)
4.长时间在后台运行的任务
给for循环中添加autoreleasepool,释放每次循环创建的临时对象,对于这个autoreleasepool的释放则是根据pool的作用域,超过其作用域就会释放
for _ in 0...10000 {
autoreleasepool {
let view1: UIView? = UIView.init()
print(view1.debugDescription)
}
}
另外,更详细关于__AtAutoreleasePool是如果创建,如果将对象添加到pool中,以及如何释放的pool中的对象的,大家可以看看其他文章,参考文章
https://www.jianshu.com/p/50bdd8438857
https://www.jianshu.com/p/b875065074f2
https://www.jianshu.com/p/733447ca44ae