iOS Runloop 在实际开发中的应用

概述

本篇主要是围绕着项目使用到的Runloop的应用场景及衍生出来的知识点,将讲述以下部分:

  • 控制线程的生命周期【线程保活】
  • 解决NSTimer在滑动过程中停止工作的问题及衍生问题
  • 监控应用卡顿
  • 性能优化
img

一、线程保活

线程保活问题,从字面意思上就是保护线程的生命周期不结束.正常情况下,当线程执行完一次任务之后,需要进行资源回收,但是当有一个任务,随时都有可能去调用,如果在子线程去执行,并且让子线程一直存活着,为了避免来回多次创建毁线程的动作, 降低性能消耗.

情景1

#import <Foundation/Foundation.h>
//定义继承自NSThread线程
@interface ZXYThread : NSThread
@end

@implementation ZXYThread
//线程销毁会被调用
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[ZXYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)run {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"%s ----end----", __func__);
}
@end
复制代码

当执行完上面的代码后,会发现打印出如下-[子线程也就销毁了]

img

但是运行完App,当点击App时没有反应,也可以证明此线程已经销毁.如果改进让线程处于随时接受命令的状态呢?

情景2

从Runloop中得知,如果Mode里没有任何的Source0/Source1/Timer/Observer, Runloop会立马退出.

所以会想到能不能向其中加入上面中的一个是否可以如下: [run 方法中]

// 这个方法的目的:线程保活
- (void)run {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:[[NSTimer alloc]init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"%s ----end----", __func__);
}
复制代码

通过在run方法中加入上面代码,让线程一直不死,打印屏幕界面:

img

好像上面已经满足了要求,达到了线程不死的状态,但是能不能在销毁页面控制器的时候,也销毁定时器,并且随时停掉定时器.

情景3

**知识点: **

如何停止runloop?通过CFRunLoopStop(CFRunLoopGetCurrent())方法可停掉定时器,但是对于用[[NSRunLoop currentRunLoop] run]的Runloop是不会停掉的,因为通过CFRunLoopStop(CFRunLoopGetCurrent())方法仅仅是停掉了本次的Runloop,而不是停掉所有的,但是[[NSRunLoop currentRunLoop] run]的run方法是一直有runloop循环,所以通过[[NSRunLoop currentRunLoop] run]方法是不可能被停掉runloop的
那应该改成什么样的? ----[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
复制代码

下面直接用代码来解释,里面已经写明了代码思路,下面是A页面->B页面->A页面

img
@interface ViewController ()
//继承自NSThead的子线程
@property (strong, nonatomic) ZXYThread *thread;
//有个暂停定时器的需求,stopped代表是否点击了暂停
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //NSThread使用block的方法,消除循环引用
    __weak typeof(self) weakSelf = self;

    self.stopped = NO;
    self.thread = [[ZXYThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);

        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }

        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

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

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void) stop {
    if (!self.thread) return;
    // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;

    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);

    // 清空线程
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);

    [self stop];
}

@end
复制代码

如果想将上面的代码抽取出来应该怎么办呢?

情景4

此处封装工具类并不是直接继承自NSThread,而是继承自NSObject[因为并不想让别人直接能调用NSThread里面的方法.]这样符合开闭原则

#import <Foundation/Foundation.h>
typedef void (^ZXYPermenantThreadTask)(void);
@interface ZXYPermenantThread : NSObject
/**
 在当前子线程执行一个任务
 */
- (void)executeTask:(ZXYPermenantThreadTask)task;
/**
 结束线程
 */
- (void)stop;

@end

#import "ZXYPermenantThread.h"

/** ZXYThread **/
@interface ZXYThread : NSThread
@end
@implementation ZXYThread
- (void)dealloc{
    NSLog(@"%s", __func__);
}
@end

