iOS底层 -- RunLoop之相关应用

RunLoop在实际开中的应用
  • 解决NSTimer在滑动时停止工作的问题
  • 控制线程生命周期(线程保活)
  • 监控应用卡顿
  • 性能优化
一、解决NSTimer在滑动时停止工作的问题
  • 在拖拽时定时器不工作
__block int count = 0;

// 默认添加到default模式
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"%d", ++count);
}];

打印结果

  • 在拖拽时人仍然正常工作
static int count = 0;
// 2.添加到指定模式下
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"%d", ++count);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
// NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
// timer能在_commonModes数组中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

二 线程保活

我们先封装一个长久活命的线程

  • PermanentThread.h
// 声明一个block - 用于执行任务
typedef void(^PermanentThreadTask)(void);

/** 线程保活 */
@interface PermanentThread : NSObject

// 在当前线程执行一个任务
- (void)executeTask:(PermanentThreadTask)task;

// 结束线程
- (void)stop;

@end

  • PermanentThread.m
/** CSThread **/
@interface CSThread : NSThread
@end
@implementation CSThread
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

@interface PermanentThread()
/** 线程*/
@property(nonatomic,strong)CSThread *thread;
/** 是否停止*/
@property(nonatomic,assign, getter=isStopped)BOOL stopped;
@end

@implementation PermanentThread

// 初始化方法
- (instancetype)init {
    self = [super init];
    if (self) {
        self.stopped = NO;

        // 初始化线程
        __weak typeof(self) weakSelf = self;
        self.thread = [[CSThread alloc] initWithBlock:^{
            // runloop只有添加事件才会执行
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];

            // 当当前对象存在并且变量为false的时候,才一直执行
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];

        // 开启线程
        [self.thread start];
    }
    return self;
}

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

    [self stop];
}

#pragma mark - public method

// 执行任务
- (void)executeTask:(PermanentThreadTask)task {
    // 如果线程释放或者无任务,则退出
    if (!self.thread || !task) {
        return;
    }

    // 开始执行任务
    [self performSelector:@selector(innerExecuteTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

// 停止
- (void)stop {
    if (!self.thread) {
        return;
    }

    [self performSelector:@selector(innerStop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

#pragma mark - private method

// 执行任务
- (void)innerExecuteTask:(PermanentThreadTask)task {
    task();
}

// 停止线程 runloop
- (void)innerStop {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

@end

外部调用

- (void)viewDidLoad {
    [super viewDidLoad];

    // 2.线程保活
    self.thread = [[PermanentThread alloc] init];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.thread executeTask:^{
        NSLog(@"执行任务 - %@", [NSThread currentThread]);
    }];
}

- (void)stopBtnClick {
    [self.thread stop];
}

运行结果

三、让UITableView、UICollectionView延迟加载图片

首先创建一个单例,单例中定义了几个数组,用来存要在runloop循环中执行的任务,然后为主线程的runloop添加一个CFRunLoopObserver,当主线程在NSDefaultRunLoopMode中执行完任务,即将睡眠前,执行一个单例中保存的一次图片渲染任务。关键代码看 RunLoopWorkDistribution即可。

四、监测主线程的卡顿,并将卡顿时的线程堆栈信息保存下来,选择合适时机上传到服务器

第一步,创建一个子线程,在线程启动时,启动其RunLoop。

+ (instancetype)shareMonitor
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[[self class] alloc] init];
        instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];
        [instance.monitorThread start];
    });

    return instance;
}

+ (void)monitorThreadEntryPoint
{
    @autoreleasepool {
        [[NSThread currentThread] setName:@"FluencyMonitor"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

第二步,在开始监测时,往主线程的RunLoop中添加一个observer,并往子线程中添加一个定时器,每0.5秒检测一次耗时的时长。

- (void)start
{
    if (_observer) {
        return;
    }

    // 1.创建observer    CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};

    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);

    // 2.将observer添加到主线程的RunLoop中 
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

    // 3.创建一个timer,并添加到子线程的RunLoop中    
    [self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

}

- (void)addTimerToMonitorThread
{
    if (_timer) {
        return;
    }

    // 创建一个timer   
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();

    CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};

    _timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.01, 0, 0, &runLoopTimerCallBack, &context);

    // 添加到子线程的RunLoop中  
    CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
}

第三步,补充观察者回调处理

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

    FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;

    NSLog(@"MainRunLoop---%@",[NSThread currentThread]);

    switch (activity) {

        case kCFRunLoopEntry:

            NSLog(@"kCFRunLoopEntry");

            break;

        case kCFRunLoopBeforeTimers:

            NSLog(@"kCFRunLoopBeforeTimers");

            break;

        case kCFRunLoopBeforeSources:

            NSLog(@"kCFRunLoopBeforeSources");

            monitor.startDate = [NSDate date];

            monitor.excuting = YES;

            break;

        case kCFRunLoopBeforeWaiting:

            NSLog(@"kCFRunLoopBeforeWaiting");

            monitor.excuting = NO;

            break;

        case kCFRunLoopAfterWaiting:

            NSLog(@"kCFRunLoopAfterWaiting");

            break;

        case kCFRunLoopExit:

            NSLog(@"kCFRunLoopExit");

            break;

        default:

            break;

    }

}

RunLoop进入睡眠状态的时间可能会非常短,有时候只有1毫秒,有时候甚至1毫秒都不到,静止不动时,则会长时间进入睡觉状态。

因为主线程中的block、交互事件、以及其他任务都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前执行,所以我在即将开始执行Sources 时,记录一下时间,并把正在执行任务的标记置为YES,将要进入睡眠状态时,将正在执行任务的标记置为NO。

第四步,补充timer 的回调处理

static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info)
{
    FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;
    if (!monitor.excuting) {
        return;
    }

    // 如果主线程正在执行任务,并且这一次loop 执行到 现在还没执行完,那就需要计算时间差    

    NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];

    NSLog(@"定时器---%@",[NSThread currentThread]);
    NSLog(@"主线程执行了---%f秒",excuteTime);

    if (excuteTime >= 0.01) {
        NSLog(@"线程卡顿了%f秒",excuteTime);
        [monitor handleStackInfo];
    }
}

timer 每 0.01秒执行一次,如果当前正在执行任务的状态为YES,并且从开始执行到现在的时间大于阙值,则把堆栈信息保存下来,便于后面处理。

为了能够捕获到堆栈信息,我把timer的间隔调的很小(0.01),而评定为卡顿的阙值也调的很小(0.01)。 实际使用时这两个值应该是比较大,timer间隔为1s,卡顿阙值为2s即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容