RunLoop常見的應(yīng)用場(chǎng)景

以下關(guān)于RunLoop的資料都比較好:


RunLoop使用場(chǎng)景


一、保證線程長(zhǎng)時(shí)間存活

  • 問(wèn)題描述:不希望一些花費(fèi)時(shí)間較長(zhǎng)的操作阻塞主線程而導(dǎo)致界面卡頓辛藻,就需要?jiǎng)?chuàng)建一個(gè)子線程姨涡,然后把該操作放在子線程中來(lái)處理屎开〈∨溃可是當(dāng)子線程中的任務(wù)執(zhí)行完畢后甫贯,子線程就會(huì)被銷毀掉。
@interface YTThread : NSThread
@end

@implementation YTThread

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

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"RunLoop";
    
    [self threadTest];
}

- (void)threadTest {
    YTThread *thread = [[YTThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];
    [thread start];
}

- (void)subThreadOpetion {
    @autoreleasepool {
        NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
    }
}
2017-05-27 11:05:05.444 MXBarManagerDemo[23405:2814835] <YTThread: 0x600000268300>{number = 3, name = (null)}----子線程任務(wù)開始
2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] <YTThread: 0x600000268300>{number = 3, name = (null)}----子線程任務(wù)結(jié)束
2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] -[YTThread dealloc]
  • 當(dāng)子線程中任務(wù)執(zhí)行完后線程被立刻銷毀看蚜。如果程序中需要經(jīng)常在子線程中執(zhí)行任務(wù)叫搁,頻繁的創(chuàng)建和銷毀線程會(huì)造成資源的浪費(fèi)。這時(shí)可以使用RunLoop來(lái)讓該線程長(zhǎng)時(shí)間存活而不被銷毀失乾。如下所示:
@interface TestRunLoopViewController ()

@property (nonatomic, strong) NSThread *subThread;

@end

@implementation TestRunLoopViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"RunLoop";
    
    [self threadTest];
}

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

- (void)threadTest {
    YTThread *thread = [[YTThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
    [thread setName:@"YTThread"];
    [thread start];
    self.subThread = thread;
}

- (void)subThreadEntryPoint {
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // NSLog(@"runLoop--%@", runLoop);
        NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
        [runLoop run];
    }
}

- (void)subThreadOpetion {
    @autoreleasepool {
        NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.0];
        NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
    }
}

@end
2017-05-27 11:17:26.064 MXBarManagerDemo[23458:2865125] 啟動(dòng)RunLoop前--(null)
2017-05-27 11:17:30.627 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)開始
2017-05-27 11:17:33.632 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)結(jié)束
2017-05-27 11:17:36.319 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)開始
2017-05-27 11:17:39.325 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)結(jié)束
2017-05-27 11:17:56.479 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)開始
2017-05-27 11:17:59.482 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務(wù)結(jié)束

注意幾點(diǎn):

1、獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]纬乍。

應(yīng)用程序并不需要自己創(chuàng)建RunLoop碱茁,而是要在合適的時(shí)間啟動(dòng)runloop。 CF框架源碼中有CFRunLoopGetCurrent(void) 和 CFRunLoopGetMain(void),查看源碼可知仿贬,這兩個(gè)API中纽竣,都是先從全局字典中取。如果沒(méi)有與該線程對(duì)應(yīng)的RunLoop,那么就會(huì)幫我們創(chuàng)建一個(gè)RunLoop(創(chuàng)建RunLoop的過(guò)程在函數(shù)_CFRunLoopGet0(pthread_t t)中)蜓氨。

2聋袋、即使RunLoop開始運(yùn)行,如果RunLoop 中的 modes 為空穴吹,或者要執(zhí)行的mode里沒(méi)有item幽勒,那么RunLoop會(huì)直接在當(dāng)前l(fā)oop中返回,并進(jìn)入睡眠狀態(tài)港令。

如注釋掉[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];(查看注釋前后打印出的runLoop)啥容,點(diǎn)擊視圖,控制臺(tái)不會(huì)有任何輸出顷霹,因?yàn)閙ode 中并沒(méi)有item任務(wù)咪惠。經(jīng)過(guò)NSRunLoop封裝后,只可以往mode中添加兩類item任務(wù):NSPort(對(duì)應(yīng)的是source)淋淀、NSTimer遥昧。如果使用CFRunLoopRef,則可以使用C語(yǔ)言API,往mode中添加source、timer朵纷、observer炭臭。