/** ZXYPermenantThread **/
@interface ZXYPermenantThread()
@property (strong, nonatomic) ZXYThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation ZXYPermenantThread
#pragma mark - public methods
- (instancetype)init{
    if (self = [super init]) {
        self.stopped = NO;
        __weak typeof(self) weakSelf = self;
        self.innerThread = [[ZXYThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];

        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(ZXYPermenantThreadTask)task{
    if (!self.innerThread || !task) return;

    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop{
    if (!self.innerThread) return;
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc{
    NSLog(@"%s", __func__);
    [self stop];
}

#pragma mark - private methods
- (void)__stop{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(ZXYPermenantThreadTask)task{
    task();
}

@end
复制代码

上面是针对Runloop在实际开发中的第一个使用场景,那么我们是否在一些好的开源项目中使用过呢或者是看到过呢?

*拓展[AFNetworking也使用到了Runloop的线程保活]*

AFNetworking中的ANURLConnectionOperation是基于NSURLConnection构建,本质是希望能在后台线程接收到Delegate回调.为此AFNetworking单独创建了一个线程, 并在这个线程中开启了一个Runloop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [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启动前必须要至少一个Timer/Observer/Source,所以AFNetworking在[runLoop run]

之前创建了NSMachPort添加进去了.通常情况下调用者需要持有这个NSMachPort并在外部线程通过这个port发送消息到loop内

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
复制代码

当需要这个后台线程执行任务时,AFNetworking通过调用[NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中

二、NSTimer问题

在日常开发中,列表经常会用到NSTimer倒计时问题,或者Interview的时候被面试官问到: NSTimer准时嘛等问题?今天就展开讲述一下原因及方案,最后讲述衍生出来的问题循环引用!争取彻底解决NSTimer带来的疑问?

问题一、 NSTimer定时器不准

原因

  • NSTimer被添加在mainRunloop中,模式是NSDefaultRunLoopMode, mainRunloop负责所有的主线程事件,例如UI界面的操作,负责的运算使当前Runloop持续的时间超过了定时器的间隔时间,那么下一次定时就被延后,这样就造成timer的阻塞
  • 模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作时,runloop的mode会切换为TrackingRunloopMode,这时tiemr会停止回调

解决方案

  1. Mode方式的改变,兼顾TrackingRunloopMode
  2. 在子线程中创建timer,在主线程进行定时任务的操作或者在子线程中创建timer,在子线程中进行定时任务的操作,需要UI的操作时再切换到主线程进行操作
  3. GCD操作: dispatch_source_create以及depatch_resume等方法

方案一

主线程的Runloop使用到的主要有两种模式, NSDefaultRunLoopMode与TrackingRunloopMode模式

添加定时器到主线程的CommonMode中

[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

方案二

子线程创建timer,主线程执行定时或者子线程创建timer,在子线程执行定时,需要刷新再到主线程

*子线程启动NSTimer*

__weak __typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.countTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:strongSelf selector:@selector(countDown) userInfo:nil repeats:YES];
            NSRunLoop *runloop = [NSRunLoop currentRunLoop];
            [runloop addTimer:strongSelf.countTimer forMode:NSDefaultRunLoopMode];
            [runloop run];
        }
    });
复制代码

*主线程更新UI*

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.jumpBTN setTitle:[NSString stringWithFormat:@"跳过 %lds",(long)self.count] forState:UIControlStateNormal];
    });
复制代码

方案三

使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。

dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);

    //创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置时间(start:几s后开始执行; interval:时间间隔)
    uint64_t start = 2.0;    //2s后开始执行
    uint64_t interval = 1.0; //每隔1s执行
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    //设置回调
    dispatch_source_set_event_handler(timer, ^{
       NSLog(@"%@",[NSThread currentThread]);
    });
    //启动定时器
    dispatch_resume(timer);
    NSLog(@"%@",[NSThread currentThread]);

    self.timer = timer;
复制代码

问题二、NSTimer循环引用

*常识*

这三个方法直接将timer添加到了当前runloop default mode,而不需要我们自己操作,当然这样的代价是runloop只能是当前runloop,模式是default mode:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
复制代码

下面五种创建,不会自动添加到runloop,还需调用addTimer:forMode:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
复制代码

在实际项目开发中,使用NSTimer解决定时发送任务的需求,但是还是会产生循环引用,今天讲述本项目中的解决方案.

循环引用(Circular Reference)是指两个对象之间相互强引用,两者无法按时释放,从而导致内存泄露.如下:

img

发现两者相互引用,都不能得以释放,造成了循环引用

方案一、给self添加中间件

引入一个对象proxy,proxy弱引用self,然后proxy传入NSTimer. self强引用NSTimer, NSTimer强引用proxy,proxy弱引用着self,这样通过弱引用解决了相互引用,就不会造成环..本项目中使用的方法是引入中间控件HCCProxy1

img

定义一个继承自NSObject的中间代理对象HCCProxy1,ViewController不持有timer,而是持有HCCProxy1实例, 让HCCProxy1实例弱引用ViewController, timer强引用HCCProxy1实例,如下:

@interface HCCProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation HCCProxy1
+ (instancetype)proxyWithTarget:(id)target{
    HCCProxy1 *proxy = [[HCCProxy1 alloc] init];
    proxy.target = target;
    return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}
@end
复制代码

在项目中使用如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[HCCProxy1 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
复制代码

*拓展:*

- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
  消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。

本文中由于当前target是HCCProxy1,但是HCCProxy1没有实现方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。
复制代码

方案二、使用继承自NSProxy类HCCProxy的消息转发

@interface HCCProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation HCCProxy
+ (instancetype)proxyWithTarget:(id)target{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    HCCProxy *proxy = [HCCProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
@end
复制代码

在项目中使用如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[HCCProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
复制代码

三、监控卡顿

卡顿问题主要是主线程上无法响应用户交互的问题, 如果一个App时不时给你卡一下,有时还长时间没有响应,你还会继续使用嘛?答案当然是显然的

img

对于iOS开发来说,监控卡顿就是要去找到主线程都做了哪些事情,线程的消息事件依赖于NSRunloop的,所以从NSRunloop入手,就可以知道主线程上都调用了哪些方法.可以监听NSRunloop的状态,就能够发现调用方法是否执行时间过长从而判断是否出现了卡顿.所以推荐的监控卡顿方案是: 通过监控Runloop的状态来判断是否出现卡顿

下面我们讲解一下Runloop的底层常识吧

1、知识-Runloop原理

Runloop的目的是,当有事情要去处理时保持线程忙,当没有事件要处理的时候让线程进入休眠.下面通过CFRunloop的源码来分享下Runloop的原理

第一步:

通知observers: Runloop要开始进入loop了,紧接着进入loop,代码如下:

//通知 observers
if (currentMode->_observerMask & kCFRunLoopEntry ) 
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
//进入 loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
复制代码

第二步

开启一个 do while 来保活线程。通知 Observers:RunLoop 会触发 Timer 回调、Source0 回调,接着执行加入的 block.

// 通知 Observers RunLoop 会触发 Timer 回调
if (currentMode->_observerMask & kCFRunLoopBeforeTimers)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 通知 Observers RunLoop 会触发 Source0 回调
if (currentMode->_observerMask & kCFRunLoopBeforeSources)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行 block
__CFRunLoopDoBlocks(runloop, currentMode);
复制代码

接下来,触发 Source0 回调,如果有 Source1 是 ready 状态的话,就会跳转到 handle_msg 去处理消息

if (MACH_PORT_NULL != dispatchPort ) {
    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
    if (hasMsg) goto handle_msg;
}
复制代码

第三步

回调触发后,通知 Observers:RunLoop 的线程将进入休眠(sleep)状态.

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
if (!poll && (currentMode->_observerMask & kCFRunLoopBeforeWaiting)) {
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
复制代码

第四步

进入休眠后,会等待 mach_port 的消息,以再次唤醒。只有在下面四个事件出现时才会被再次唤醒:

  • 基于 port 的 Source 事件;
  • Timer 时间到;
  • RunLoop 超时;
  • 被调用者唤醒。

等待唤醒的代码如下:

do {
    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
        // 基于 port 的 Source 事件、调用者唤醒
        if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            break;
        }
        // Timer 时间到、RunLoop 超时
        if (currentMode->_timerFired) {
            break;
        }
} while (1);
复制代码

第五步

唤醒时通知 Observer:RunLoop 的线程刚刚被唤醒了。代码如下

if (!poll && (currentMode->_observerMask & kCFRunLoopAfterWaiting))
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
复制代码

第六步

RunLoop 被唤醒后就要开始处理消息了:

  • 如果是 Timer 时间到的话,就触发 Timer 的回调;
  • 如果是 dispatch 的话,就执行 block;
  • 如果是 source1 事件的话,就处理这个事件。

消息执行完后,就执行加到 loop 里的 block。代码如下:

handle_msg:
// 如果 Timer 时间到,就触发 Timer 回调
if (msg-is-timer) {
    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
} 
// 如果 dispatch 就执行 block
else if (msg_is_dispatch) {
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} 

// Source1 事件的话,就处理这个事件
else {
    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
    if (sourceHandledThisLoop) {
        mach_msg(reply, MACH_SEND_MSG, reply);
    }
}
复制代码

第七步

根据当前 RunLoop 的状态来判断是否需要走下一个 loop。当被外部强制停止或 loop 超时时,就不继续下一个 loop 了,否则继续走下一个 loop 。代码如下:

if (sourceHandledThisLoop && stopAfterHandle) {
     // 事件已处理完
    retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
    // 超时
    retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
    // 外部调用者强制停止
    retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
    // mode 为空,RunLoop 结束
    retVal = kCFRunLoopRunFinished;
}
复制代码

全部的内部代码如下:

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1\. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2\. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3\. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4\. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5\. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            /// 7\. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            /// 8\. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            /// 收到消息,处理消息。
            handle_msg:

            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            }

            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            }

            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }

    /// 10\. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
复制代码

实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

整个Runloop过程,可以总结如下一张图片

img

2、如何监测卡顿

要想监听 RunLoop,你就首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
复制代码

将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察。然后,创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。

一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

开启一个子线程监控的代码如下:

//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子线程开启一个持续的 loop 用来进行监控
    while (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
            if (!runLoopObserver) {
                timeoutCount = 0;
                dispatchSemaphore = 0;
                runLoopActivity = 0;
                return;
            }
            //BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                //将堆栈信息上报服务器的代码放到这里
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});
复制代码

下面是封装的一个工具类HCCMonitor,用于卡顿监测

#import <Foundation/Foundation.h>
@interface HCCMonitor : NSObject
+ (instancetype)shareInstance;
- (void)beginMonitor; //开始监视卡顿
- (void)endMonitor;   //停止监视卡顿
@end

#import "HCCMonitor.h"
#import "HCCCallStack.h"
#import "HCCCPUMonitor.h"

@interface HCCMonitor() {
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
    @public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}
@property (nonatomic, strong) NSTimer *cpuMonitorTimer;
@end

@implementation HCCMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t dispatchOnce;
    dispatch_once(&dispatchOnce, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)beginMonitor {
    //监测 CPU 消耗
    self.cpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval:3
                                                             target:self
                                                           selector:@selector(updateCPUInfo)
                                                           userInfo:nil
                                                            repeats:YES];
    //监测卡顿
    if (runLoopObserver) {
        return;
    }
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
    //创建一个观察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //将观察者添加到主线程runloop的common模式下的观察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

    //创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!runLoopObserver) {
                    timeoutCount = 0;
                    dispatchSemaphore = 0;
                    runLoopActivity = 0;
                    return;
                }
                //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    // 将堆栈信息上报服务器的代码放到这里
                    //出现三次出结果
