iOS底層 -- RunLoop之相關(guān)應(yīng)用

RunLoop在實際開中的應(yīng)用
  • 解決NSTimer在滑動時停止工作的問題
  • 控制線程生命周期(線程保活)
  • 監(jiān)控應(yīng)用卡頓
  • 性能優(yōu)化
一、解決NSTimer在滑動時停止工作的問題
  • 在拖拽時定時器不工作
__block int count = 0;

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

打印結(jié)果

  • 在拖拽時人仍然正常工作
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并不是一個真的模式,它只是一個標(biāo)記
// timer能在_commonModes數(shù)組中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

二 線程?;?/h5>

我們先封裝一個長久活命的線程

  • PermanentThread.h
// 聲明一個block - 用于執(zhí)行任務(wù)
typedef void(^PermanentThreadTask)(void);

/** 線程?;?*/
@interface PermanentThread : NSObject

// 在當(dāng)前線程執(zhí)行一個任務(wù)
- (void)executeTask:(PermanentThreadTask)task;

// 結(jié)束線程
- (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只有添加事件才會執(zhí)行
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];

            // 當(dāng)當(dāng)前對象存在并且變量為false的時候,才一直執(zhí)行
            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

// 執(zhí)行任務(wù)
- (void)executeTask:(PermanentThreadTask)task {
    // 如果線程釋放或者無任務(wù),則退出
    if (!self.thread || !task) {
        return;
    }

    // 開始執(zhí)行任務(wù)
    [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

// 執(zhí)行任務(wù)
- (void)innerExecuteTask:(PermanentThreadTask)task {
    task();
}

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

@end

外部調(diào)用

- (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(@"執(zhí)行任務(wù) - %@", [NSThread currentThread]);
    }];
}

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

運行結(jié)果

三、讓UITableView、UICollectionView延遲加載圖片

首先創(chuàng)建一個單例,單例中定義了幾個數(shù)組,用來存要在runloop循環(huán)中執(zhí)行的任務(wù),然后為主線程的runloop添加一個CFRunLoopObserver,當(dāng)主線程在NSDefaultRunLoopMode中執(zhí)行完任務(wù),即將睡眠前,執(zhí)行一個單例中保存的一次圖片渲染任務(wù)。關(guān)鍵代碼看 RunLoopWorkDistribution即可。

四、監(jiān)測主線程的卡頓,并將卡頓時的線程堆棧信息保存下來,選擇合適時機(jī)上傳到服務(wù)器

第一步,創(chuàng)建一個子線程,在線程啟動時,啟動其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];
    }
}

第二步,在開始監(jiān)測時,往主線程的RunLoop中添加一個observer,并往子線程中添加一個定時器,每0.5秒檢測一次耗時的時長。

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

    // 1.創(chuàng)建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.創(chuàng)建一個timer,并添加到子線程的RunLoop中    
    [self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

}

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

    // 創(chuàng)建一個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);
}

第三步,補(bǔ)充觀察者回調(diào)處理

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進(jìn)入睡眠狀態(tài)的時間可能會非常短,有時候只有1毫秒,有時候甚至1毫秒都不到,靜止不動時,則會長時間進(jìn)入睡覺狀態(tài)。

因為主線程中的block、交互事件、以及其他任務(wù)都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前執(zhí)行,所以我在即將開始執(zhí)行Sources 時,記錄一下時間,并把正在執(zhí)行任務(wù)的標(biāo)記置為YES,將要進(jìn)入睡眠狀態(tài)時,將正在執(zhí)行任務(wù)的標(biāo)記置為NO。

第四步,補(bǔ)充timer 的回調(diào)處理

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

    // 如果主線程正在執(zhí)行任務(wù),并且這一次loop 執(zhí)行到 現(xiàn)在還沒執(zhí)行完,那就需要計算時間差    

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

    NSLog(@"定時器---%@",[NSThread currentThread]);
    NSLog(@"主線程執(zhí)行了---%f秒",excuteTime);

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

timer 每 0.01秒執(zhí)行一次,如果當(dāng)前正在執(zhí)行任務(wù)的狀態(tài)為YES,并且從開始執(zhí)行到現(xiàn)在的時間大于闕值,則把堆棧信息保存下來,便于后面處理。

為了能夠捕獲到堆棧信息,我把timer的間隔調(diào)的很?。?.01),而評定為卡頓的闕值也調(diào)的很?。?.01)。 實際使用時這兩個值應(yīng)該是比較大,timer間隔為1s,卡頓闕值為2s即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容