iOS核心机制和框架iOS开发系列

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

2018-06-05  本文已影响4人  darcy87

前言

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

管理自动释放池

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

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

由于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是我们可以直接操作的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会创建两个新线程:

参与整个网络通讯的有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主要有以下三个应用场景:

维护线程的生命周期

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];
}

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

@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]];
}


+ (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

上一篇 下一篇

猜你喜欢

热点阅读