iOS Runloop & AutoReleasePool

Runloop概述

runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
  • runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。
  • runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
RunLoop的作用?
  • 1.保持程序运行
  • 2.处理app的各种事件(比如触摸,定时器等等)
  • 3.节省CPU资源,提高性能。
RunLoop内部是怎么实现的?
    1. RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
    1. 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
    1. 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
runloop的mode是用来做什么的?有几种mode?
  • model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode:
    (1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    (2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    (3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    (4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    (5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
Runloop启动过程

1.通知观察者 run loop 已经启动
2.通知观察者将要开始处理Timer事件
3.通知观察者将要处理非基于端口的Source0
4.启动准备好的Souecr0
5.如果基于端口的源Source1准备好并处于等待状态,立即启动:并进入步骤9
6.通知观察者线程进入休眠
7.将线程置于休眠直到任一下面的事件发生
(1)某一事件到达基于端口的源
(2)定时器启动
(3)Run loop 设置的时间已经超时
(4)run loop 被显式唤醒
8.通知观察者线程将被唤醒
9.处理未处理的事件,跳回2
(1)如果用户定义的定时器启动,处理定时器事件并重启 run loop。进入步骤 2
(2)如果输入源启动,传递相应的消息
(3)如果 run loop 被显式唤醒而且时间还没超时,重启 run loop。进入步骤 2
10.通知观察者run loop 结束

为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
  • NSTimer对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应nstime发送的消息。所以如果想在滑动scrollview的情况下面还调用nstime的消息,我们可以把nsrunloop的模式更改为NSRunLoopCommonModes.

与FPS的关系

使用实例

在开发中如何使用RunLoop?什么应用场景?
  1. 常驻线程:给子线程开启一个RunLoop,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出

  2. AutoreleasePool: iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool

    第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。

    第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。

3.UITableView 与 NSTimer 冲突
4.AFNetworking内部创建了一个空的线程并启动了RunLoop,当需要使用这个后台线程执行任务时AFNetworking通过performSelector: onThread: 将这个任务放到后台线程的RunLoop中。
5.防止UITableView滚动卡顿([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])
6.sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。
7.老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

[beforeSource, beforeWaiting], [AfterWaitng, ...]
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
     
    // 记录状态值
    object->activity = activity;
     
    // 发送信号
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}
 
- (void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
     
    // 创建信号
    semaphore = dispatch_semaphore_create(0);
     
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                     
                    NSLog(@"好像有点儿卡哦");
                }
            }
            timeoutCount = 0;
        }
    });
}

AutoReleasePool(深入解析 Autoreleasepool)

苹果是如何实现Autorelease Pool的?
  • Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的autoreleasePool中,等到autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease.

每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,以双向链表的形式连接起来的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节

class AutoreleasePoolPage {
    magic_t const magic; //对当前 AutoreleasePoolPage 完整性的校验
    id *next;
    pthread_t const thread; // 保存了当前页所在的线程
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
POOL_SENTINEL(哨兵对象)
  • POOL_SENTINEL 只是 nil 的别名。
  • 在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象
  • 当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL
objc_autoreleasePoolPush
  • 有 hotPage 并且当前 page 不满 , 调用 page->add(obj) 方法将对象添加
  • 有 hotPage 并且当前 page 已满 , 调用 autoreleaseFullPage 初始化一个新的页 , 调用 page->add(obj) 方法将对象添加
  • 无 hotPage , 调用 autoreleaseNoPage 创建一个 hotPage , 调用 page->add(obj) 方法将对象添加
  • page->add 添加对象 , 对next指针的一个压入操作
objc_autoreleasePoolPop
  • objc_autoreleasePoolPop 传入哨兵对象
总结

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息

使用场景

1.写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
2.写循环,循环里面包含了大量临时创建的对象。
3.创建了新的线程。(非Cocoa程序创建线程时才需要)
4.长时间在后台运行的任务。

第三方库使用举例
SDWebImage(SDWebImageDecoder.m文件中,3.7.0版本)

+ (UIImage *)decodedImageWithImage:(UIImage *)image {

    if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
        return nil;
    }
    ...
    @autoreleasepool{
        CGImageRef imageRef = image.CGImage;

        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
        if (anyAlpha) {
            return image;
        }
        ...
        return imageWithoutAlpha;
    }
}

此处操作是生成解压缩图片,该方法会在SDWebImageDownloaderOperation connection:(NSURLConnection *)connection didReceiveData:(NSData *)data方法中不停回调,该方法调用会发生在子线程,同时方法内绘制bitmap会生成大量临时对象,符合第二、第三两种情况

CocoaLumberjack gcd操作大量使用

- (void)addLogger:(id )logger withLevel:(DDLogLevel)level {
    dispatch_async(_loggingQueue, ^{ @autoreleasepool {
        [self lt_addLogger:logger level:level];
    } });
}
- (void)removeLogger:(id )logger {
    dispatch_async(_loggingQueue, ^{ @autoreleasepool {
        [self lt_removeLogger:logger];
    } });
}

dispatch_async使用自定义并发队列,系统并不会内部线程中添加autoreleasepool,所以手动添加autoreleasepool更好,此处如果使用globalQueue是不需要添加的,符合第三种使用条件

自动释放池什么时候释放?
  • 当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池

Runloop其他博客

掘金总结 https://juejin.im/entry/599c13bc6fb9a0248926a77d
iOS-RunLoop充满灵性的死循环 https://www.jianshu.com/p/b9426458fcf6
RunLoop入门 看我就够了 https://www.jianshu.com/p/2d3c8e084205
RunLoop总结:RunLoop的应用场景(三) https://blog.csdn.net/u011619283/article/details/53483965
iOS刨根问底-深入理解RunLoop https://www.cnblogs.com/kenshincui/p/6823841.html
iOS实时卡顿监控 http://www.cocoachina.com/ios/20161101/17903.html
iOS Runloop相关面试题

你可能感兴趣的:(iOS Runloop & AutoReleasePool)