关于runloop

今天在学习线程安全的时候,看到了多线程编程指南,提到了关于runloop的部分,再学习一下runloop。

1.什么是runloop

runloop其实是线程相关的基础框架的一部分,如果没有runloop的话一个线程在执行完一个任务以后就会退出,而使用了runloop就可以让线程随时处理事物。然而为了节省资源,线程也不是一直在工作,runloop让线程在有工作的时候工作,没工作的时候休息(不占用内存资源)。

2.CFRunLoopRef和NSRunLoop

CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

3.runloop与多线程

系统不允许我们直接创建runloop,只提供了两个自动获取runloop的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。

每条线程都有唯一一个与之对应的Runloop对象(其关系保存在一个全局字典里)。主线程的Runloop已经自动创建好了,子线程的Runloop需要主动创建,如果在子线程里不主动创建runloop那就这个线程里就一直不会有runloop。

除了主线程以外,只能在线程的内部获取他的runloop。

Runloop在第一次获取时创建(获取runloop的时候就自动创建了),在线程结束时销毁(主线程的runloop除外)。⚠️:runloop不是直接创建的,而是在我们获取的时候自动创建的。

4.runloop的mode(模式)

每个mode里包括了若干个source、timer、observer。每个runloop里又包含了若干个mode。每次调用runloop的时候要指定一个mode,这个mode就是currentMode。只有和这个currentMode相关联的源才会被执行,然而其他的源要等到他们所在的mode被执行的时候才会被执行。

如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。

mode的名字是可以自定义的,但是mode里的内容必须有source、timer、observer否则就没有意义了。

source+timer+observer = mode item,一个mode item可以被同时加入多个mode,但是如果一个modeitem多次加入同一个mode是无效的,如果一个mode里没有modeitem,那么runloop就会退出循环。

应用:

在kCFRunLoopDefaultMode模式下添加一个NSTimer,不拖动scrollview的时候定时器正常工作,拖动scrollview了以后,相当于切换到了UITrackingRunLoopMode模式,但是定时器还在原来的模式里,定时器停止工作。等到停止拖拽以后,定时器继续工作。

NSRunLoopCommonModes可以做到让定时器在不拖拽和拖拽的时候都能工作。NSRunLoopCommonModes =kCFRunLoopDefaultMode +UITrackingRunLoopMode。

NSRunLoopCommonModes其实不是一种真正的运行模式,他其实是一种标签(每当runloop发生变化都会把mode里的mode item添加到带有common标签的mode里)。

5.runloop的内部实现

CFRunLoopSourceRef 产生事件,source0和source1

Source0 只包含了一个回调,他并不能主动出发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef  是基于时间的触发器。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 观察者用来观察runloop状态的改变。

📓:CFRunLoopTimerRef和NSTimer是toll-free-bridged的。

什么是toll-free-bridged呢,也叫做免费桥,就是说把Core Foundation框架对象和Foundation框架对象进行转换,由于这种转换不需要使用额外的cpu资源所以叫免费桥。

❓:Core Foundation框架和Foundation框架有什么区别呢?

这两个框架都提供了相同功能的api,不同的是cf框架提供的是c语言的接口,Foundation框架提供的是oc接口。无论是哪个框架生成的对象都可以在两个框架里使用,就是用toll-free-bridged去转换。

三种转换方式:

1. __bridge,什么也不做,仅仅是转换。此种情况下:

    i). 从Cocoa转换到Core,需要人工CFRetain,否则,Cocoa指针释放后, 传出去的指针则无效。

    ii). 从Core转换到Cocoa,需要人工CFRelease,否则,Cocoa指针释放后,对象引用计数仍为1,不会被销毁。

2. __bridge_retained,转换后自动调用CFRetain,即帮助自动解决上述i的情形。相当于CFBridgingRetain方法。

3. __bridge_transfer,转换后自动调用CFRelease,即帮助自动解决上述ii的情形。相当于CFBridgingRelease方法。

6.runloop事件执行过程

