深入浅出 RunLoop (2) — 应用实践

前言

接上篇 核心机制 ,本文主要介绍RunLoop在应用中的实践。
iOS/OS X系统中很多基础功能,比如自动释放池就是由RunLoop实现或者协助实现的,所以RunLoop是iOS系统中基础中的基础,组件中的组件。

  • 由RunLoop直接管理的机制有:自动释放池、定时器、视图刷新等机制。这些机制的生命周期完全由RunLoop管理。
  • 需要RunLoop支持的机制有:事件响应、动画、异步方法调用、网络请求等机制。这些机制借助了RunLoop的特性完成自己的功能。

管理自动释放池

自动释放池,AutoreleasePool有两种管理方式,一种方式是由程序员负责管理,通过AutoreleasePool块将块中的临时变量在出块的时候释放掉,主要在循环读取大文件中会用到。
另一种方式就是由系统管理AutoreleasePool的创建和销毁,实质上这个系统管理就是Runloop管理的。

App启动后,主线程RunLoop中会注册Observer,分别在RunLoopEntry、BeforeWaiting和Exit时调用。回调都是_wrapRunLoopWithAutoreleasePoolHandler()。

  • RunLoopEntry Observer 在RunLoop进入的时候会被触发,在_wrapRunLoopWithAutoreleasePoolHandler()函数中调用_objc_autoreleasePoolPush()创建自动释放池。order是- 2^31,优先级最高,确保在所有回调之前创建自动释放池。

  • RunLoopBeforWaiting Observer 在RunLoop进入休眠之前被触发,同样在_wrapRunLoopWithAutoreleasePoolHandler()函数中处理,但是调用的方法不同,分别调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()方法。释放旧的自动释放池,同时创建新的自动释放池。

  • RunLoopExit Observer 在RunLoop退出的时候被触发。调用_objc_autoreleasePoolPop()释放自动释放池。

由于RunLoop管理AutoreleasePool,所以在线程中执行代码,无论是事件回调,还是Timer回调都回被AutoreleasePool环绕,所以不会有内存泄露的问题。

管理定时器

timer是RunLoop的事件源之一,timer添加到RunLoop之后,RunLoop会在timer的时间点上注册定时事件。因为各种原因,RunLoop执行回调的时间点并不准确,可能在执行一个长任务,可能在其他mode下。Timer有个属性叫Tolerance,宽容度,这个属性标示了当timer被触发的时候同标定的时间点允许有多大的误差度。

如果超过宽容度在这个时间点timer的回调函数不会被执行。同样的如果某个时间点被错过了,则这个时间点也会被跳过,回调函数不会被触发执行。也就出现了,1秒执行一次的timer,理论上1分钟应该执行60次,但是出现了执行57、58次的情况。

NSTimer同CFRunLoopTimerRef 是 toll-free bridged的。底层由XNU 内核的mk_timer 驱动。

管理视图刷新

当视图内容更新的时候,调用layoutSubView方法进行重新布局,调用drawRect方法进行重绘。我们都知道在开发的时候不能直接调用layoutSubviews或者drawRect方法,而是调用setNeedsLayout方法触发重新布局,setNeedsDisplay方法触发重新绘制。这样做的目的是为了效率和流畅度,众所周知界面的重新布局和重新绘制都是非常耗时的操作,如果在短时间内频繁进行这个操作,CPU就没办法进行其他操作,影响app整体的运行效率和流畅度。所以将需要重排、重绘的View和Layer进行标记,在一次RunLoop循环中只进行一次重排、重绘操作。

这个视图刷新的机制就需要RunLoop去支持。RunLoop Observer会在即将进入休眠 BeforeWaiting 和 退出 Exit 的时候调用CFRunLoopObservermPv()函数,这个函数会遍历所有有标记的View和Layer,执行真正的重新布局和重新绘制方法。达到刷新视图界面的目的。

CFRunLoopObservermPv()

QuartzCore::observer_callback:
CA::commit();
CA::commit_transaction();
                layout_and_display_if_needed();
layout_if_needed();
                        
[CALayer layoutSublayers];
[UIView layoutSubviews];

display_if_needed();
[CALayer display];
[UIView drawRect];

支持事件响应

在RunLoop中事件源分为Source0和Source1。Source1事件是可以主动唤醒RunLoop的,Source1除了回调函数外还有一个mack_port端口,通过这个端口来接收系统事件,回调函数是_IOHIDEventSystemClientQueueCallback()。

当硬件事件如触屏、摇晃、翻转、锁屏,系统会由IOKit产生一个用户设备(human interface devices)事件。事件类型:IOHIDEvent。由SpringBoard组件负责接收。
SpringBoard 只接收按键,触屏,加速,接近传感器等4种 事件。之后通过mach_port发送给注册的应用进程,应用进程通过Source1事件源响应这个事件,并通过_UIApplicationHandleEventQueue()进行分发。

_UIApplicationHandleEventQueue方法会将IOHIDEvent对象封装成UIEvent对象再进行分发,手势、屏幕旋转交给Window处理,点击事件交给响应者链处理。touchBegin、touchMove、touchEnd都是在这个方法中调用的。

手势事件同touch事件是互斥的,如果UIEvent被识别成一个手势,则不会当成touch事件来处理。系统会调用Cancel将touchBegin、touchMove中断。
当_UIApplicationHandleEventQueue()识别一个手势时,会将对应的手势标记为待处理。当RunLoop 通过Observer 准备进入到休眠状态时,Observer的回调函数会处理所有标记为待处理的手势,并执行手势的回调方法。

