參考文章
深入理解RunLoop(不要看到右邊滾動條很長,其實文章占篇幅2/5左右,下面有很多的評論羹铅,可見這篇文章的火熱)
RunLoop個人小結(jié) (這是一篇總結(jié)的很通俗容易理解的文章)
sunnyxx線下分享RunLoop(這是一份關(guān)于線下分享與討論RunLoop的視頻香缺,備用地址:https://pan.baidu.com/s/1pLm4Vf9)
RunLoop的使用場景
下面介紹一下奥额,可以使用RunLoop的幾個使用場景。
1.保證線程的長時間存活
在iOS開發(fā)過程中访诱,有時候我們不希望一些花費(fèi)時間比較長的操作阻塞主線程披坏,導(dǎo)致界面卡頓,那么我們就會創(chuàng)建一個子線程盐数,然后把這些花費(fèi)時間比較長的操作放在子線程中來處理棒拂。可是當(dāng)子線程中的任務(wù)執(zhí)行完畢后玫氢,子線程就會被銷毀掉帚屉。 怎么來驗證上面這個結(jié)論呢?首先漾峡,我們創(chuàng)建一個HLThread類攻旦,繼承自NSThread,然后重寫dealloc 方法生逸。
@interface HLThread : NSThread
?
@end
?
@implementation HLThread
?
- (void)dealloc
{
NSLog(@"%s",__func__);
}
?
@end
然后牢屋,在控制器中用HLThread創(chuàng)建一個線程,執(zhí)行一個任務(wù)槽袄,觀察任務(wù)執(zhí)行完畢后烙无,線程是否被銷毀。
- (void)viewDidLoad {
[super viewDidLoad];
?
// 1.測試線程的銷毀
[self threadTest];
}
?
- (void)threadTest
{
HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];
[subThread start];
}
?
- (void)subThreadOpetion
{
@autoreleasepool {
NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
}
}
控制臺輸出的結(jié)果如下:
2016-12-01 16:44:25.559 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務(wù)開始
2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務(wù)結(jié)束
2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] -[HLThread dealloc]
當(dāng)子線程中的任務(wù)執(zhí)行完畢后遍尺,線程就被立刻銷毀了截酷。如果程序中,需要經(jīng)常在子線程中執(zhí)行任務(wù)乾戏,頻繁的創(chuàng)建和銷毀線程迂苛,會造成資源的浪費(fèi)。這時候我們就可以使用RunLoop來讓該線程長時間存活而不被銷毀鼓择。
我們將上面的示例代碼修改一下三幻,修改后的代碼過程為,創(chuàng)建一個子線程呐能,當(dāng)子線程啟動后念搬,啟動runloop,點擊視圖催跪,會在子線程中執(zhí)行一個耗時3秒的任務(wù)(其實就是讓線程睡眠3秒)锁蠕。
修改后的代碼如下:
@implementation ViewController
?
- (void)viewDidLoad {
[super viewDidLoad];
?
// 1.測試線程的銷毀
[self threadTest];
}
?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];
}
?
- (void)threadTest
{
HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
[subThread setName:@"HLThread"];
[subThread start];
self.subThread = subThread;
}
?
/**
子線程啟動后,啟動runloop
*/
- (void)subThreadEntryPoint
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//如果注釋了下面這一行懊蒸,子線程中的任務(wù)并不能正常執(zhí)行
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
[runLoop run];
}
}
?
/**
子線程任務(wù)
*/
- (void)subThreadOpetion
{
NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
[NSThread sleepForTimeInterval:3.0];
NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
}
?
@end
先看控制臺輸出結(jié)果:
2016-12-01 17:22:44.396 RunLoopDemo[4733:369202] 啟動RunLoop前--(null)
2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-01 17:22:52.359 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束
2016-12-01 17:22:55.244 RunLoopDemo[4733:369202] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-01 17:22:55.245 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-01 17:22:58.319 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束
有幾點需要注意: 1.獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]; 2.即使RunLoop開始運(yùn)行荣倾,如果RunLoop 中的 modes為空,或者要執(zhí)行的mode里沒有item骑丸,那么RunLoop會直接在當(dāng)前l(fā)oop中返回舌仍,并進(jìn)入睡眠狀態(tài)妒貌。 3.自己創(chuàng)建的Thread中的任務(wù)是在kCFRunLoopDefaultMode這個mode中執(zhí)行的。 4.在子線程創(chuàng)建好后铸豁,最好所有的任務(wù)都放在AutoreleasePool中灌曙。
注意點一解釋 RunLoop官方文檔中的第二段中就已經(jīng)說明了,我們的應(yīng)用程序并不需要自己創(chuàng)建RunLoop节芥,而是要在合適的時間啟動runloop在刺。 CF框架源碼中有CFRunLoopGetCurrent(void)
和CFRunLoopGetMain(void)
,查看源碼可知,這兩個API中头镊,都是先從全局字典中取蚣驼,如果沒有與該線程對應(yīng)的RunLoop,那么就會幫我們創(chuàng)建一個RunLoop(創(chuàng)建RunLoop的過程在函數(shù)_CFRunLoopGet0(pthread_tt)
中)相艇。
注意點二解釋 這一點颖杏,可以將示例代碼中的[runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];
,可以看到注釋掉后坛芽,無論我們?nèi)绾吸c擊視圖留储,控制臺都不會有任何的輸出,那是因為mode中并沒有item任務(wù)咙轩。經(jīng)過NSRunLoop封裝后获讳,只可以往mode中添加兩類item任務(wù):NSPort(對應(yīng)的是source)、NSTimer臭墨,如果使用CFRunLoopRef
,則可以使用C語言API,往mode中添加source赔嚎、timer膘盖、observer胧弛。 如果不添加 [runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];
,我們把runloop的信息輸出侠畔,可以看到:
如果我們添加上[runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];
,再把RunLoop的信息輸出结缚,可以看到:
注意點三解釋 怎么確認(rèn)自己創(chuàng)建的子線程上的任務(wù)是在kCFRunLoopDefaultMode這個mode中執(zhí)行的呢? 我們只需要在執(zhí)行任務(wù)的時候软棺,打印出該RunLoop的currentMode即可红竭。 因為RunLoop執(zhí)行任務(wù)是會在mode間切換,只執(zhí)行該mode上的任務(wù)喘落,每次切換到某個mode時茵宪,currentMode就會更新。源碼請下載:CF框架源碼 CFRunLoopRun()
方法中會調(diào)用CFRunLoopRunSpecific()
方法瘦棋,而CFRunLoopRunSpecific()
方法中有這么兩行關(guān)鍵代碼:
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
......這中間還有好多邏輯代碼
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
...... 這中間也有一堆的邏輯
rl->_currentMode = previousMode;
我測試后稀火,控制臺輸出的是:
2016-12-02 11:09:47.909 RunLoopDemo[5479:442560] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-02 11:09:47.910 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-02 11:09:50.984 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束
注意點四解釋 關(guān)于AutoReleasePool的官方文檔中有提到:
If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows.
If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
AFNetworking中的RunLoop案例
在AFNetworking2.6.3之前的版本,使用的還是NSURLConnection赌朋,可以在AFURLConnectionOperation
中找到使用RunLoop的源碼:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
?
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
AFNetworking都是通過調(diào)用 [NSObject performSelector:onThread:..] 將這個任務(wù)扔到了后臺線程的RunLoop 中凰狞。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
我們在使用NSURLConnection
或者NSStream
時篇裁,也需要考慮到RunLoop問題,因為默認(rèn)情況下這兩個類的對象生成后赡若,都是在當(dāng)前線程的NSDefaultRunLoopMode
模式下執(zhí)行任務(wù)达布。如果是在主線程,那么就會出現(xiàn)滾動ScrollView以及其子視圖時逾冬,主線程的RunLoop切換到UITrackingRunLoopMode
模式黍聂,那么NSURLConnection
或者NSStream
的回調(diào)就無法執(zhí)行了。
要解決這個問題身腻,有兩種方式: 第一種方式是創(chuàng)建出NSURLConnection
對象或者NSStream
對象后分冈,再調(diào)用 -(void)scheduleInRunLoop:(NSRunLoop *)aRunLoopforMode:(NSRunLoopMode)mode
,設(shè)置RunLoopMode即可。需要注意的是NSURLConnection
必須使用其初始化構(gòu)造方法-(nullable instancetype)initWithRequest:(NSURLRequest *)requestdelegate:(nullable id)delegatestartImmediately:(BOOL)startImmediately
來創(chuàng)建對象霸株,設(shè)置Mode才會起作用雕沉。
第二種方式,就是所有的任務(wù)都在子線程中執(zhí)行去件,并保證子線程的RunLoop正常運(yùn)行即可(即上面AFNetworking的做法坡椒,因為主線程的RunLoop切換到UITrackingRunLoopMode
,并不影響其他線程執(zhí)行哪個mode中的任務(wù)尤溜,計算機(jī)CPU是在每一個時間片切換到不同的線程去跑一會倔叼,呈現(xiàn)出的多線程效果)。
二宫莱、RunLoop如何保證NSTimer在視圖滑動時丈攒,依然能正常運(yùn)轉(zhuǎn)
使用場景
1.我們經(jīng)常會在應(yīng)用中看到tableView 的header上是一個橫向ScrollView,一般我們使用NSTimer授霸,每隔幾秒切換一張圖片巡验。可是當(dāng)我們滑動tableView的時候碘耳,頂部的scollView并不會切換圖片显设,這可怎么辦呢? 2.界面上除了有tableView辛辨,還有顯示倒計時的Label捕捂,當(dāng)我們在滑動tableView時,倒計時就停止了斗搞,這又該怎么辦呢指攒?
場景中的代碼實現(xiàn)
我們的定時器Timer是怎么寫的呢? 一般的做法是僻焚,在主線程
(可能是某控制器的viewDidLoad方法)中允悦,創(chuàng)建Timer。 可能會有兩種寫法溅呢,但是都有上面的問題澡屡,下面先看下Timer的兩種寫法:
// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
上面的兩種寫法其實是等價的猿挚。第二種寫法,默認(rèn)也是將timer添加到NSDefaultRunLoopMode
下的驶鹉,并且會自動fire绩蜻。。 要驗證這一結(jié)論室埋,我們只需要在timerUpdate方法中办绝,將當(dāng)前runLoop的currentMode打印出來即可。
- (void)timerUpdate
{
NSLog(@"當(dāng)前線程:%@",[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;
});
}
// 控制臺輸出結(jié)果:
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 當(dāng)前線程:<NSThread: 0x600000065500>{number = 1, name = main}
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 啟動RunLoop后--kCFRunLoopDefaultMode
然后姚淆,我們在滑動tableView的時候timerUpdate方法孕蝉,并不會調(diào)用。 ** 原因是啥呢腌逢?** 原因是當(dāng)我們滑動scrollView時降淮,主線程的RunLoop會切換到UITrackingRunLoopMode
這個Mode,執(zhí)行的也是UITrackingRunLoopMode
下的任務(wù)(Mode中的item)搏讶,而timer是添加在NSDefaultRunLoopMode
下的佳鳖,所以timer任務(wù)并不會執(zhí)行,只有當(dāng)UITrackingRunLoopMode
的任務(wù)執(zhí)行完畢媒惕,runloop切換到NSDefaultRunLoopMode
后系吩,才會繼續(xù)執(zhí)行timer。
** 要如何解決這一問題呢妒蔚?** 解決方法很簡單穿挨,我們只需要在添加timer 時,將mode 設(shè)置為NSRunLoopCommonModes
即可肴盏。
- ( void)timerTest
{
// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
// 第二種寫法科盛,因為是固定添加到defaultMode中,就不要用了
}
從RunLoop官方文檔和iPhonedevwiki中的CFRunLoop可以看出叁鉴,NSRunLoopCommonModes
并不是一種Mode土涝,而是一種特殊的標(biāo)記,關(guān)聯(lián)的有一個set幌墓,官方文檔說:For Cocoa applications, this set includes the default, modal, andevent tracking modes bydefault.
(默認(rèn)包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode冀泻、NSEventTrackingRunLoopMode) 添加到NSRunLoopCommonModes
中的還沒有執(zhí)行的任務(wù)常侣,會在mode切換時,再次添加到當(dāng)前的mode中弹渔,這樣就能保證不管當(dāng)前runloop切換到哪一個mode胳施,任務(wù)都能正常執(zhí)行。并且被添加到NSRunLoopCommonModes
中的任務(wù)會存儲在runloop的commonModeItems中肢专。
其他一些關(guān)于timer的坑
我們在子線程中使用timer舞肆,也可以解決上面的問題焦辅,但是需要注意的是把timer加入到當(dāng)前runloop后,必須讓runloop運(yùn)行起來椿胯,否則timer僅執(zhí)行一次筷登。
示例代碼:
//首先是創(chuàng)建一個子線程
- (void)createThread
{
NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
[subThread start];
self.subThread = subThread;
}
?
// 創(chuàng)建timer,并添加到runloop的mode中
- (void)timerTest
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
// 第一種寫法,改正前
// NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
?
[[NSRunLoop currentRunLoop] run];
}
}
?
//更新label
- (void)timerUpdate
{
NSLog(@"當(dāng)前線程:%@",[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 前的控制臺輸出:
添加timer后的控制臺輸出:
從控制臺輸出可以看出哩盲,timer確實被添加到NSDefaultRunLoopMode
中了前方。可是添加到子線程中的NSDefaultRunLoopMode
里廉油,無論如何滾動惠险,timer都能夠很正常的運(yùn)轉(zhuǎn)。這又是為啥呢抒线?
這就是多線程與runloop的關(guān)系了班巩,每一個線程都有一個與之關(guān)聯(lián)的RunLoop,而每一個RunLoop可能會有多個Mode嘶炭。CPU會在多個線程間切換來執(zhí)行任務(wù)趣竣,呈現(xiàn)出多個線程同時執(zhí)行的效果。執(zhí)行的任務(wù)其實就是RunLoop去各個Mode里執(zhí)行各個item旱物。因為RunLoop是獨立的兩個遥缕,相互不會影響,所以在子線程添加timer宵呛,滑動視圖時,timer能正常運(yùn)行户秤。
總結(jié)
1鸡号、如果是在主線程中運(yùn)行timer须鼎,想要timer在某界面有視圖滾動時,依然能正常運(yùn)轉(zhuǎn)晋控,那么將timer添加到RunLoop中時汞窗,就需要設(shè)置mode為NSRunLoopCommonModes
。 2赡译、如果是在子線程中運(yùn)行timer,那么將timer添加到RunLoop中后仲吏,Mode設(shè)置為NSDefaultRunLoopMode
或NSRunLoopCommonModes
均可,但是需要保證RunLoop在運(yùn)行,且其中有任務(wù)裹唆。
三誓斥、UITableView井厌、UICollectionView等的滑動優(yōu)化
讓UITableView矩父、UICollectionView等延遲加載圖片。下面就拿UITableView來舉例說明: UITableView 的 cell 上顯示網(wǎng)絡(luò)圖片名船,一般需要兩步舞吭,第一步下載網(wǎng)絡(luò)圖片泡垃;第二步,將網(wǎng)絡(luò)圖片設(shè)置到UIImageView上羡鸥。 為了不影響滑動蔑穴,第一步,我們一般都是放在子線程中來做捐腿,這個不做贅述。 第二步宪祥,一般是回到主線程去設(shè)置。有了前兩節(jié)文章關(guān)于Mode的切換耀找,想必你已經(jīng)知道怎么做了。 就是在為圖片視圖設(shè)置圖片時复罐,在主線程設(shè)置,并調(diào)用performSelector:withObject:afterDelay:inModes:
方法乱投。最后一個參數(shù),僅設(shè)置一個NSDefaultRunLoopMode
双肤。
UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
當(dāng)然,即使是讀取沙盒或者bundle內(nèi)的圖片蔑赘,我們也可以運(yùn)用這一點來改善視圖的滑動。但是如果UITableView上的圖片都是默認(rèn)圖酥馍,似乎也不是很好,你需要自己來權(quán)衡了峦失。
有一個非常好的關(guān)于設(shè)置圖片視圖的圖片,在RunLoop切換Mode時優(yōu)化的例子:RunLoopWorkDistribution 先看一下界面布局:
一個Cell里有兩個Label,和三個imageView,這里的圖片是非常高清的(2034 ×1525)购啄,一個界面最多有18張圖片狮含。為了表現(xiàn)出卡頓的效果,我先自己實現(xiàn)了一下Cell,主要示例代碼:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = @"cellId";
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:@"spaceship" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = image;
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;
[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;
[cell.contentView addSubview:label2];
[cell.contentView addSubview:imageView3];
?
return cell;
}
然后在滑動的時候,順便打印出當(dāng)前的runloopMode坑填,打印結(jié)果是:
2016-12-08 10:34:31.450 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:31.701 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:32.184 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.317 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.601 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:37.217 TestDemo[3202:1791817] current:UITrackingRunLoopMode
可以看出,為imageView設(shè)置image,是在UITrackingRunLoopMode
中進(jìn)行的,如果圖片很大忌穿,圖片解壓縮和渲染肯定會很耗時,那么卡頓就是必然的朴译。
查看實時幀率眠寿,我們可以在Xcode 中選擇
真機(jī)調(diào)試
,然后 Product –>Profile–>Core Animation
下面就是幀率:
這里就可以使用先使用上面的方式做一次改進(jìn)。
[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
可以保證在滑動起來順暢,可是停下來之后雀彼,渲染還未完成時仍律,繼續(xù)滑動就會變的卡頓。 在切換到NSDefaultRunLoopMode
中窒盐,一個runloop循環(huán)要解壓和渲染18張大圖,耗時肯定超過50ms(1/60s)葡粒。 我們可以繼續(xù)來優(yōu)化,一次runloop循環(huán)夫壁,僅渲染一張大圖片,分18次來渲染邑茄,這樣每一次runloop耗時就比較短了,滑動起來就會非常順暢搓谆。這也是RunLoopWorkDistribution中的做法泉手。 簡單描述一下這種做法: 首先創(chuàng)建一個單例缝裤,單例中定義了幾個數(shù)組,用來存要在runloop循環(huán)中執(zhí)行的任務(wù)榛做,然后為主線程的runloop添加一個CFRunLoopObserver,當(dāng)主線程在NSDefaultRunLoopMode
中執(zhí)行完任務(wù),即將睡眠前锰瘸,執(zhí)行一個單例中保存的一次圖片渲染任務(wù)。關(guān)鍵代碼看DWURunLoopWorkDistribution
類即可管削。
一點UITableView滑動性能優(yōu)化擴(kuò)展
影響UITableView的滑動,有哪些因素呢茸俭? 關(guān)于這一點,人眼能識別的幀率是60左右腾窝,這也就是為什么,電腦屏幕的最佳幀率是60Hz循集。 屏幕一秒鐘會刷新60次(屏幕在一秒鐘會重新渲染60次)疆柔,那么每次刷新界面之間的處理時間,就是1/60,也就是1/60秒鞋屈。也就是說,所有會導(dǎo)致計算、渲染耗時的操作都會影響UITableView的流暢评也。下面舉例說明:
1.在主線程中做耗時操作 耗時操作,包括從網(wǎng)絡(luò)下載罚缕、從網(wǎng)絡(luò)加載、從本地數(shù)據(jù)庫讀取數(shù)據(jù)腌乡、從本地文件中讀取大量數(shù)據(jù)、往本地文件中寫入數(shù)據(jù)等急迂。(這一點,相信大家都知道听盖,要盡量避免在主線程中執(zhí)行仓坞,一般都是創(chuàng)建一個子線程來執(zhí)行,然后再回到主線程)
2.動態(tài)計算UITableViewCell的高度嫉称,時間過久 在iOS7之前,每一個Cell的高度荔棉,只會計算一次,后面再次滑到這個Cell這里壹若,都會讀取緩存的高度,也即高度計算的代理方法不會再執(zhí)行壁查。但是到了iOS8,不會再緩存Cell的高度了,也就是說每次滑到某個Cell席怪,代理方法都會執(zhí)行一次,重新計算這個Cell的高度(iOS9以后沒測試過)刻撒。 所以态贤,如果計算Cell高度的這個過程過于復(fù)雜,或者某個計算使用的算法耗時很長柿冲,可能會導(dǎo)致計算時間大于1/60,那么必然導(dǎo)致界面的卡頓慨亲,或不流暢巴刻。
關(guān)于這一點沥寥,我以前的做法是在Cell中定義一個public方法,用來計算Cell高度淮野,然后計算完高度后,將高度存儲在Cell對應(yīng)的Model中(Model里定義一個屬性來存高度)洞难,然后在渲染Cell時队贱,我們依然需要動態(tài)計算各個子視圖的高度锋恬。(可能是沒用什么太過復(fù)雜的計算或算法,時間都很短滑動也順暢)
其實癣防,更優(yōu)的做法是:再定義一個ModelFrame對象,在子線程請求服務(wù)器接口返回后级遭,轉(zhuǎn)換為對象的同時挫鸽,也把各個子視圖的frame計算好,存在ModelFrame中,ModelFrame和 Model 合并成一個Model存儲到數(shù)組中干茉。這樣在為Cell各個子控件賦值時,僅僅是取值上遥、賦值粉楚,在計算Cell高度時,也僅僅是加法運(yùn)算携狭。
3.界面中背景色透明的視圖過多 為什么界面中背景色透明的視圖過多會影響UITableView的流暢?
很多文章中都提到单默,可以使用模擬器—>Debug—>Color BlendedLayers來檢測透明背景色,把透明背景色改為與父視圖背景色一樣的顏色境蜕,這樣來提高渲染速度粱年。
簡單說明一下舟舒,就是屏幕上顯示的所有東西,都是通過一個個像素點呈現(xiàn)出來的夺鲜。而每一個像素點都是通過三原色(紅、綠、藍(lán))組合呈現(xiàn)出不同的顏色仅胞,最終才是我們看到的手機(jī)屏幕上的內(nèi)容。在iPhone5 的液晶顯示器上有1,136×640=727,040個像素挠将,因此有2,181,120個顏色單元。在15寸視網(wǎng)膜屏的 MacBook Pro上镶蹋,這一數(shù)字達(dá)到15.5百萬以上。所有的圖形堆棧一起工作以確保每次正確的顯示。當(dāng)你滾動整個屏幕的時候婶熬,數(shù)以百萬計的顏色單元必須以每秒60次的速度刷新,這是一個很大的工作量饺谬。
每一個像素點的顏色計算是這樣的: R = S + D * (1 - Sa) 結(jié)果的顏色 是子視圖這個像素點的顏色 + 父視圖這個像素點的顏色 * (1 - 子視圖的透明度) 當(dāng)然,如果有兩個兄弟視圖疊加拔鹰,那么上面的中文解釋可能并不貼切恰画,只是為了更容易理解。
如果兩個兄弟視圖重合,計算的是重合區(qū)域的像素點: 結(jié)果的顏色 是 上面的視圖這個像素點的顏色 + 下面這個視圖該像素點的顏色 * (1 - 上面視圖的透明度)
只有當(dāng)透明度為1時,上面的公式變?yōu)镽 = S孝偎,就簡單的多了爷抓。否則的話蓝撇,就非常復(fù)雜了果复。 每一個像素點是由三原色組成,例如父視圖的顏色和透明度是(Pr,Pg,Pb,Pa)渤昌,子視圖的顏色顏色和透明度是(Sr,Sg,Sb,Sa)虽抄,那么我們計算這個重合區(qū)域某像素點的顏色,需要先分別計算出紅独柑、綠、藍(lán)清女。 Rr = Sr + Pr * (1 - Sa)怜瞒, Rg = Sg + Pg * (1 - Sa)楞卡, Rb = Sb + Pb * (1 - Sa)。 如果父視圖的透明度,即Pa = 1,那么這個像素的顏色就是(Rr,Rg,Rb)蹈丸。 但是,如果父視圖的透明Pa 不等 1女嘲,那么我們需要將這個結(jié)果顏色當(dāng)做一個整體作為子視圖的顏色顶别,再去與父視圖組合計算顏色拴孤,如此遞推化漆。
所以設(shè)置不透明時厌衔,可以為GPU節(jié)省大量的工作,減少大量的消耗赃梧。
更加詳細(xì)的說明巷折,可以看繪制像素到屏幕上這篇文章,這是一篇關(guān)于繪制像素的非常棒
使用TableView時出現(xiàn)的問題:
平時開發(fā)中繪制 tableView
時,我們使用的 cell
可能包含很多業(yè)務(wù)邏輯叶洞,比如加載網(wǎng)絡(luò)圖片仅财、繪制內(nèi)容等等。如果我們不進(jìn)行優(yōu)化的話,在繪制cell
時這些任務(wù)將同時爭奪系統(tǒng)資源,最直接的后果就是頁面出現(xiàn)卡頓,更嚴(yán)重的則會 crash
霜瘪。 我通過在 cell
上加載大的圖片(找的系統(tǒng)的壁紙徙歼,大小10M左右)并改變其大小來模擬 cell
的復(fù)雜業(yè)務(wù)邏輯考润。
cell
的繪制方法中實現(xiàn)如下:
CGFloat width = (self.view.bounds.size.width-4*kBorder_W) /3;
?
UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
[cell addSubview:img1];
UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
[cell addSubview:img2];
UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
[cell addSubview:img3];
tableView
在繪制 cell
的時候同時處理這么多資源纤怒,會導(dǎo)致頁面滑動不流暢等問題。此處只是模擬囊榜,可能效果不明顯,但這都不是重點~
微信對cell
的優(yōu)化方案是當(dāng)監(jiān)聽到列表滾動時,停止 cell
上的動畫等方式,來提升用戶體驗营袜。
Q:那么問題來了,這個監(jiān)聽是怎么做到的呢筒饰?
一種是通過
scrollView
的delegate
方法;另一種就是通過監(jiān)聽
runLoop
;
如果有其他方案兔仰,歡迎告知~
二、下面就分享下通過監(jiān)聽RunLoop來優(yōu)化TableView:
步驟如下:
(1).獲取當(dāng)前主線程的 runloop
。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
(2).創(chuàng)建觀察者 CFRunLoopObserverRef
丈冬, 來監(jiān)聽 runloop
槐脏。
- 創(chuàng)建觀察者用到的核心函數(shù)就是
CFRunLoopObserverCreate
:
// allocator:該參數(shù)為對象內(nèi)存分配器侵贵,一般使用默認(rèn)的分配器kCFAllocatorDefault辽旋。
// activities:要監(jiān)聽runloop的狀態(tài)
/*typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件 };*/
// repeats:是否重復(fù)監(jiān)聽
// order:觀察者優(yōu)先級浩嫌,當(dāng)Run Loop中有多個觀察者監(jiān)聽同一個運(yùn)行狀態(tài)時,根據(jù)該優(yōu)先級判斷补胚,0為最高優(yōu)先級別码耐。
// callout:觀察者的回調(diào)函數(shù),在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包溶其。
// context:觀察者的上下文骚腥。
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
a).創(chuàng)建觀察者
// 1.定義上下文
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
// 2.定義觀察者
static CFRunLoopObserverRef defaultModeObserver;
// 3.創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopBeforeWaiting,
YES,
0,
&callBack,
&context);
// 4.給當(dāng)前runloop添加觀察者
// kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個 Mode 下運(yùn)行的瓶逃。
// UITrackingRunLoopMode: 界面跟蹤 Mode束铭,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響厢绝。
// UIInitializationRunLoopMode: 在剛啟動 App 時進(jìn)入的第一個 Mode契沫,啟動完成后就不再使用。
// GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode昔汉,通常用不到懈万。
// kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用靶病。
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
// 5.內(nèi)存管理
CFRelease(defaultModeObserver);
b).實現(xiàn) callBack 函數(shù)会通,只要檢測到對應(yīng)的runloop狀態(tài),該函數(shù)就會得到響應(yīng)娄周。
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
ViewController *vc = (__bridge ViewController *)info;
//無任務(wù) 退出
if (vc.tasksArr.count == 0) return;
//從數(shù)組中取出任務(wù)
runloopBlock block = [vc.tasksArr firstObject];
//執(zhí)行任務(wù)
if (block) {
block();
}
//執(zhí)行完任務(wù)之后移除任務(wù)
[vc.tasksArr removeObjectAtIndex:0];
}
c).從上面的函數(shù)實現(xiàn)中我們看到了block渴语、arr等對象,下面解析下:
- 使用
Array
來存儲需要執(zhí)行的任務(wù)昆咽;
- (NSMutableArray *)tasksArr {
if (!_tasksArr) {
_tasksArr = [NSMutableArray array];
}
return _tasksArr;
}
-
定義參數(shù)
maxTaskCount
來表示最大任務(wù)數(shù)驾凶,優(yōu)化項目牙甫;//最大任務(wù)數(shù)@property (nonatomic, assign) NSUInteger maxTaskCount;...// 當(dāng)超出最大任務(wù)數(shù)時,以前的老任務(wù)將從數(shù)組中移除self.maxTaskCount = 50;
-
使用
block代碼塊
來包裝一個個將要執(zhí)行的任務(wù)调违,便于callBack
函數(shù)中分開執(zhí)行任務(wù)窟哺,減少同時執(zhí)行對系統(tǒng)資源的消耗。//1. 定義一個任務(wù)blocktypedef void(^runloopBlock)();
//2. 定義一個添加任務(wù)的方法技肩,將任務(wù)裝在數(shù)組中
// 添加任務(wù)
- (void)addTasks:(runloopBlock)task {
// NSLog(@"%d",_maxTaskCount);
// 保存新任務(wù)
[self.tasksArr addObject:task];
// 如果超出最大任務(wù)數(shù) 丟棄之前的任務(wù)
if (self.tasksArr.count > _maxTaskCount) {
[self.tasksArr removeObjectAtIndex:0];
}
}
//3\. 將任務(wù)添加到代碼塊中
// 耗時操作可以放在任務(wù)中
[self addTasks:^{
UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
[cell addSubview:img1];
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}];
[self addTasks:^{
UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
[cell addSubview:img2];
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}];
[self addTasks:^{
UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
kBorder_W,
width,
kCell_H-kBorder_W)];
img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
[cell addSubview:img3];
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}];
(3).使 runloop
不進(jìn)入休眠狀態(tài)且轨。
Q:按照上面步驟實現(xiàn)的情況下:我有500行的cell,為什么才顯示這么一點點呢虚婿?
A:runloop
在加載完 cell
時沒有其他事情做了旋奢,為了節(jié)省資源消耗,就進(jìn)入了休眠狀態(tài)然痊,等待有任務(wù)時再次被喚醒至朗。在我們觀察者的
callBack
函數(shù)中任務(wù)被一個個取出執(zhí)行,還沒有執(zhí)行完剧浸,runloop
就切換狀態(tài)了(休眠了)锹引, callBack
函數(shù)不再響應(yīng)。導(dǎo)致出現(xiàn)上面的情況唆香。
解決方法:
//創(chuàng)建定時器 (保證runloop回調(diào)函數(shù)一直在執(zhí)行)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(notDoSomething)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- (void)notDoSomething {
// 不做事情,就是為了讓 callBack() 函數(shù)一直相應(yīng)
}
參考資料
關(guān)于今天要介紹的使用RunLoop 監(jiān)測主線程卡頓的資料如下:
微信iOS卡頓監(jiān)控系統(tǒng) (這篇文章要首先閱讀嫌变,了解有哪些情況會引起主線程卡頓,監(jiān)測到卡頓后怎么處理等)
監(jiān)控卡頓的方法 (使用RunLoop監(jiān)測卡頓的片段代碼)
簡單監(jiān)測iOS卡頓的demo (使用RunLoop監(jiān)測卡頓的例子)
原理
官方文檔說明了RunLoop的執(zhí)行順序:
1\. Notify observers that the run loop has been entered.
2\. Notify observers that any ready timers are about to fire.
3\. Notify observers that any input sources that are not port based are about to fire.
4\. Fire any non-port-based input sources that are ready to fire.
5\. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
6\. Notify observers that the thread is about to sleep.
7\. Put the thread to sleep until one of the following events occurs:
* An event arrives for a port-based input source.
* A timer fires.
* The timeout value set for the run loop expires.
* The run loop is explicitly woken up.
8\. Notify observers that the thread just woke up.
9\. Process the pending event.
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
* If an input source fired, deliver the event.
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
10\. Notify observers that the run loop has exited.
用偽代碼來實現(xiàn)就是這樣的:
{
/// 1\. 通知Observers躬它,即將進(jìn)入RunLoop
/// 此處有Observer會創(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的) 的事件喚醒了祸挪,處理這個事件
__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是在應(yīng)用啟動時自動開啟的,也沒有超時時間贿条,所以正常情況下雹仿,主線程的RunLoop 只會在 2—9 之間無限循環(huán)下去。 那么整以,我們只需要在主線程的RunLoop中添加一個observer胧辽,檢測從 kCFRunLoopBeforeSources
到kCFRunLoopBeforeWaiting
花費(fèi)的時間是否過長。如果花費(fèi)的時間大于某一個闕值公黑,我們就認(rèn)為有卡頓邑商,并把當(dāng)前的線程堆棧轉(zhuǎn)儲到文件中摄咆,并在以后某個合適的時間,將卡頓信息文件上傳到服務(wù)器人断。
實現(xiàn)步驟
在看了上面的兩個監(jiān)測卡頓的示例Demo后吭从,我按照上面講述的思路寫了一個Demo,應(yīng)該更容易理解吧恶迈。 第一步涩金,創(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)的很惺鞣恪(0.01),而評定為卡頓的闕值也調(diào)的很辛尽(0.01)团赏。實際使用時這兩個值應(yīng)該是比較大,timer間隔為1s耐薯,卡頓闕值為2s即可舔清。
2016-12-15 08:56:39.921 RunLoopDemo03[957:16300] lag happen, detail below:
Incident Identifier: 68BAB24C-3224-46C8-89BF-F9AABA2E3530
CrashReporter Key: TODO
Hardware Model: x86_64
Process: RunLoopDemo03 [957]
Path: /Users/harvey/Library/Developer/CoreSimulator/Devices/6ED39DBB-9F69-4ACB-9CE3-E6EB56BBFECE/data/Containers/Bundle/Application/5A94DEFE-4E2E-4D23-9F69-7B1954B2C960/RunLoopDemo03.app/RunLoopDemo03
Identifier: com.Haley.RunLoopDemo03
Version: 1.0 (1)
Code Type: X86-64
Parent Process: debugserver [958]
?
Date/Time: 2016-12-15 00:56:38 +0000
OS Version: Mac OS X 10.1 (16A323)
Report Version: 104
?
Exception Type: SIGTRAP
Exception Codes: TRAP_TRACE at 0x1063da728
Crashed Thread: 4
?
Thread 0:
0 libsystem_kernel.dylib 0x000000010a14341a mach_msg_trap + 10
1 CoreFoundation 0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212
2 CoreFoundation 0x0000000106f1dc31 __CFRunLoopRun + 1345
3 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420
4 GraphicsServices 0x000000010ad8aa6f GSEventRunModal + 161
5 UIKit 0x00000001073b7964 UIApplicationMain + 159
6 RunLoopDemo03 0x00000001063dbf8f main + 111
7 libdyld.dylib 0x0000000109d7468d start + 1
?
Thread 1:
0 libsystem_kernel.dylib 0x000000010a14be5e kevent_qos + 10
1 libdispatch.dylib 0x0000000109d13074 _dispatch_mgr_invoke + 248
2 libdispatch.dylib 0x0000000109d12e76 _dispatch_mgr_init + 0
?
Thread 2:
0 libsystem_kernel.dylib 0x000000010a14b4e6 __workq_kernreturn + 10
1 libsystem_pthread.dylib 0x000000010a16e221 start_wqthread + 13
?
Thread 3:
0 libsystem_kernel.dylib 0x000000010a14341a mach_msg_trap + 10
1 CoreFoundation 0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212
2 CoreFoundation 0x0000000106f1dc31 __CFRunLoopRun + 1345
3 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420
4 Foundation 0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274
5 Foundation 0x000000010655f991 -[NSRunLoop runUntilDate:] + 78
6 UIKit 0x0000000107e3d539 -[UIEventFetcher threadMain] + 118
7 Foundation 0x00000001064e7ee4 __NSThread__start__ + 1243
8 libsystem_pthread.dylib 0x000000010a16eabb _pthread_body + 180
9 libsystem_pthread.dylib 0x000000010a16ea07 _pthread_body + 0
10 libsystem_pthread.dylib 0x000000010a16e231 thread_start + 13
?
Thread 4 Crashed:
0 RunLoopDemo03 0x00000001063dfae5 -[PLCrashReporter generateLiveReportWithThread:error:] + 632
1 RunLoopDemo03 0x00000001063da728 -[FluencyMonitor handleStackInfo] + 152
2 RunLoopDemo03 0x00000001063da2cf runLoopTimerCallBack + 351
3 CoreFoundation 0x0000000106f26964 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
4 CoreFoundation 0x0000000106f265f3 __CFRunLoopDoTimer + 1075
5 CoreFoundation 0x0000000106f2617a __CFRunLoopDoTimers + 250
6 CoreFoundation 0x0000000106f1df01 __CFRunLoopRun + 2065
7 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420
8 Foundation 0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274
9 Foundation 0x00000001064d7ecb -[NSRunLoop run] + 76
10 RunLoopDemo03 0x00000001063d9cbd +[FluencyMonitor monitorThreadEntryPoint] + 253
11 Foundation 0x00000001064e7ee4 __NSThread__start__ + 1243
12 libsystem_pthread.dylib 0x000000010a16eabb _pthread_body + 180
13 libsystem_pthread.dylib 0x000000010a16ea07 _pthread_body + 0
14 libsystem_pthread.dylib 0x000000010a16e231 thread_start + 13
?
Thread 4 crashed with X86-64 Thread State:
rip: 0x00000001063dfae5 rbp: 0x000070000f53fc50 rsp: 0x000070000f53f9c0 rax: 0x000070000f53fa20
rbx: 0x000070000f53fb60 rcx: 0x0000000000005e0b rdx: 0x0000000000000000 rdi: 0x00000001063dfc6a
rsi: 0x000070000f53f9f0 r8: 0x0000000000000014 r9: 0xffffffffffffffec r10: 0x000000010a1433f6
r11: 0x0000000000000246 r12: 0x000060800016b580 r13: 0x0000000000000000 r14: 0x0000000000000006
r15: 0x000070000f53fa40 rflags: 0x0000000000000206 cs: 0x000000000000002b fs: 0x0000000000000000
gs: 0x0000000000000000
剩下的工作就是將字符串保存進(jìn)文件,以及上傳到服務(wù)器了曲初。
我們不能將卡頓的闕值定的太小体谒,也不能將所有的卡頓信息都上傳,原因有兩點臼婆,一抒痒,太浪費(fèi)用戶流量;二颁褂、文件太多故响,App內(nèi)存儲和上傳后服務(wù)器端保存都會占用空間。
可以參考微信的做法颁独,7天以上的文件刪除彩届,隨機(jī)抽取上傳,并且上傳前對文件進(jìn)行壓縮處理等誓酒。
Crash 收集資料
漫談iOS Crash收集框架(簡單介紹了下iOS 中Crash 的一些知識樟蠕。)
IOS程序異常crash捕獲與攔截 (我下面的Demo 就是在這部分代碼上做了簡化,以方便理解)
原理
iOS應(yīng)用崩潰靠柑,常見的崩潰信息有EXC_BAD_ACCESS
寨辩、SIGABRTXXXXXXX
,而這里分為兩種情況,一種是未被捕獲的異常歼冰,我們只需要添加一個回調(diào)函數(shù)靡狞,并在應(yīng)用啟動時調(diào)用一個 API即可;另一種是直接發(fā)送的SIGABRT XXXXXXX
,這里我們也需要監(jiān)聽各種信號隔嫡,然后添加回調(diào)函數(shù)甸怕。
針對情況一,其實我們都見過畔勤。我們在收集App崩潰信息時蕾各,需要添加一個函數(shù)NSSetUncaughtExceptionHandler(&HandleException)
扒磁,參數(shù)是一個回調(diào)函數(shù)庆揪,在回調(diào)函數(shù)里獲取到異常的原因,當(dāng)前的堆棧信息等保存到 dump文件妨托,然后供下次打開App時上傳到服務(wù)器缸榛。
其實吝羞,我們在HandleException回調(diào)函數(shù)中,可以獲取到當(dāng)前的RunLoop内颗,然后獲取該RunLoop中的所有Mode钧排,手動運(yùn)行一遍。
針對情況二均澳,首先針對多種要捕獲的信號恨溜,設(shè)置好回調(diào)函數(shù),然后也是在回調(diào)函數(shù)中獲取RunLoop找前,然后拿到所有的Mode糟袁,手動運(yùn)行一遍。
代碼實現(xiàn)
第一步躺盛,我創(chuàng)建了一個處理類项戴,并添加一個單例方法。(代碼見末尾的Demo)
第二步槽惫,在單例中對象實例化時周叮,添加 異常捕獲 和 signal 處理的 回調(diào)函數(shù)。
- (void)setCatchExceptionHandler
{
// 1.捕獲一些異常導(dǎo)致的崩潰
NSSetUncaughtExceptionHandler(&HandleException);
// 2.捕獲非異常情況界斜,通過signal傳遞出來的崩潰
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
第三步仿耽,分別實現(xiàn) 異常捕獲的回調(diào) 和 signal 的回調(diào)。
void HandleException(NSException *exception)
{
// 獲取異常的堆棧信息
NSArray *callStack = [exception callStackSymbols];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
CrashHandler *crashObject = [CrashHandler sharedInstance];
NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
?
void SignalHandler(int signal)
{
// 這種情況的崩潰信息锄蹂,就另某他法來捕獲吧
NSArray *callStack = [CrashHandler backtrace];
NSLog(@"信號捕獲崩潰氓仲,堆棧信息:%@",callStack);
CrashHandler *crashObject = [CrashHandler sharedInstance];
NSException *customException = [NSException exceptionWithName:kSignalExceptionName
reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
第四步,添加讓應(yīng)用起死回生的 RunLoop 代碼
- (void)handleException:(NSException *)exception
{
NSString *message = [NSString stringWithFormat:@"崩潰原因如下:\n%@\n%@",
[exception reason],
[[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
NSLog(@"%@",message);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"程序崩潰了"
message:@"如果你能讓程序起死回生得糜,那你的決定是敬扛?"
delegate:self
cancelButtonTitle:@"崩就蹦吧"
otherButtonTitles:@"起死回生", nil];
[alert show];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!ignore) {
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:kSignalExceptionName]) {
kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
} else {
[exception raise];
}
}
因為我這里弄了一個AlertView彈窗,所以必須要回到主線程來處理朝抖。 實際上啥箭,RunLoop 相關(guān)的代碼:
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!ignore) {
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
完全可以寫在 上面的 HandleException 回調(diào) 和 SignalHandler回調(diào)中。
第五步治宣,寫一段會導(dǎo)致崩潰的代碼
我是在ViewController 中添加了一個點擊事件急侥,弄了一個數(shù)組越界的Bug:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSArray *array =[NSArray array];
NSLog(@"%@",[array objectAtIndex:1]);
}
動態(tài)效果圖:
遇到數(shù)組越界,應(yīng)用依然沒崩潰
sunnyxx 稱之為回光返照侮邀,為什么呢坏怪? 我再一次點擊視圖,應(yīng)用依然還是崩潰了绊茧,只能防止第一次崩潰铝宵。 我測試了,確實是第二次應(yīng)用崩潰,未能起死回生鹏秋。