//                    if (++timeoutCount < 3) {
//                        continue;
//                    }
                    NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//                        [HCCCallStack callStackWithType:HCCCallStackTypeAll];
                    });
                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });

}

- (void)endMonitor {
    [self.cpuMonitorTimer invalidate];
    if (!runLoopObserver) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    CFRelease(runLoopObserver);
    runLoopObserver = NULL;
}

#pragma mark - Private

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    HCCMonitor *lagMonitor = (__bridge HCCMonitor*)info;
    lagMonitor->runLoopActivity = activity;

    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)updateCPUInfo {
    thread_act_array_t threads;
    mach_msg_type_number_t threadCount = 0;
    const task_t thisTask = mach_task_self();
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
    if (kr != KERN_SUCCESS) {
        return;
    }
    for (int i = 0; i < threadCount; i++) {
        thread_info_data_t threadInfo;
        thread_basic_info_t threadBaseInfo;
        mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
        if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
            threadBaseInfo = (thread_basic_info_t)threadInfo;
            if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
                integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
                if (cpuUsage > 70) {
                    //cup 消耗大于 70 时打印和记录堆栈
                    NSString *reStr = HCCStackOfThread(threads[i]);
                    //记录数据库中
//                    [[[HCCDB shareInstance] increaseWithStackString:reStr] subscribeNext:^(id x) {}];
                    NSLog(@"CPU useage overload thread stack:\n%@",reStr);
                }
            }
        }
    }
}