3、自己創(chuàng)建的Thread中的任務(wù)是在kCFRunLoopDefaultMode這個(gè)mode中執(zhí)行的柴罐。

查看modes

2017-05-27 14:13:53.475 MXBarManagerDemo[29040:3134640] runLoop--<CFRunLoop 0x610000175180 [0x108d9fe40]>{wakeup port = 0x731b, stopped = false, ignoreWakeUps = true, 
current mode = (none),
common modes = <CFBasicHash 0x610000058f00 [0x108d9fe40]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x108d77970 [0x108d9fe40]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x610000058420 [0x108d9fe40]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x610000183e90 [0x108d9fe40]>{name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x6100001750c0, source = 0x6100001d40a0 (not fired), timer port = 0x7503, 
    sources0 = <CFBasicHash 0x600000058780 [0x108d9fe40]>{type = mutable set, count = 0,
entries =>
}
,
    sources1 = <CFBasicHash 0x60000005abe0 [0x108d9fe40]>{type = mutable set, count = 1,
entries =>
    1 : <CFRunLoopSource 0x60000017b480 [0x108d9fe40]>{signalled = No, valid = Yes, order = 200, context = <CFMachPort 0x600000544570 [0x108d9fe40]>{valid = Yes, port = 7603, source = 0x60000017b480, callout = __NSFireMachPort (0x1080a0737), context = <CFMachPort context 0x60000005cdd0>}}
}
,
    observers = (null),
    timers = (null),
    currently 517558433 (51534038374775) / soft deadline in: 1.84466925e+10 sec (@ -1) / hard deadline in: 1.84466925e+10 sec (@ -1)
},

}
}
2017-05-27 14:13:53.476 MXBarManagerDemo[29040:3134640] 啟動(dòng)RunLoop前--(null)
2017-05-27 14:16:58.114 MXBarManagerDemo[29040:3134640] <YTThread: 0x61800007a0c0>{number = 3, name = YTThread}----子線程任務(wù)開始
2017-05-27 14:17:01.115 MXBarManagerDemo[29040:3134640] <YTThread: 0x61800007a0c0>{number = 3, name = YTThread}----子線程任務(wù)結(jié)束

4徽缚、在子線程創(chuàng)建好后,最好所有的任務(wù)都放在AutoreleasePool中革屠。

  • 舉例
    YYKit中使用YYWebImageOperation對(duì)網(wǎng)絡(luò)圖片進(jìn)行下載請(qǐng)求凿试,使用[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];將任務(wù)丟到后臺(tái)線程的 RunLoop 中。
// runs on network thread
- (void)_startOperation {
    if ([self isCancelled]) return;
    @autoreleasepool {
        // get image from cache
        if (_cache &&
            !(_options & YYWebImageOptionUseNSURLCache) &&
            !(_options & YYWebImageOptionRefreshImageCache)) {
            UIImage *image = [_cache getImageForKey:_cacheKey withType:YYImageCacheTypeMemory];
            if (image) {
                [_lock lock];
                if (![self isCancelled]) {
                    if (_completion) _completion(image, _request.URL, YYWebImageFromMemoryCache, YYWebImageStageFinished, nil);
                }
                [self _finish];
                [_lock unlock];
                return;
            }
            if (!(_options & YYWebImageOptionIgnoreDiskCache)) {
                __weak typeof(self) _self = self;
                dispatch_async([self.class _imageQueue], ^{
                    __strong typeof(_self) self = _self;
                    if (!self || [self isCancelled]) return;
                    UIImage *image = [self.cache getImageForKey:self.cacheKey withType:YYImageCacheTypeDisk];
                    if (image) {
                        [self.cache setImage:image imageData:nil forKey:self.cacheKey withType:YYImageCacheTypeMemory];
                        [self performSelector:@selector(_didReceiveImageFromDiskCache:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
                    } else {
                        [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
                    }
                });
                return;
            }
        }
    }
    [self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];
}
/// Network thread entry point.
+ (void)_networkThreadMain:(id)object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"com.ibireme.yykit.webimage.request"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

/// Global image request network thread, used by NSURLConnection delegate.
+ (NSThread *)_networkThread {
    static NSThread *thread = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
        if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
            thread.qualityOfService = NSQualityOfServiceBackground;
        }
        [thread start];
    });
    return thread;
}

