以下關于RunLoop的資料都比較好:
RunLoop充滿靈性的死循環:http://www.lxweimin.com/p/b9426458fcf6
視頻——iOS線下分享RunLoop(孫源):http://v.youku.com/v_show/id_XODgxODkzODI0.html
非常好的博文Runloop:https://www.zybuluo.com/qidiandasheng/note/346387
-
NSTimer需要注意的地方:https://www.zybuluo.com/qidiandasheng/note/492821
- 問題:如果我就是想讓這個 NSTimer 一直輸出,直到 DemoViewController 銷毀了才停止,我該如何讓它停止呢? http://www.cocoachina.com/ios/20150710/12444.html
- Demo示例:https://github.com/ChatGame/HWWeakTimer
RunLoop 入門 看我就夠了:http://ios.jobbole.com/92177/
【iOS程序啟動與運轉】- RunLoop個人小結 http://www.lxweimin.com/p/37ab0397fec7#
深入理解RunLoop[http://blog.ibireme.com/2015/05/18/runloop/]
RunLoop使用場景
一、保證線程長時間存活
- 問題描述:不希望一些花費時間較長的操作阻塞主線程而導致界面卡頓,就需要創建一個子線程,然后把該操作放在子線程中來處理。可是當子線程中的任務執行完畢后,子線程就會被銷毀掉。
@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(@"%@----子線程任務開始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子線程任務結束",[NSThread currentThread]);
}
}
2017-05-27 11:05:05.444 MXBarManagerDemo[23405:2814835] <YTThread: 0x600000268300>{number = 3, name = (null)}----子線程任務開始
2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] <YTThread: 0x600000268300>{number = 3, name = (null)}----子線程任務結束
2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] -[YTThread dealloc]
- 當子線程中任務執行完后線程被立刻銷毀。如果程序中需要經常在子線程中執行任務,頻繁的創建和銷毀線程會造成資源的浪費。這時可以使用RunLoop來讓該線程長時間存活而不被銷毀。如下所示:
@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(@"啟動RunLoop前--%@",runLoop.currentMode);
[runLoop run];
}
}
- (void)subThreadOpetion {
@autoreleasepool {
NSLog(@"%@----子線程任務開始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子線程任務結束",[NSThread currentThread]);
}
}
@end
2017-05-27 11:17:26.064 MXBarManagerDemo[23458:2865125] 啟動RunLoop前--(null)
2017-05-27 11:17:30.627 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務開始
2017-05-27 11:17:33.632 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務結束
2017-05-27 11:17:36.319 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務開始
2017-05-27 11:17:39.325 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務結束
2017-05-27 11:17:56.479 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務開始
2017-05-27 11:17:59.482 MXBarManagerDemo[23458:2865125] <YTThread: 0x600000269640>{number = 3, name = YTThread}----子線程任務結束
注意幾點:
1、獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]。
應用程序并不需要自己創建RunLoop,而是要在合適的時間啟動runloop。 CF框架源碼中有CFRunLoopGetCurrent(void) 和 CFRunLoopGetMain(void),查看源碼可知,這兩個API中,都是先從全局字典中取。如果沒有與該線程對應的RunLoop,那么就會幫我們創建一個RunLoop(創建RunLoop的過程在函數_CFRunLoopGet0(pthread_t t)中)。
2、即使RunLoop開始運行,如果RunLoop 中的 modes 為空,或者要執行的mode里沒有item,那么RunLoop會直接在當前loop中返回,并進入睡眠狀態。
如注釋掉[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];(查看注釋前后打印出的runLoop),點擊視圖,控制臺不會有任何輸出,因為mode 中并沒有item任務。經過NSRunLoop封裝后,只可以往mode中添加兩類item任務:NSPort(對應的是source)、NSTimer。如果使用CFRunLoopRef,則可以使用C語言API,往mode中添加source、timer、observer。
3、自己創建的Thread中的任務是在kCFRunLoopDefaultMode這個mode中執行的。
查看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] 啟動RunLoop前--(null)
2017-05-27 14:16:58.114 MXBarManagerDemo[29040:3134640] <YTThread: 0x61800007a0c0>{number = 3, name = YTThread}----子線程任務開始
2017-05-27 14:17:01.115 MXBarManagerDemo[29040:3134640] <YTThread: 0x61800007a0c0>{number = 3, name = YTThread}----子線程任務結束
4、在子線程創建好后,最好所有的任務都放在AutoreleasePool中。
- 舉例
YYKit中使用YYWebImageOperation對網絡圖片進行下載請求,使用[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];將任務丟到后臺線程的 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在視圖滑動時依然能正常運轉
問題描述:UITableView的header 上是一個橫向ScrollView,使用NSTimer每隔幾秒切換一張圖片,當滑動UITableView的時頂部的scollView并不會切換圖片;UITableView有顯示倒計時的Label,當滑動tableView時倒計時就停止了。
-
創建定時器的兩種方法
方法1和方法2等價,區別:方法2默認也是將timer添加到NSDefaultRunLoopMode下的,并且會自動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(@"當前線程:%@",[NSThread currentThread]);
NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
// NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
dispatch_async(dispatch_get_main_queue(), ^{
self.count ++;
NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
self.timerLabel.text = timerText;
});
}
2017-05-27 17:21:55.418 MXBarManagerDemo[32985:3929954] 當前線程:<NSThread: 0x600000066140>{number = 1, name = main}
2017-05-27 17:21:55.419 MXBarManagerDemo[32985:3929954] 啟動RunLoop后--kCFRunLoopDefaultMode
原因:滑動scrollView時主線程的RunLoop 會切換到UITrackingRunLoopMode這個Mode,執行的也是UITrackingRunLoopMode下的任務(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的,所以timer任務并不會執行。只有當UITrackingRunLoopMode的任務執行完畢,RunLoop切換到NSDefaultRunLoopMode后,才會繼續執行timer。
解決方法:需要在添加timer 時,將mode 設置為NSRunLoopCommonModes即可,只針對方法1。方法2因為是固定添加到defaultMode中,就不要用了。
關于timer的坑
上面的示例是在主線程中使用timer。在子線程中使用timer也可解決上面的問題,但需注意的是把timer加入到當前runloop后,必須讓runloop 運行起來,否則timer僅執行一次。
- (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(@"啟動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(@"當前線程:%@",[NSThread currentThread]);
NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
// NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
dispatch_async(dispatch_get_main_queue(), ^{
self.count ++;
NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
self.timerLabel.text = timerText;
});
}
添加timer 前的控制臺輸出:
2017-05-27 22:31:41.162 MXBarManagerDemo[704:80646] 啟動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后的控制臺輸出:
2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 當前線程:<NSThread: 0x61000006e180>{number = 3, name = (null)}
2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 啟動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)
},
}
}
- 從控制臺輸出可以看出,timer確實被添加到NSDefaultRunLoopMode中了。可是添加到子線程中的NSDefaultRunLoopMode里,無論如何滾動,timer都能夠很正常的運轉。
解釋:多線程與runloop的關系 —— 每一個線程都有一個與之關聯的RunLoop,而每一個RunLoop可能會有多個Mode。CPU會在多個線程間切換來執行任務,呈現出多個線程同時執行的效果。執行的任務其實就是RunLoop去各個Mode里執行各個item。因為RunLoop是獨立的兩個,相互不會影響,所以在子線程添加timer,滑動視圖時,timer能正常運行。
- 結論
1、如果是在主線程中運行timer,想要timer在某界面有視圖滾動時依然能正常運轉,那么將timer添加到RunLoop中時,就需要設置mode 為NSRunLoopCommonModes。
2、如果是在子線程中運行timer,那么將timer添加到RunLoop中后,Mode設置為NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保證RunLoop在運行,且其中有任務。
三、RunLoop如何保證不影響UI卡頓
- 問題描述:UITableView、UICollectionView等延遲加載圖片。
以UITableView 的 cell 上顯示網絡圖片為例,需要兩步:1、下載網絡圖片;2、將網絡圖片設置到UIImageView上。為了不影響滑動第1步一般都是放在子線程處理,第2步回到主線程設置。model切換調用方法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設置圖片
// [imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; // 方法2設置圖片
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;
}
如上所示,一個Cell里有兩個Label,和三個imageView,這里的圖片是非常高清的。
1、方法1:為imageView設置image,是在UITrackingRunLoopMode中進行的,如果圖片很大,圖片解壓縮和渲染肯定會很耗時,那么卡頓就是必然的。
2、方法2: 切換到NSDefaultRunLoopMode中,一個runloop循環要解壓和渲染18張大圖(假如一個頁面能顯示6行,每行3張圖),耗時肯定超過50ms(1/60s)。我們可以繼續來優化,一次runloop循環,僅渲染一張大圖片,分18次來渲染,這樣每一次runloop耗時就比較短了,滑動起來就會非常順暢。這也是 RunLoopWorkDistribution 中的做法,即:首先創建一個單例,單例中定義了幾個數組,用來存要在runloop循環中執行的任務,然后為主線程的runloop添加一個CFRunLoopObserver,當主線程在NSDefaultRunLoopMode中執行完任務,即將睡眠前,執行一個單例中保存的一次圖片渲染任務。關鍵代碼看 RunLoopWorkDistribution 類即可。
四、使用RunLoop 監測主線程卡頓
問題描述:用RunLoop 監測主線程的卡頓,并將卡頓時的線程堆棧信息保存下來,下次上傳到服務器。
-
RunLoop 的內部邏輯:
image 偽代碼如下:
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會創建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發 Timer 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基于port的,Source0) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基于port的) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有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喚醒的,回調Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執行所有調用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了,處理這個事件
__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);
}
實現思路:主線程的RunLoop是在應用啟動時自動開啟的,也沒有超時時間,所以正常情況下,主線程的RunLoop 只會在 步驟2—9 之間無限循環下去。
那么,我們只需要在主線程的RunLoop中添加一個observer,檢測從 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 花費的時間是否過長。如果花費的時間大于某一個闕值,我們就認為有卡頓,并把當前的線程堆棧轉儲到文件中,并在以后某個合適的時間,將卡頓信息文件上傳到服務器。代碼如下:
#import <Foundation/Foundation.h>
@interface FluencyMonitor : NSObject
+ (instancetype)shareMonitor;
/**
開始監控
@param interval 定時器間隔時間
@param fault 卡頓的闕值
*/
- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault;
/**
開始監控
*/
- (void)start;
/**
停止監控
*/
- (void)stop;
@end
#import "FluencyMonitor.h"
#import <CrashReporter/CrashReporter.h>
@interface FluencyMonitor ()
@property (strong, nonatomic) NSThread *monitorThread; /**< 監控線程 */
@property (assign, nonatomic) CFRunLoopObserverRef observer; /**< 觀察者 */
@property (assign, nonatomic) CFRunLoopTimerRef timer; /**< 定時器 */
@property (strong, nonatomic) NSDate *startDate; /**< 開始執行的時間 */
@property (assign, nonatomic) BOOL excuting; /**< 執行時長 */
@property (assign, nonatomic) NSTimeInterval interval; /**< 定時器間隔時間 */
@property (assign, nonatomic) NSTimeInterval fault; /**< 卡頓的闕值 */
@end
@implementation FluencyMonitor
static FluencyMonitor *instance = nil;
/**
第一步:創建一個子線程,在線程啟動時,啟動其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;
}
/**
子線程中啟動RunLoop
*/
+ (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 {
[self startWithInterval:1.0 fault:2.0];
}
/**
開始監控
@param interval 定時器間隔時間
@param fault 卡頓的闕值:超出該闕值則被視為卡頓
*/
- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault {
_interval = interval;
_fault = fault;
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]];
}
/**
創建一個定時器timer并添加到子線程的RunLoop中
*/
- (void)addTimerToMonitorThread {
if (_timer) {
return;
}
// 1.創建一個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);
}
/**
移除定時器
*/
- (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]];
}
/**
處理卡頓信息:如上傳到服務器等
*/
- (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];
//將字符串上傳服務器
NSLog(@"lag happen, detail below: \n %@", lagReportString);
}
/**
觀察者回調處理:主線程中的block、交互事件、以及其他任務都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前執行,所以我在即將開始執行Sources 時,記錄一下時間,并把正在執行任務的標記置為YES,將要進入睡眠狀態時,將正在執行任務的標記置為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;
}
}
/**
定時器回調
@param timer <#timer description#>
@param info <#info description#>
*/
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 >= monitor.fault) {
// 執行時間大于閾值時處理卡頓信息
NSLog(@"線程卡頓了%f秒", excuteTime);
[monitor handleStackInfo];
}
}
@end