每次运行 runloop,你线程的 run loop 对会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:

1.通知观察者 runloop 已经启动

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

调用 _objc_autoreleasePoolPush();方法,创建新的自动释放池。

2.通知观察者任何即将触发timer回调

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

3.通知观察者即将启动的非基于端口的源source0回调

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

4.启动任何准备好的非基于端口的源source0回调

5.如果基于端口的源source1准备好并处于等待状态,立即启动;并进入步骤 9。

⚠️:source1可以主动唤醒runloop。

6.通知观察者线程进入休眠kCFRunLoopBeforeWaiting

⚠️:观察者即将进入休眠时会释放旧的runloop,autoreleasePoolPop。并且创建新的runloop,autoreleasePoolPush。

7.将线程置于休眠直到任一下面的事件发生:

mach_msg() -> mach_msg_trap();休眠状态

 某一事件到达基于端口的源source1port

 定时器启动(定时器会设定一个时间,到了那个时间以后runloop会执行定时器的回调)

 Run loop 设置的时间已经超时

 run loop 被显式唤醒

8.通知观察者线程将被唤醒。kCFRunLoopAfterWaiting

9.处理未处理的事件handle_msg

 如果用户定义的定时器启动,处理定时器事件并重启 run loop。进入步骤 2

 如果输入source1,启动

如果 run loop 被显式唤醒而且时间还没超时,重启 run loop。进入步骤 2

⚠️:runloop一直在循环里执行,那么什么时候才会退出这个循环呢。就是在超时或者手动停止的时候。runloop退出的时候状态时这样的:kCFRunLoopExit。这个时候会释放自动释放池, _objc_autoreleasePoolPop()。

7.runloop与autoreleasePool

我们都知道,当把一个对象添加到autoreleasePool之后相当于延迟了对象的销毁,自动释放池中的对象在自动释放池被销毁(pool drain)的时候会被发送release方法,也就是说引用计数会减1。引用计数减1并不代表这个对象被销毁!

在MRC环境下,可以用NSAutoreleasePool这个类声明一个autoreleasePool,给一个对象发送autorelease消息就相当于把这个对象添加到autoreleasePool对象里。

也就是说这个方法:

[obj autorelease];

就相当于:

-(id)autorelease{

    [NSAutoreleasePool addObject:self];

那么addObject方法是这样实现的:

-(void)addObject:(id)anObj{

    [array addObject:anObj];

}也就是把这个被发送了autorelease消息的对象添加到当前正在使用的NSAutoreleasePool的数组里。

当前正在使用的NSAutoreleasePool被销毁的时候会调用drain方法,drain方法相当于给自动释放池里的每个对象发送了release消息。

-(void)drain{ [self dealloc];}

-(void)dealloc{

    [self emptyPool];

    [array release];

}

-(void)emptyPool{

    for(id obj in array){[obj release];}

}

那么autoreleasePool是怎么实现的呢?

其实NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];

相当于调用了objc_autoreleasePoolPush();方法。

[obj autorelease];

相当于调用了objc_autorelease(obj);方法。

[pool drain];

相当于调用了objc_autoreleasePoolPop();方法。

⚠️:不能给一个NSAutoreleasePool对象发送autorelease消息,因为这个对象的autorelease方法已经被重载了,运行时报错。

!!!那么在arc环境下是不能使用NSAutoreleasePool去创建自动释放池的,也不能调用autorelease方法,那么是怎么实现自动释放池的呢❓

用@autorelease{}来代替NSAutoreleasePool对象的生命和销毁。

❓但是,并不能说自动释放池就是在大括号结束的位置销毁的!参考:黑幕背后的autoreleasePool

那么自动释放池是在什么时候被销毁的呢?

在没有手加Autorelease Pool(@autoreleasepool{})的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。

在每一个runloop的迭代过程中,在runloop里会注册两个observer,第一个observer监测即将进入runloop状态(kCFRunLoopEntry),并且调用_objc_autoreleasePoolPush()方法,创建一个自动释放池,这个方法的优先级最高,确保创建自动释放池的操作在其他所有操作之前执行。