二似芝、RunLoop如何保證NSTimer在視圖滑動(dòng)時(shí)依然能正常運(yùn)轉(zhuǎn)

  • 問(wèn)題描述:UITableView的header 上是一個(gè)橫向ScrollView那婉,使用NSTimer每隔幾秒切換一張圖片,當(dāng)滑動(dòng)UITableView的時(shí)頂部的scollView并不會(huì)切換圖片;UITableView有顯示倒計(jì)時(shí)的Label,當(dāng)滑動(dòng)tableView時(shí)倒計(jì)時(shí)就停止了党瓮。

  • 創(chuàng)建定時(shí)器的兩種方法

    方法1和方法2等價(jià)详炬,區(qū)別:方法2默認(rèn)也是將timer添加到NSDefaultRunLoopMode下的,并且會(huì)自動(dòng)fire寞奸。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 方法1
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [timer fire];
    
    // 方法2
    // [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
}
    
- (void)timerUpdate {
    NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
    NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
//    NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.count ++;
        NSString *timerText = [NSString stringWithFormat:@"計(jì)時(shí)器:%ld",self.count];
        self.timerLabel.text = timerText;
    });
}
2017-05-27 17:21:55.418 MXBarManagerDemo[32985:3929954] 當(dāng)前線程:<NSThread: 0x600000066140>{number = 1, name = main}
2017-05-27 17:21:55.419 MXBarManagerDemo[32985:3929954] 啟動(dòng)RunLoop后--kCFRunLoopDefaultMode
  • 原因:滑動(dòng)scrollView時(shí)主線程的RunLoop 會(huì)切換到UITrackingRunLoopMode這個(gè)Mode呛谜,執(zhí)行的也是UITrackingRunLoopMode下的任務(wù)(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的枪萄,所以timer任務(wù)并不會(huì)執(zhí)行隐岛。只有當(dāng)UITrackingRunLoopMode的任務(wù)執(zhí)行完畢,RunLoop切換到NSDefaultRunLoopMode后瓷翻,才會(huì)繼續(xù)執(zhí)行timer聚凹。

  • 解決方法:需要在添加timer 時(shí)割坠,將mode 設(shè)置為NSRunLoopCommonModes即可,只針對(duì)方法1妒牙。方法2因?yàn)槭枪潭ㄌ砑拥絛efaultMode中彼哼,就不要用了。

關(guān)于timer的坑

上面的示例是在主線程中使用timer湘今。在子線程中使用timer也可解決上面的問(wèn)題敢朱,但需注意的是把timer加入到當(dāng)前runloop后,必須讓runloop 運(yùn)行起來(lái)象浑,否則timer僅執(zhí)行一次蔫饰。

- (void)viewDidLoad {
    [super viewDidLoad];
    ......
    
    [self createThread];
}

- (void)createThread {
    NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
    [subThread start];
    self.subThread = subThread;
}

- (void)timerTest {
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        NSLog(@"啟動(dòng)RunLoop前--%@",runLoop.currentMode);
        NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
        // 方法1
        //    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
        //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        //    [timer fire];
        
        // 方法2
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
        
        [[NSRunLoop currentRunLoop] run];
    }
}

- (void)timerUpdate {
    NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
    NSLog(@"啟動(dòng)RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
    //    NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.count ++;
        NSString *timerText = [NSString stringWithFormat:@"計(jì)時(shí)器:%ld",self.count];
        self.timerLabel.text = timerText;
    });
}

添加timer 前的控制臺(tái)輸出:

2017-05-27 22:31:41.162 MXBarManagerDemo[704:80646] 啟動(dòng)RunLoop前--(null)
2017-05-27 22:31:41.163 MXBarManagerDemo[704:80646] currentRunLoop:<CFRunLoop 0x60000016b340 [0x10d83ce40]>{wakeup port = 0x741b, stopped = false, ignoreWakeUps = true, 
current mode = (none),
common modes = <CFBasicHash 0x600000240450 [0x10d83ce40]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x10d814970 [0x10d83ce40]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x600000242ca0 [0x10d83ce40]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x600000199570 [0x10d83ce40]>{name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x600000168ac0, source = 0x6000001d8600 (not fired), timer port = 0x7603, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = (null),
    currently 517588301 (785273542974) / soft deadline in: 1.84467433e+10 sec (@ -1) / hard deadline in: 1.84467433e+10 sec (@ -1)
},

}
}

添加timer后的控制臺(tái)輸出:

2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 當(dāng)前線程:<NSThread: 0x61000006e180>{number = 3, name = (null)}
2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 啟動(dòng)RunLoop后--kCFRunLoopDefaultMode
2017-05-27 22:32:33.927 MXBarManagerDemo[704:80646] currentRunLoop:<CFRunLoop 0x60000016b340 [0x10d83ce40]>{wakeup port = 0x741b, stopped = false, ignoreWakeUps = true, 
current mode = kCFRunLoopDefaultMode,
common modes = <CFBasicHash 0x600000240450 [0x10d83ce40]>{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x10d814970 [0x10d83ce40]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x600000242ca0 [0x10d83ce40]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x600000199570 [0x10d83ce40]>{name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x600000168ac0, source = 0x6000001d8600 (not fired), timer port = 0x7603, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = <CFArray 0x6180000bda60 [0x10d83ce40]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x618000169000 [0x10d83ce40]>{valid = Yes, firing = Yes, interval = 5, tolerance = 0, next fire date = 517588354 (-0.00657904148 @ 838031380701), callout = (NSTimer) [TestRunLoopViewController timerUpdate] (0x10cb44ec4 / 0x10ca00960) (/Users/yitudev/Library/Developer/CoreSimulator/Devices/CA10957A-B14D-4E49-80EE-E2B23C4E6183/data/Containers/Bundle/Application/8809F8A7-130A-4AC5-B9D6-798FFB53C6B1/MXBarManagerDemo.app/MXBarManagerDemo), context = <CFRunLoopTimer context 0x618000024880>}
)},
    currently 517588354 (838035515992) / soft deadline in: 1.84467432e+10 sec (@ -1) / hard deadline in: 1.84467432e+10 sec (@ -1)
},

}
}
  • 從控制臺(tái)輸出可以看出,timer確實(shí)被添加到NSDefaultRunLoopMode中了愉豺÷ㄓ酰可是添加到子線程中的NSDefaultRunLoopMode里,無(wú)論如何滾動(dòng)蚪拦,timer都能夠很正常的運(yùn)轉(zhuǎn)杖剪。

解釋:多線程與runloop的關(guān)系 —— 每一個(gè)線程都有一個(gè)與之關(guān)聯(lián)的RunLoop,而每一個(gè)RunLoop可能會(huì)有多個(gè)Mode驰贷。CPU會(huì)在多個(gè)線程間切換來(lái)執(zhí)行任務(wù)盛嘿,呈現(xiàn)出多個(gè)線程同時(shí)執(zhí)行的效果。執(zhí)行的任務(wù)其實(shí)就是RunLoop去各個(gè)Mode里執(zhí)行各個(gè)item括袒。因?yàn)镽unLoop是獨(dú)立的兩個(gè)次兆,相互不會(huì)影響,所以在子線程添加timer锹锰,滑動(dòng)視圖時(shí)芥炭,timer能正常運(yùn)行。

  • 結(jié)論

1恃慧、如果是在主線程中運(yùn)行timer园蝠,想要timer在某界面有視圖滾動(dòng)時(shí)依然能正常運(yùn)轉(zhuǎn),那么將timer添加到RunLoop中時(shí)痢士,就需要設(shè)置mode 為NSRunLoopCommonModes彪薛。

2、如果是在子線程中運(yùn)行timer怠蹂,那么將timer添加到RunLoop中后善延,Mode設(shè)置為NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保證RunLoop在運(yùn)行城侧,且其中有任務(wù)易遣。


三、RunLoop如何保證不影響UI卡頓

  • 問(wèn)題描述:UITableView赞庶、UICollectionView等延遲加載圖片训挡。