@end
复制代码

四、性能优化

当tableview的cell有多个ImageView,并且是大图的话,会不会在滑动的时候导致卡顿,答案是显然意见的。

通过上面讲述Runloop的原理,我们可以使用Runloop每次循环添加一张图片。

/*
 为什么要优化:
    Runloop会在一次循环中绘制屏幕上所有的点,如果加载的图片过大,过多,就会造成需要绘制很多的
的点,导致一次循环的时间过长,从而导致UI卡顿。
 */
复制代码

监听Runloop

//添加runloop监听者
- (void)addRunloopObserver{

    //    获取 当前的Runloop ref - 指针
    CFRunLoopRef current =  CFRunLoopGetCurrent();

    //定义一个RunloopObserver
    CFRunLoopObserverRef defaultModeObserver;

    //上下文
    /*
     typedef struct {
        CFIndex version; //版本号 long
        void * info;    //这里我们要填写对象(self或者传进来的对象)
        const void *(*retain)(const void *info);        //填写&CFRetain
        void (*release)(const void *info);           //填写&CGFRelease
        CFStringRef (*copyDescription)(const void *info); //NULL
     } CFRunLoopObserverContext;
     */
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };

    /*
     1 NULL空指针 nil空对象 这里填写NULL
     2 模式
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
     3 是否重复 - YES
     4 nil 或者 NSIntegerMax - 999
     5 回调
     6 上下文
     */
    //    创建观察者
    defaultModeObserver = CFRunLoopObserverCreate(NULL,
                                                  kCFRunLoopBeforeWaiting, YES,
                                                  NSIntegerMax - 999,
                                                  &Callback,
                                                  &context);

    //添加当前runloop的观察着
    CFRunLoopAddObserver(current, defaultModeObserver, kCFRunLoopDefaultMode);

    //释放
    CFRelease(defaultModeObserver);
}

