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即可。

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

推荐阅读更多精彩内容