以UITableView 的 cell 上顯示網(wǎng)絡(luò)圖片為例,需要兩步:1歧强、下載網(wǎng)絡(luò)圖片澜薄;2、將網(wǎng)絡(luò)圖片設(shè)置到UIImageView上摊册。為了不影響滑動(dòng)第1步一般都是放在子線程處理肤京,第2步回到主線程設(shè)置。model切換調(diào)用方法performSelector:withObject:afterDelay:inModes:茅特,如下(方法2):

UIImage *downloadedImage = ....;
// 方法1
// self.myImageView.image = downloadedImage;
// 方法2
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }

    for (NSInteger i = 1; i <= 5; i++) {
        [[cell.contentView viewWithTag:i] removeFromSuperview];
    }

    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 300, 25)];
    label.backgroundColor = [UIColor clearColor];
    label.textColor = [UIColor redColor];
    label.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
    label.font = [UIFont boldSystemFontOfSize:13];
    label.tag = 1;
    [cell.contentView addSubview:label];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(105, 20, 85, 85)];
    imageView.tag = 2;
    NSString *path = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.image = image; // 方法1設(shè)置圖片
//    [imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; // 方法2設(shè)置圖片
    NSLog(@"current:%@", [NSRunLoop currentRunLoop].currentMode);
    [cell.contentView addSubview:imageView];

    UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 85, 85)];
    imageView2.tag = 3;
    UIImage *image2 = [UIImage imageWithContentsOfFile:path];
    imageView2.contentMode = UIViewContentModeScaleAspectFit;
    imageView2.image = image2;
//    [imageView2 performSelectorOnMainThread:@selector(setImage:) withObject:image2 waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
    [cell.contentView addSubview:imageView2];

    UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(5, 99, 300, 35)];
    label2.lineBreakMode = NSLineBreakByWordWrapping;
    label2.numberOfLines = 0;
    label2.backgroundColor = [UIColor clearColor];
    label2.textColor = [UIColor colorWithRed:0 green:100.f / 255.f blue:0 alpha:1];
    label2.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
    label2.font = [UIFont boldSystemFontOfSize:13];
    label2.tag = 4;

    UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];
    imageView3.tag = 5;
    UIImage *image3 = [UIImage imageWithContentsOfFile:path];
    imageView3.contentMode = UIViewContentModeScaleAspectFit;
    imageView3.image = image3;
//    [imageView3 performSelectorOnMainThread:@selector(setImage:) withObject:image3 waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
    [cell.contentView addSubview:label2];
    [cell.contentView addSubview:imageView3];

    return cell;
}

如上所示忘分,一個(gè)Cell里有兩個(gè)Label,和三個(gè)imageView,這里的圖片是非常高清的白修。

1妒峦、方法1:為imageView設(shè)置image,是在UITrackingRunLoopMode中進(jìn)行的,如果圖片很大兵睛,圖片解壓縮和渲染肯定會(huì)很耗時(shí)肯骇,那么卡頓就是必然的。

2祖很、方法2: 切換到NSDefaultRunLoopMode中笛丙,一個(gè)runloop循環(huán)要解壓和渲染18張大圖(假如一個(gè)頁(yè)面能顯示6行,每行3張圖)假颇,耗時(shí)肯定超過(guò)50ms(1/60s)胚鸯。我們可以繼續(xù)來(lái)優(yōu)化,一次runloop循環(huán)笨鸡,僅渲染一張大圖片姜钳,分18次來(lái)渲染,這樣每一次runloop耗時(shí)就比較短了镜豹,滑動(dòng)起來(lái)就會(huì)非常順暢傲须。這也是 RunLoopWorkDistribution 中的做法,即:首先創(chuàng)建一個(gè)單例趟脂,單例中定義了幾個(gè)數(shù)組泰讽,用來(lái)存要在runloop循環(huán)中執(zhí)行的任務(wù),然后為主線程的runloop添加一個(gè)CFRunLoopObserver,當(dāng)主線程在NSDefaultRunLoopMode中執(zhí)行完任務(wù)昔期,即將睡眠前已卸,執(zhí)行一個(gè)單例中保存的一次圖片渲染任務(wù)。關(guān)鍵代碼看 RunLoopWorkDistribution 類即可硼一。


