什么是RunLoop英古?
可以簡單理解為剧防,讓程序保持運行的一個while
循環(huán)贸人,這個循環(huán)內(nèi)監(jiān)聽各種事件(如觸摸事件撮弧、performSelector
瓦堵、定時器NSTimer
等)逼纸,沒有事件的時候睡眠借帘,從而有效的利用CPU(只有在有事件的時候才用CPU蜘渣,沒事件的時候睡眠)
不管RunLoop有多復(fù)雜,其本質(zhì)就是上面所說的:一個循環(huán)肺然,有事件的時候處理事件蔫缸,無事件的時候休眠(這里的睡眠是指用戶態(tài)切換到內(nèi)核態(tài),這樣的休眠線程是被掛起的际起,不會再占用cpu資源)拾碌。
RumLoop與線程有如下關(guān)系:
- 一個線程只有一個RunLoop對象
- 主線程的RunLoop默認(rèn)已經(jīng)創(chuàng)建好了吐葱,而子線程的需要手動創(chuàng)建。
- RunLoop在第一次獲取時創(chuàng)建校翔,在線程結(jié)束時銷毀唇撬。
我們驗證一下,在main
函數(shù)返回之前展融,打印一下:
int main(int argc, char *argv[])
{
NSString *appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
int ret = UIApplicationMain(argc, argv, nil, appDelegateClassName);
NSLog(@"after ret");
return ret;
}
結(jié)果沒有打印窖认,這說明主進程已經(jīng)進入了一個RunLoop主了,主進程不結(jié)束告希,就跳不出RunLoop扑浸,也就執(zhí)行不了之后的打印。
我們打印一下主線程的RunLoop試試:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", [NSRunLoop currentRunLoop]);
}
// 打印結(jié)果(只取關(guān)鍵信息):
// CFRunLoop 0x600001704700
// current mode = kCFRunLoopDefaultMode,
這說明主線程在一個RunLoop中燕偶,并且當(dāng)前的運行模式是kCFRunLoopDefaultMode
這樣感覺RunLoop很簡單喝噪,但它又很復(fù)雜,因為要考慮的因素有很多指么,比如各種事件的處理順序酝惧,定時器、多線程等等
對于一個復(fù)雜問題伯诬,解決方法之一就是抽象晚唇,蘋果為解決上面的問題,抽象出了RunLoop對象盗似,RunLoop中包含多個Mode類哩陕,每個mode類中包含若干個 Source,Observer和Timer類赫舒,關(guān)系如下:
Mode是RunLoop的運行模式悍及,有五類:
kCFRunLoopDefaultMode //App的默認(rèn)Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode //界面跟蹤 Mode接癌,用于 ScrollView 追蹤觸摸滑動心赶,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode // 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode // 接受系統(tǒng)事件的內(nèi)部 Mode缺猛,通常用不到
kCFRunLoopCommonModes // 這是一個占位用的Mode缨叫,不是一種真正的Mode,可以簡單理解為kCFRunLoopDefaultMode和UITrackingRunLoopMode的結(jié)合
這里的Source是事件源枯夜,比如觸摸事件弯汰。
Observer是觀察者,監(jiān)聽事件源的事件湖雹,可以簡單理解為線程咏闪,比如主線程RunLoop的的Observer是主線程。
還有一些規(guī)定:
- RunLoop雖然有多個Mode摔吏,但RunLoop函數(shù)執(zhí)行的時候鸽嫂,只能指定一個Mode
- 如果要切換Mode纵装,需要等到一個Loop循環(huán)結(jié)束,再讓新的Mode進入
上面說一個RunLoop只有一個Mode在執(zhí)行据某,下面做個試驗看看:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextView *textView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
@end
這里我們在ViewControlller
里面創(chuàng)建了一個timer
橡娄,把他加到NSDefaultRunLoopMode
中,這個ViewControlller
有個可以滾動的UITextView
(繼承UIScrollView
癣籽,UIScrollView
默認(rèn)的Mode是UITrackingRunLoopMode
)
當(dāng)我們滑動UITextView
的時候挽唉,timer
停止觸發(fā)事件了,說明RunLoop的Mode從Default切換到了UITrackingRunLoopMode
解決方法就是把timer
放入kCFRunLoopCommonModes
中筷狼,這個Mode相當(dāng)于同時是kCFRunLoopDefaultMode
和UITrackingRunLoopMode:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
上面是一個經(jīng)典的例子瓶籽,可以解決在UIScrollView
(包括其子類)中有NSTimer
定時的場景。
受此啟發(fā)埂材,我們可以用RunLoop解決卡頓問題塑顺,有一種卡頓問題就是UITableView
中有很多高清大圖需要載入,在滑動屏幕的時候卡頓俏险。
我們先分析一下卡頓的原因:最根本的原因是RunLoop轉(zhuǎn)一圈的時間太長了严拒,因為一次RunLoop循環(huán)需要解析很多張高清大圖,系統(tǒng)渲染每一張高清大圖都需要一定的時間竖独,這樣需要等到渲染的RunLoop結(jié)束之后裤唠,才能切換滑動屏幕RunLoop的Mode(UITrackingRunLoopMode),解決方法就是:
- 創(chuàng)建一個定時器:每間隔一定時間(可以是0.01s)執(zhí)行一個空方法來喚醒RunLoop
- 將加載圖片的方法裝入block预鬓,將block加入一個有數(shù)量限制的數(shù)組巧骚,當(dāng)block超過最大數(shù)量限制,移除最早添加的block
- 監(jiān)聽RunLoop的蘇醒格二,蘇醒回掉就執(zhí)行一次就從數(shù)組中取出一個block事件執(zhí)行,執(zhí)行完的事件從數(shù)組中刪除
這樣設(shè)計讓RunLoop的每次循環(huán)只執(zhí)行一個加載圖片的block(減少RunLoop單次循環(huán)的時間)竣蹦。給數(shù)組設(shè)置一個最大數(shù)量限制顶猜,可以防止同一時間需要渲染的圖片過多(減少RunLoop渲染圖片的總時間)。
下面我們可以看看RunLoop里面長什么樣了:
RunLoop內(nèi)部邏輯
這里引入了新概念:source0是觸摸事件和所有執(zhí)行
performSelector
方法痘括,source1是基于port的線程間的通信长窄。
這里我們可以大概看出RunLoop中處理事件的順序,可以簡要的總結(jié)為:
- 先通知Timer纲菌,Sources要處理事件了
- 處理source0
- 看看有沒有source1挠日,沒有就休眠,有就不休眠
- 休眠狀態(tài)下sources翰舌,timer嚣潜,dispatch,手動都可以喚醒
- 3結(jié)束或者4喚醒后椅贱,就開始處理各種其他事件(timer懂算,source1只冻,dispatch)
- 如果第五步處理了至少一個事件,則開始新一輪的RunLoop计技,否則退出RunLoop
以上邏輯可以推出喜德,在RunLoop中,只要有任何一個事件垮媒,RunLoop就不會退出舍悯,除非是RunLoop在休眠超時被喚醒或者外部強制停止,才會退出睡雇。
下面用一個例子感受一下RunLoop里的邏輯:
- (void)viewDidLoad {
[super viewDidLoad];
[self createObserver];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
}
- (void)timerFired
{
NSLog(@"---- timer fired ----");
}
- (void)createObserver
{
//創(chuàng)建監(jiān)聽者
/*
第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認(rèn)分配
第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
第四個參數(shù) CFIndex order:優(yōu)先級萌衬,一般填0即可
第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); // 添加監(jiān)聽者,關(guān)鍵入桂!
CFRelease(observer); // 釋放
}
這里給RunLoop創(chuàng)建了一個觀察者奄薇,觀察者的回調(diào)打印RunLoop里的邏輯,另外有一個Timer每隔1.0秒觸發(fā)一下抗愁。結(jié)果如下:
// 23:26:30 RunLoop醒來了
// 23:26:30 ---- timer fired ----
// 23:26:30 RunLoop要處理Timers了
// 23:26:30 RunLoop要處理Sources了
// 23:26:30 RunLoop要休息了
// 23:26:31 RunLoop醒來了
// 23:26:31 ---- timer fired ----
// 23:26:31 RunLoop要處理Timers了
// 23:26:31 RunLoop要處理Sources了
// 23:26:31 RunLoop要休息了
可以看到馁蒂,Timer要觸發(fā)的時候,喚醒了RunLoop蜘腌,RunLoop醒來后去處理Timer沫屡,執(zhí)行了Timer的方法(打印---- timer fired ----
),然后RunLoop回到循環(huán)的開頭撮珠,通知觀察者要處理Timers和Sources了沮脖,結(jié)果發(fā)現(xiàn)沒有要處理的,然后就去休息了芯急,如此循環(huán)勺届。。娶耍∶庾耍基本和上面的邏輯一致。
這里介紹一個RunLoop的應(yīng)用:
創(chuàng)建一個常駐線程
首先我們創(chuàng)建一個繼承自NSThread
的類BZThread
榕酒,用來打印銷毀時候的信息胚膊,然后在viewDidLoad
中創(chuàng)建一個線程:
@interface BZThread : NSThread
@end
@implementation BZThread
- (void)dealloc {
NSLog(@"BZThread is dealloced");
}
@end
@interface ViewController ()
@property NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[BZThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
self.thread = thread;
[self.thread start];
}
- (void)threadTest {
NSLog(@"thread is created");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(doSomethingInThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)doSomethingInThread {
NSLog(@"doSomethingInThread is fired");
}
@end
// BZThread is created
我們發(fā)現(xiàn),線程是被創(chuàng)建了想鹰,也被ViewControlelr
持有了(沒有馬上被銷毀)紊婉,但是我們在這個線程里執(zhí)行方法沒有反應(yīng),這說明這個線程的RunLoop沒有運行起來辑舷。
解決方法是在這個線程方法里喻犁,給這個線程的RunLoop創(chuàng)建一個Mode:
- (void)threadTest {
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"thread is created");
}
點擊屏幕,我們就執(zhí)行了線程的方法了:
// doSomethingInThread is fired
這是因為,雖然一個線程對應(yīng)一個RunLoop株汉,但一個RunLoop至少需要一個Mode筐乳,才能跑起來,主線程默認(rèn)就有Mode了乔妈,而新的線程需要我們手動去創(chuàng)建新的Mode蝙云。
最后介紹一個RunLoop的應(yīng)用:
檢測卡頓:
如果 RunLoop 的線程,進入睡眠前方法的執(zhí)行時間過長而導(dǎo)致無法進入睡眠路召,或者線程喚醒后接收消息時間過長而無法進入下一步的話勃刨,就可以認(rèn)為是線程受阻了。如果這個線程是主線程的話股淡,表現(xiàn)出來的就是出現(xiàn)了卡頓身隐。
如何檢查卡頓呢?需要創(chuàng)建一個持續(xù)的子線程專門用來監(jiān)控主線程的 RunLoop 狀態(tài)唯灵。一旦發(fā)現(xiàn)進入睡眠前的 狀態(tài)贾铝,或者喚醒后的狀態(tài),在設(shè)置的時間閾值內(nèi)一直沒有變化埠帕,即可判定為卡頓垢揩。接下來,我們就可以 dump 出堆棧的信息敛瓷,從而進一步分析出具體是哪個方法的執(zhí)行時間過長叁巨。