音视频知识库

Runtime 实际运用

2020-02-23  本文已影响0人  TaoGeNet
image.png

runloop和线程一一对应
runloop包含多个mode, mode包含多个 mode item(sources,timers,observers)
runloop一次只能运行在一个model下:
切换mode:停止loop -> 设置mode -> 重启runloop
runloop通过切换mode来筛选要处理的事件,让其互不影响
iOS运行流畅的关键

image.png
/// 核心函数
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        /// 通知 Observers: 即将处理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

        /// 通知 Observers: 即将处理Source事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)

        /// 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);

        /// 处理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

        /// 处理sources0返回为YES
        if (sourceHandledThisLoop) {
            /// 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }

        /// 判断有无端口消息(Source1)
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            /// 处理消息
            goto handle_msg;
        }

        /// 通知 Observers: 即将进入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);

        /// 等待被唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);

        /// 通知 Observers: 被唤醒,结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    handle_msg:
        if (被Timer唤醒) {
            /// 处理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD唤醒) {
            /// 处理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else if (被Source1唤醒) {
            /// 被Source1唤醒,处理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
        }

        /// 处理block
        __CFRunLoopDoBlocks(rl, rlm);

        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }

    } while (0 == retVal);

    return retVal;
}
image.png

事件响应

当一个硬件事件(触摸/锁屏/摇晃/加速)发生后,首先IOKit.framework 生成一个IOHIDEvent事件并有SprintBoard接收,之后有mach_port转发给需要的App进程。
苹果注册了一个Source1来接收系统事件,通过回调函数触发Source0 (所以Event实际上是基于Source0的),调用_UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把IOHIDEvent 处理并包装成UIEvent 进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。

手势识别

当上面的_UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用Cancel 将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的UIGestureRecognizer 标记为待处理。
苹果注册了一个Observer监测BeforeWaiting(Loop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有UIGestureRecognizer的变化(创建、销毁、状态改变)时,这个回调都会进行相应的处理。

界面刷新

当UI发生改变时(Frame变化,UIView/CALayer的结构变化)时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理。
苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer来执行实际的绘制和调整,并更新UI界面。

AutoreleasePool

主线程Runloop注册了两个Observers,其回调都是_wrapRunloopWithAutoreleasePoolHandler
Observers1 监听Entry事件: 优先级最高,确保在所有的回调前创建释放池,回调内调用 _objc_autoreleasePoolPush()创建自动释放池
Observers2监听BeforeWaiting 和Exit事件: 优先级最低,保证在所有回调后释放释放池。BeforeWaiting事件:调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧池并创建新池,Exit事件: 调用_objc_autoreleasePoolPop(),释放自动释放池

Timer不被ScrollView的滑动影响

+timerWithTimerInterval ... 创建timer
[[NSRunLoop currentRunLoop] addTimer: timer forMode:NSRunLoopCommonModes] 把timer加到当前runloop,使用占位模式
runloop run/runUntilData 手动开启子线程
使用GCD创建定时器,GCD创建的定时器不会受RunLoop 的影响。

// 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 创建一个定时器(dispatch_source_t本质还是个OC对象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
    // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
    // 比当前时间晚1秒开始执行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

    //每隔一秒执行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);

    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });

    // 启动定时器
    dispatch_resume(self.timer);

GCD

dispatch_async(dispatch_get_main_queue)使用到了RunLoop
libDispatch向主线程的Runloop发送消息将其唤醒,并从消息中取得block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里执行这个block

NSURLConnection

使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate就会不停收到事件回调。
start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

AFNetworking

使用runloop开启常驻线程

NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];

给 runloop 添加NSMachPort port使runloop不退出,实际并没有给这个port发消息

AsyncDisplayKit

仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

卡顿检测

通过监听mainRunloop的状态和信号量阻塞线程的特点来检测卡顿,通过kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting的间隔时长超过自定义阀值则记录堆栈信息。

RunLoop实战:实时卡顿监控

FPS 检测

创建CADisplayLink对象的时候会指定一个selector,把创建的CADisplayLink对象加入runloop,所以就实现了以屏幕刷新的频率调用某个方法。
在调用的方法中计算执行的次数,用次数除以时间,就算出了FPS。
注:iOS正常刷新率为每秒60次。

@implementation ViewController {
    UILabel *_fpsLbe;

    CADisplayLink *_link;
    NSTimeInterval _lastTime;
    float _fps;
}

- (void)startMonitoring {
    if (_link) {
        [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        [_link invalidate];
        _link = nil;
    }
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }

    self.count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    _fps = _count / delta;
    NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
    self.count = 0;
    _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}

防崩溃处理

NSSetUncaughtExceptionHandler(&HandleException);监听异常信号SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGV,SIGFPE
回调方法内创建一个Runloop,将主线程的所有Runmode都拿过来跑,作为应用程序主Runloop的替代

// 我的处理
LHLExceptionHelper *exceptionHandler = [LHLExceptionHelper new];
[exceptionHandler performSelectorOnMainThread:@selector(makeException:)
                                   withObject:exceptionTemp waitUntilDone:
- (void)makeException:(NSException *)exception {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);

while (captor.needKeepAlive) {
    for (NSString *mode in (__bridge NSArray *)allModesRef) {
        if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
            continue;
        }
        CFStringRef modeRef  = (__bridge CFStringRef)mode;
        CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
    }
}

常驻线程

可以把自己创建的线程添加到Runloop中,做一些频繁处理的任务,例如:检测网络状态,定时上传一些信息等。

- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"----------run----%@", [NSThread currentThread]);
    @autoreleasepool{
    /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
      下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    [[NSRunLoop currentRunLoop] run];
   }

    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];

    NSLog(@"---------");
}

- (void)test
{
    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

参考:
实时卡顿监控
玩转Runloop - 代码示例使用Source, Observer, Timer
一文看完Runloop
深入理解Runloop
RunLoop实战:实时卡顿监控
简单监测iOS卡顿的demo
关于dispatch_semaphore的使用

上一篇下一篇

猜你喜欢

热点阅读