Runtime 实际运用

作者: TaoGeNet | 来源:发表于2020-02-23 00:05 被阅读0次
    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 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

    卡顿检测

    • dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。GCD信号量-dispatch_semaphore_t

    通过监听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的使用

    相关文章

      网友评论

        本文标题:Runtime 实际运用

        本文链接:https://www.haomeiwen.com/subject/eofrqhtx.html