iOS 收藏篇

iOS 记录runLoop与线程,runLoop与autorel

2019-08-15  本文已影响0人  煎包小混沌

网上有很多关于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中的对象的,大家可以看看其他文章,参考文章
https://www.jianshu.com/p/50bdd8438857
https://www.jianshu.com/p/b875065074f2
https://www.jianshu.com/p/733447ca44ae

上一篇下一篇

猜你喜欢

热点阅读