四累澡、使用RunLoop 監(jiān)測(cè)主線程卡頓

  • 問(wèn)題描述:用RunLoop 監(jiān)測(cè)主線程的卡頓,并將卡頓時(shí)的線程堆棧信息保存下來(lái)般贼,下次上傳到服務(wù)器愧哟。

  • RunLoop 的內(nèi)部邏輯:


    image
  • 偽代碼如下:

{
    /// 1. 通知Observers奥吩,即將進(jìn)入RunLoop
    /// 此處有Observer會(huì)創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {

        /// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)蕊梧。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)霞赫。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 6. 通知Observers,即將進(jìn)入休眠
        /// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();


        /// 8. 通知Observers肥矢,線程被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

        /// 9. 如果是被Timer喚醒的端衰,回調(diào)Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

        /// 9. 如果是被dispatch喚醒的,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了甘改,處理這個(gè)事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


    } while (...);

    /// 10. 通知Observers旅东,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
  • 實(shí)現(xiàn)思路:主線程的RunLoop是在應(yīng)用啟動(dòng)時(shí)自動(dòng)開啟的,也沒(méi)有超時(shí)時(shí)間十艾,所以正常情況下抵代,主線程的RunLoop 只會(huì)在 步驟2—9 之間無(wú)限循環(huán)下去。
    那么忘嫉,我們只需要在主線程的RunLoop中添加一個(gè)observer主守,檢測(cè)從 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 花費(fèi)的時(shí)間是否過(guò)長(zhǎng)。如果花費(fèi)的時(shí)間大于某一個(gè)闕值榄融,我們就認(rèn)為有卡頓参淫,并把當(dāng)前的線程堆棧轉(zhuǎn)儲(chǔ)到文件中,并在以后某個(gè)合適的時(shí)間愧杯,將卡頓信息文件上傳到服務(wù)器涎才。

  • 代碼如下:

#import <Foundation/Foundation.h>

@interface FluencyMonitor : NSObject

+ (instancetype)shareMonitor;

/**
 開始監(jiān)控

 @param interval 定時(shí)器間隔時(shí)間
 @param fault 卡頓的闕值
 */
- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault;

/**
 開始監(jiān)控
 */
- (void)start;

/**
 停止監(jiān)控
 */
- (void)stop;

@end
#import "FluencyMonitor.h"

#import <CrashReporter/CrashReporter.h>

@interface FluencyMonitor ()

@property (strong, nonatomic) NSThread *monitorThread;       /**< 監(jiān)控線程 */
@property (assign, nonatomic) CFRunLoopObserverRef observer; /**< 觀察者 */
@property (assign, nonatomic) CFRunLoopTimerRef timer;       /**< 定時(shí)器 */

@property (strong, nonatomic) NSDate *startDate; /**< 開始執(zhí)行的時(shí)間 */
@property (assign, nonatomic) BOOL excuting;     /**< 執(zhí)行時(shí)長(zhǎng) */

@property (assign, nonatomic) NSTimeInterval interval; /**< 定時(shí)器間隔時(shí)間 */
@property (assign, nonatomic) NSTimeInterval fault;    /**< 卡頓的闕值 */

@end

@implementation FluencyMonitor

static FluencyMonitor *instance = nil;

/**
 第一步:創(chuàng)建一個(gè)子線程,在線程啟動(dòng)時(shí)力九,啟動(dòng)其RunLoop

 @return <#return value description#>
 */
