理解 OC 中 RunLoop

什么是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)系如下:


OC RunLoop關(guān)系圖.png

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)部邏輯

OC RunLoop 內(nèi)部邏輯圖.PNG

這里引入了新概念:source0是觸摸事件和所有執(zhí)行performSelector方法痘括,source1是基于port的線程間的通信长窄。

這里我們可以大概看出RunLoop中處理事件的順序,可以簡要的總結(jié)為:

  1. 先通知Timer纲菌,Sources要處理事件了
  2. 處理source0
  3. 看看有沒有source1挠日,沒有就休眠,有就不休眠
  4. 休眠狀態(tài)下sources翰舌,timer嚣潜,dispatch,手動都可以喚醒
  5. 3結(jié)束或者4喚醒后椅贱,就開始處理各種其他事件(timer懂算,source1只冻,dispatch)
  6. 如果第五步處理了至少一個事件,則開始新一輪的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í)行時間過長叁巨。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市呐籽,隨后出現(xiàn)的幾起案子锋勺,更是在濱河造成了極大的恐慌,老刑警劉巖狡蝶,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庶橱,死亡現(xiàn)場離奇詭異,居然都是意外死亡贪惹,警方通過查閱死者的電腦和手機悬包,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馍乙,“玉大人,你說我怎么就攤上這事垫释∷扛瘢” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵棵譬,是天一觀的道長显蝌。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么曼尊? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任酬诀,我火速辦了婚禮,結(jié)果婚禮上骆撇,老公的妹妹穿的比我還像新娘瞒御。我一直安慰自己,他們只是感情好神郊,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布肴裙。 她就那樣靜靜地躺著,像睡著了一般涌乳。 火紅的嫁衣襯著肌膚如雪蜻懦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天夕晓,我揣著相機與錄音宛乃,去河邊找鬼。 笑死蒸辆,一個胖子當(dāng)著我的面吹牛征炼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吁朦,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼柒室,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了逗宜?” 一聲冷哼從身側(cè)響起雄右,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纺讲,沒想到半個月后擂仍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡熬甚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年逢渔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乡括。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡肃廓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诲泌,到底是詐尸還是另有隱情盲赊,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布敷扫,位于F島的核電站哀蘑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绘迁,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一合溺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缀台,春花似錦棠赛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至依疼,卻和暖如春痰腮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背律罢。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工膀值, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人误辑。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓沧踏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親巾钉。 傳聞我的和親對象是個殘疾皇子翘狱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359