支持动画渲染

Core Animation

Core Animation 在呈现的过程中有三个tree。

  • model tree
  • presentation tree
  • render tree

model tree是我们可以直接操作的tree,当修改CALayer的时候,CALayer的属性值会修改model tree。

presentation tree 是layer在屏幕中的真实位置也是一个CALayer对象。可以通过view.layer.presentationLayer 获得。presentation是只读的

render tree 是私有的,应用开发无法访问到。render tree在专用的render server 进程中执行,是真正用来渲染动画的地方,线程优先级高于主线程。所以即使app主线程阻塞,也不会影响到动画的绘制工作。无论隐式还是显式动画都是在当前线程的RunLoop结束后提交到render tree。因为 commit transaction 操作是从app进程到render server 进程是IPC,会有进程间通讯开销,所以官方不推荐我们手动 commit transaction。

CADisplayLink

CADisplayLink 的selector是在屏幕内容刷新完成的时候调用。实质上是向RunLoop注册了一个Source0事件。CADisplayLink一般被用来执行自定义动画和播放视频,相比于CoreAnimation的方式,CADisplayLink会导致部分绘制工作放在了App的进程中进行,增大了CPU和内存的开销,更容易引发性能问题。

CADisplayLink可以用来播放视频,使用AVPlayerItemVideoOutput 提供一个样板缓冲区(sample buffers),输出到CAEAGLLayer 上。CAEAGLLayer 是 Core Animation Embedded Apple Graphics Library 的缩写。OpenGL ES渲染出来的图层在iOS中必须使用 CAEAGLLayer 。通过CADisplayLink 从缓冲区拿纹理内容,呈现在屏幕上。
官方代码

支持异步方法调用

performSelector

performSelector.jpg

上图引自苹果的官方文档,除了输入源和定时源之外,RunLoop还是performSelector的基础设施。

我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,performSelector 方法就会失效。

GCD

dispatch_async() 方法,当第一个参数是主线程队列的时候,libDispatch 会向主线程RunLoop发送mack_msg 消息。如果RunLoop在休眠态,会被唤醒,从消息中取得dispatch_async() 第二个参数 block 并执行。

为了确保GCD的有效性, dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。

支持网络请求

在iOS中进行网络通讯功能的开发一般都是基于NSURLConnection。NSURLConnection的底层是CFNetwork,CFNetwork是基于CFSocket的。NSURLConnection是基于socket的面向对象的网络库。
在iOS7之后苹果提供了NSURLSession,相比NSURLConnection提供了更丰富的功能,如身份验证、后台下载等。底层都是基于CFNetwork和CFSocket。

NSURLConnection的start()方法中,会获取当前的RunLoop,getCurrentRunLoop,然后在其中的defaultMode中添加Source0事件用于接收网络回调。

NSURLConnection会创建两个新线程:

  • com.apple.CFSocket.private 线程,负责处理socket连接
  • com.apple.NSURLConnectionLoader 线程, 用于接受底层socket 的 Source1 事件,通过 Source0 事件通知到NSURLConnection的start所在的RunLoop中。

参与整个网络通讯的有3个线程,2个RunLoop。

线程 RunLoop 作用
com.apple.CFSocket.private 无 RunLoop 处理socket连接
com.apple.NSURLConnectionLoader 有RunLoop 1、接收CFSocket的Source1通知。2、向应用线程的RunLoop的Source0发送通知
应用线程 有RunLoop 通过Source0接收NSURLConnectionLoader 发送的通知,并回调delegate

Run Loop应用实践

Run Loop主要有以下三个应用场景:

  • 维护线程的生命周期,让线程不自动退出
  • 创建常驻线程,执行一些会一直存在的任务。该线程的生命周期跟App相同
  • 在一定时间内监听某种事件,或执行某种任务的线程

维护线程的生命周期

isFinished为Yes时退出。

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

创建常驻线程

@autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
}

在一定时间内监听某种事件

  • 如下代码,在30分钟内,每隔30s执行onTimerFired:。这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据。
@autoreleasepool {
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                    target:self
                                                  selector:@selector(onTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}

  • AFNetworking中RunLoop的创建

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         // 这里主要是监听某个 port,目的是让RunLoop不会退出,确保该 Thread 不会被回收
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread =
        [[NSThread alloc] initWithTarget:self
                                selector:@selector(networkRequestThreadEntryPoint:)
                                  object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 开发注意


//错误做法 
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
while (!self.isCancelled && !self.isFinished) {
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
};

//正确做法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
        [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

参考文章

http://iphonedevwiki.net/index.php/IOHIDFamily
https://en.wikipedia.org/wiki/SpringBoard
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
https://developer.apple.com/reference/foundation/urlsession
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html

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

推荐阅读更多精彩内容

  • 转自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飘金阅读 976评论 0 4
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,436评论 0 13
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技术 RunLoop 是 iOS 和 ...
    橙娃阅读 849评论 1 2
  • 最近看了很多RunLoop的文章,看完很懵逼,决心整理一下,文章中大部分内容都是引用大神们的,但好歹对自己有个交代...
    小凉介阅读 6,693评论 12 79
  • 台州又出现了一个新的休闲公园,由四个次公园和一个主题公园组成一个大的中央公园。公园今年国庆期间刚刚开放,一直没去看...
    柳枝冉冉阅读 347评论 0 0