+ (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;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

/**
 子線程中啟動(dòng)RunLoop
 */
+ (void)monitorThreadEntryPoint {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"FluencyMonitor"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

/**
 第二步:開始監(jiān)控耍铜,往主線程的RunLoop中添加一個(gè)observer,并往子線程中添加一個(gè)定時(shí)器跌前,每0.5秒檢測(cè)一次耗時(shí)的時(shí)長(zhǎng)
 */
- (void)start {
    [self startWithInterval:1.0 fault:2.0];
}

/**
 開始監(jiān)控

 @param interval 定時(shí)器間隔時(shí)間
 @param fault 卡頓的闕值:超出該闕值則被視為卡頓
 */
- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault {
    _interval = interval;
    _fault = fault;

    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)建一個(gè)timer棕兼,并添加到子線程的RunLoop中
    [self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}

/**
 創(chuàng)建一個(gè)定時(shí)器timer并添加到子線程的RunLoop中
 */
- (void)addTimerToMonitorThread {
    if (_timer) {
        return;
    }
    
    // 1.創(chuàng)建一個(gè)timer
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
    CFRunLoopTimerContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
    _timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, _interval, 0, 0, &runLoopTimerCallBack, &context);
    
    // 2.添加到子線程的RunLoop中
    CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
}

/**
 移除定時(shí)器
 */
- (void)removeTimer {
    if (_timer) {
        CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
        CFRunLoopRemoveTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
        CFRelease(_timer);
        _timer = NULL;
    }
}

/**
 從主線程中移除觀察者observer
 */
- (void)stop {
    if (_observer) {
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
        CFRelease(_observer);
        _observer = NULL;
    }

    [self performSelector:@selector(removeTimer) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}

/**
 處理卡頓信息:如上傳到服務(wù)器等
 */
- (void)handleStackInfo {
    NSData *lagData = [[[PLCrashReporter alloc] initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
    PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
    NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
    //將字符串上傳服務(wù)器
    NSLog(@"lag happen, detail below: \n %@", lagReportString);
}

/**
 觀察者回調(diào)處理:主線程中的block、交互事件抵乓、以及其他任務(wù)都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前執(zhí)行伴挚,所以我在即將開始執(zhí)行Sources 時(shí),記錄一下時(shí)間灾炭,并把正在執(zhí)行任務(wù)的標(biāo)記置為YES茎芋,將要進(jìn)入睡眠狀態(tài)時(shí),將正在執(zhí)行任務(wù)的標(biāo)記置為NO

 @param observer <#observer description#>
 @param activity <#activity description#>
 @param info <#info description#>
 */
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;
    }
}

/**
 定時(shí)器回調(diào)

 @param timer <#timer description#>
 @param info <#info description#>
 */
static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {
    FluencyMonitor *monitor = (__bridge FluencyMonitor *)info;
    if (!monitor.excuting) {
        return;
    }

    // 如果主線程正在執(zhí)行任務(wù)蜈出,并且這一次loop執(zhí)行到 現(xiàn)在還沒(méi)執(zhí)行完田弥,那就需要計(jì)算時(shí)間差
    NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
    NSLog(@"定時(shí)器---%@", [NSThread currentThread]);
    NSLog(@"主線程執(zhí)行了---%f秒", excuteTime);

    if (excuteTime >= monitor.fault) {
        // 執(zhí)行時(shí)間大于閾值時(shí)處理卡頓信息
        NSLog(@"線程卡頓了%f秒", excuteTime);
        [monitor handleStackInfo];
    }
}

@end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市铡原,隨后出現(xiàn)的幾起案子偷厦,更是在濱河造成了極大的恐慌商叹,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件只泼,死亡現(xiàn)場(chǎng)離奇詭異沈自,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)辜妓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)忌怎,“玉大人籍滴,你說(shuō)我怎么就攤上這事×裥ィ” “怎么了孽惰?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鸥印。 經(jīng)常有香客問(wèn)我勋功,道長(zhǎng),這世上最難降的妖魔是什么库说? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任狂鞋,我火速辦了婚禮,結(jié)果婚禮上潜的,老公的妹妹穿的比我還像新娘骚揍。我一直安慰自己,他們只是感情好啰挪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布信不。 她就那樣靜靜地躺著,像睡著了一般亡呵。 火紅的嫁衣襯著肌膚如雪抽活。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天锰什,我揣著相機(jī)與錄音下硕,去河邊找鬼。 笑死汁胆,一個(gè)胖子當(dāng)著我的面吹牛卵牍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沦泌,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼糊昙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了谢谦?” 一聲冷哼從身側(cè)響起释牺,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萝衩,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后没咙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猩谊,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年祭刚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牌捷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涡驮,死狀恐怖暗甥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捉捅,我是刑警寧澤撤防,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站棒口,受9級(jí)特大地震影響寄月,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜无牵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一漾肮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茎毁,春花似錦初橘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至崔梗,卻和暖如春夜只,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒜魄。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工扔亥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谈为。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓旅挤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親伞鲫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粘茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容