@end
复制代码

回调方法

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

    //通过info桥接为当前的对象
    ZXYRunloop * runloop = (__bridge ZXYunloop *)info;

    //如果没有任务,就直接返回
    if (runloop.tasks.count == 0) {
        return;
    }

    BOOL result = NO;
    while (result == NO && runloop.tasks.count) {

        //取出任务
        RunloopBlock unit = runloop.tasks.firstObject;

        //执行任务
        result = unit();

        //删除任务
        [runloop.tasks removeObjectAtIndex:0];
    }
}
复制代码

通过上面的两个方法我们可以做到监听Runloop循环,以及每次循环需要处理的事情,这个时候我们只需要对外提供一个添加任务的方法,用数组保存起来。

//add task 添加任务
- (void)addTask:(RunloopBlock)unit withId:(id)key{
    //添加任务到数组
    [self.tasks addObject:unit];
    [self.taskKeys addObject:key];

    //为了保证加载到图片最大数是20所以要删除
    if (self.tasks.count > self.maxQueue) {
        [self.tasks removeObjectAtIndex:0];
        [self.taskKeys removeObjectAtIndex:0];
    }
复制代码

在ZXYRunloop初始化方法设置初始化对象和基本信息

- (instancetype)init{
    self = [super init];
    if (self) {
        //初始化对象/基本信息
        self.maxQueue = 20;
        self.tasks = [NSMutableArray array];
        self.taskKeys = [NSMutableArray array];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:YES block:^(NSTimer * _Nonnull timer) { }];
        //添加Runloop观察者
        [self addRunloopObserver];
    }
    return self;
}
复制代码

在TableViewCell中使用:

[[ZXYRunloop shareInstance] addTask:^BOOL{
        [ViewController addCenterImg:cell];
        return YES;
    } withId:indexPath];
复制代码

总结一下思想

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

推荐阅读更多精彩内容