第二个observer监测runloop的两个状态,一个是BeforeWaiting。在监测到这个状态以后调用_objc_autoreleasePoolPop()方法,销毁当前的自动释放池。并且调用_objc_autoreleasePoolPush()方法,创建新的自动释放池。另一个是exit,即将退出runloop这个状态。这个时候调用_objc_autoreleasePoolPush()方法,销毁自动释放池。

⚠️:什么时候需要我们手动创建自动释放池呢?

比如说,我们创建了大量的autorelease对象,都放在一个自动释放池里,这些对象占据的内存很大,如果一直不主动销毁自动释放池就会引起内存的暴增。这个时候就需要我们手动创建一个自动释放池。

🙋☝️🌰

for(int i = 0;i<count;<i++){

    @autoreleasePool{

        //读入图像并对图像进行处理

    }

}//这样就避免了内存暴增

8.runloop和UI刷新

当UI发生变化的时候,比如说视图的frame变化,图层的层级变化,或者调用了setNeedsLayout/setNeedsDisplay方法的时候就需要刷新UI,这些需要修改的控件被标记为待处理,runloop的观察者观察到runloop即将进入睡眠状态或者即将退出的时候会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

🐶:关于runloop对于tableView的优化思路。

场景1:

有些时候我们希望在TableView滑动的时候不去加载图片,而等到TableView停止滑动的时候再去加载。

解决方案1:

设置TableView的代理方法,判断TableView滑动的情况。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{}

解决方案2:

把设置图片放到一个方法里,设置图片的时候调用这个方法并且设置在runloop的defaultmode下执行,也就是说在滑动的时候就不会调用设置图片的方法。

🌰.jpg

场景2:UITableView里加载多张图片时非常容易造成界面的卡顿,可以利用预加载,缓存等方法来进行优化,监听runloop也可以作为一个优化的思路。

参考:RunLoop UITableViewCell加载高清大图的速度优化

这里面提到的一种思路是,我们加载图片的时候之所以会卡顿是因为我们在一个runloop里加载了多张图片,如果我们换一种做法,在一个runloop里一次加载一张图片,那么就肯定u会卡顿了。

1,因为这里用到了Runloop循环,那么我们可以监听到runloop的每次循环,在每一次循环当中我们考虑去进行一次图片下载和布局。

2,既然要在每次循环执行一次任务,我们可以先把所有图片加载的任务代码块添加到一个数组当中,每次循环取出第一个任务进行执行。

3,因为runloop在闲置的时候会自动休眠,所以我们要想办法让runloop始终处于循环中的状态。

关键的代码就是要创建一个监听者,监听runloop即将进入休眠的状态。

-(void)addRunloopObserver{

    //获取当前的RunLoop

    CFRunLoopRef runloop = CFRunLoopGetCurrent();

    //定义一个centext

    CFRunLoopObserverContext context = {

        0,

        ( __bridge void *)(self),

        &CFRetain,

        &CFRelease,

        NULL

    };

    //定义一个观察者

    static CFRunLoopObserverRef defaultModeObsever;

    //创建观察者

    defaultModeObsever = CFRunLoopObserverCreate(NULL,

                                                kCFRunLoopBeforeWaiting,

                                                YES,

                                                NSIntegerMax - 999,

                                                &Callback,

                                                &context

                                                );

    //添加当前RunLoop的观察者

    CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);

    //c语言有creat 就需要release

    CFRelease(defaultModeObsever);

}

还有就是需要设置一个nstimer让runloop一直处于运行状态。

//定时器,保证runloop一直处于循环中

@property (nonatomic, weak) NSTimer *timer;

self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(setRunLoop) userInfo:nil repeats:YES];

//此方法主要是利用计时器事件保持runloop处于循环中,不用做任何处理

-(void)setRunLoop{}

在runloop循环里处理事件

//MARK: 回调函数

//定义一个回调函数  一次RunLoop来一次

