iOS 记录runLoop与线程,runLoop与autoreleasepool

网上有很多关于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是在什么时候开启的,打一个断点

image.png

我们启动程序,会立即停在断点处,因为我们都知道在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中的对象的,大家可以看看其他文章,参考文章
//www.greatytc.com/p/50bdd8438857
//www.greatytc.com/p/b875065074f2
//www.greatytc.com/p/733447ca44ae

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

推荐阅读更多精彩内容