static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){

    ViewController * vcSelf = (__bridge ViewController *)(info);

    if (vcSelf.saveTaskMarr.count > 0) {

        //获取一次数组里面的任务并执行

        SaveFuncBlock funcBlock = vcSelf.saveTaskMarr.firstObject;

        funcBlock();

        [vcSelf.saveTaskMarr removeObjectAtIndex:0];

    }

}

参考视频:synnyxx关于runloop的线下分享会

待读:iOS 保持界面流畅的技巧

9.关于runloop和AFNetworking

网上有很多资料说在AFNetworking里使用runloop创建了一个常驻线程,让后台线程不退出,一直处于运行状态,这是在AFNetworking2.x版本里采用的方法,在AFNetworking3.x版本里已经用NSURLSession替换了NSURLConnection,也就没有这种操作了。

首先要说明为什么需要加一个常驻的runloop。

首先了解一下使用NSURLConnection代理方法监听下载任务时的几种情况。

回顾一下NSURLConnection发送请求的方法:

发送同步请求:

[NSURLConnectionsendSynchronousRequest:requestreturningResponse:&responseerror:nil];⚠️:发送同步请求的问题在于会阻塞当前线程,并且请求到的数据一次性回传,放到一个对象里,会引起内存的暴增。

发送异步请求:

[NSURLConnectionsendAsynchronousRequest:requestqueue:[NSOperationQueuemainQueue]completionHandler:^(NSURLResponse  *_Nullableresponse,NSData*_Nullabledata,NSError*_NullableconnectionError) {执行的block块}⚠️:发送异步请求的问题在于请求到的数据会被放到一个对象里,引起内存的增加。

通过代理发送异步请求:

NSURLConnection*connect =[[NSURLConnectionalloc]initWithRequest:requestdelegate:self];

⚠️:在代理方法里可以将下载的数据拼接起来并显示进度,但是内存也会飙升

1.在主线程调异步接口

当在主线程调用[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]时,请求发出,侦听任务(代理方法,回调函数)会加入到主线程的Runloop下,RunloopMode会默认为NSDefaultRunLoopMode。那也就意味着,当runloop的mode改变了以后就不会执行侦听任务。也就是说当用户在滑动tableview的时候,不会执行代理回调方法,只有停止滑动,mode切换成defaultMode以后才能执行代理方法。

解决方法:手动把runloopMode设置为NSRunLoopCommonModes。

⚠️:NSURLConnection类型的对象connect是一个局部变量,函数执行完就应该被销毁了为什么还可以执行代理方法呢?

因为这个方法内部其实会将connect作为一个source添加到当前的runloop中,并且执行运行模式为默认,等所有的代理方法执行完以后再销毁。

2.在子线程调同步接口

若在子线程调用同步接口,一条线程只能处理一个请求,因为请求一发出去线程就阻塞住等待回调,需要给每个请求新建一个线程,这是很浪费的,这种方式唯一的好处应该是易于控制请求并发的数量。

3.在子线程调异步接口⚠️

在子线程发送异步请求就需要把这个connection放到子线程的runloop里面去,但是子线程的runloop是需要手动开启的,如果每次在一个子线程里执行一个connection就开启一个子线程对应的runloop是非常不好的,所以AFNetworking采取的办法就是开启一个常驻的子线程,所有的请求共用一个响应线程,创建了一条常驻线程专门处理所有请求的回调事件。

⚠️那为什么在使用NSURLSession的时候就不需要创建常驻子线程了呢?

NSURLSessionTask会在子线程工作,不会阻碍主线程。在AF3.0没有再使用runloop来接受回调事件,而是创建NSURLSession时,传入了一个子操作队列NSOperationQueue,所有的回调事件都在这个操作队列中处理。Runloop的模式不会影响它的回调。

sessionWithConfiguration:delegate:delegateQueue:xxx

AF3.0在xxx这里传入了一个子操作队列,让所有的回调都在这个操作队列执行。

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

推荐阅读更多精彩内容