OC-Run Loop的理解和使用

  • Run Loop是什么

RunLoop顧名思義,是運行循環(huán)。它跟線程是一一對應(yīng)的,每一個線程都有一個RunLoop,在需要的時候創(chuàng)建。RunLoop的作用很簡單,就是保持線程不會退出,并且處理一些事件。

如果沒有RunLoop,線程只要一執(zhí)行完代碼就會退出。RunLoop類似一個while循環(huán),但是又不像while循環(huán)會占用CPU資源,RunLoop在等待的時候處于休眠狀態(tài),只有接收到事件時,才會被喚醒,然后再做相應(yīng)的處理。

程序啟動時,系統(tǒng)會自動為我們開啟主線程的RunLoop,這就保證了我們的程序不會退出,并且可以一直響應(yīng)我們的操作。而子線程的RunLoop并沒有開啟,需要我們手動開啟。

  • Run Loop使用

說到使用RunLoop,其實我們在使用NSTimer的時候就已經(jīng)使用過它了,只不過那時候并沒有對RunLoop深入研究,我們來重新體驗一下一個NSTimer的簡單使用:

    //創(chuàng)建一個Timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer");
    }];
    
    //把它加到RunLoop里
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

一個NSTimer必須和RunLoop一起工作,不然它沒辦法運作。這里再給RunLoop添加timer的時候,有一個參數(shù)叫Mode,這是RunLoop模式,不同模式處理不同類型輸入源的事件。

  1. NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行。
  2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
  3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
  5. NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode,它會同時處理默認模式和UI模式中的事件。

到這里又引發(fā)出來了新的問題,為什么NSTimer必須添加到RunLoop才能使用呢?

這就涉及到了RunLoop所能處理的事件了:

Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。輸入源傳遞異步事件,通常消息來自于其他線程或程序。定時源則傳遞同步事件,發(fā)生在特定時間或者重復(fù)的時間間隔。兩種源都使用程序的某一特定的處理例程來處理到達的事件。

這張官方的圖簡單的描述了RunLoop所能處理的事件來源:


接下來,我們來玩一玩RunLoop。在OC中,有NSRunLoopCFRunLoop兩種方式來獲取并且操作RunLoop。其中NSRunLoopCFRunLoop的封裝。我們以NSRunLoop為主對RunLoop進行使用。首先,我們創(chuàng)建一個線程,然后開啟它的runloop,我們?nèi)绾巫C明它的runloop已經(jīng)開啟呢?結(jié)合上圖,我們只需要找一個runloop能夠處理的事件,然后讓它去處理就可以了,我這里挑選了- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait ;方法:

//首先持有一個線程對象,方便我們之后使用它
@property (nonatomic, strong) NSThread *thread;

- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化并開啟,在線程內(nèi)部開啟它的runloop
    self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"這是一條子線程%@",[NSThread currentThread]);
        
        [[NSRunLoop currentRunLoop] run];
    }];
    [self.thread start];
}

//點擊屏幕時,在線程上執(zhí)行下面的打印
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

//打印出線程,以便我們確認是同一條
- (void)test{
    NSLog(@"哈哈哈%@",[NSThread currentThread]);
}

我們可以通過[NSRunLoop currentRunLoop]獲取當前線程的RunLoop,開啟RunLoop只需要調(diào)用其run方法。

按照我們預(yù)想的結(jié)果,運行以后每次點擊屏幕都應(yīng)該有輸出,但是實際上我們點擊屏幕并沒有任何效果。這是因為開啟RunLoop之前必須給其指定至少一種輸入源或者定時源,不然開啟之后會馬上退出。說到這里,我們得看一下RunLoop在一次循環(huán)的周期內(nèi),到底做了什么事情:

每次運行run loop,你線程的run loop對會自動處理之前未處理的消息,并通知相關(guān)的觀察者。具體的順序如下:

  1. 通知觀察者run loop已經(jīng)啟動
  2. 通知觀察者任何即將要開始的定時器
  3. 通知觀察者任何即將啟動的非基于端口的源
  4. 啟動任何準備好的非基于端口的源
  5. 如果基于端口的源準備好并處于等待狀態(tài),立即啟動;并進入步驟9。
  6. 通知觀察者線程進入休眠
  7. 將線程置于休眠直到任一下面的事件發(fā)生:
    • 某一事件到達基于端口的源
    • 定時器啟動
    • Run loop設(shè)置的時間已經(jīng)超時
    • run loop被顯式喚醒
  8. 通知觀察者線程將被喚醒。
  9. 處理未處理的事件
    • 如果用戶定義的定時器啟動,處理定時器事件并重啟run loop。進入步驟2
    • 如果輸入源啟動,傳遞相應(yīng)的消息
    • 如果run loop被顯式喚醒而且時間還沒超時,重啟run loop。進入步驟2
  10. 通知觀察者run loop結(jié)束。

從以上我們可以知道,如果是定時器事件,執(zhí)行之后會直接重啟RunLoop,如果是其它事件,處理完畢后,不會再次喚醒RunLoop,要想它繼續(xù)監(jiān)聽事件,我們必須得手動喚醒它。之前在我們點擊的時候,runloop已經(jīng)退出了,所以代碼并沒有執(zhí)行。

不過這難不倒我們,我們可以給它一個循環(huán),讓它不斷得開啟:

    while (true) {
        [[NSRunLoop currentRunLoop] run];
    }

再次運行,點擊屏幕就可以看到打印出的信息:

開啟RunLoop還有另外幾個方法,我們平時最好不要直接使用run方法,可能會造成無限循環(huán):

//同run方法,增加超時參數(shù)limitDate,避免進入無限循環(huán)。使用在UI線程(亦即主線程)上,可以達到暫停的效果。
(void)runUntilDate:(NSDate *)limitDate; 

//等待消息處理,好比在PC終端窗口上等待鍵盤輸入。一旦有合適事件(mode相當于定義了事件的類型)被處理了,則立刻返回;類同run方法,如果沒有事件處理也立刻返回;有否事件處理由返回布爾值判斷。同樣limitDate為超時參數(shù)。
(BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate;

以上是一些簡單的操作,我們可以利用RunLoop去監(jiān)測一些事件,當它發(fā)生的時候再去做處理。但是用while循環(huán)會讓CPU一直在工作,所以我們最好設(shè)置一種終止RunLoop循環(huán)的條件。

前面提到,RunLoop在開啟時,需要給它指定輸入源,而輸入源是可以自定義的,不過它需要使用CFRunLoop。接下來我們可以自己自定義一種輸入源:

/* Run Loop Source Context的三個回調(diào)方法,其實是C語言函數(shù) */

// 當把當前的Run Loop Source添加到Run Loop中時,會回調(diào)這個方法。
void runLoopSourceScheduleRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
    NSLog(@"Input source被添加%@",[NSThread currentThread]);

}

// 當前Input source被告知需要處理事件的回調(diào)方法
void runLoopSourcePerformRoutine (void *info)
{
    NSLog(@"回調(diào)方法%@",[NSThread currentThread]);
}

// 如果使用CFRunLoopSourceInvalidate函數(shù)把輸入源從Run Loop里面移除的話,系統(tǒng)會回調(diào)該方法。
void runLoopSourceCancelRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
    NSLog(@"Input source被移除%@",[NSThread currentThread]);
}

//創(chuàng)建兩個屬性來保存`runLoopSource `和`runLoop `
@implementation ViewController{
    CFRunLoopSourceRef runLoopSource;
    CFRunLoopRef runLoop;
}

        //在之前的線程代碼中為RunLoop添加Source
        self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"這是一條子線程%@",[NSThread currentThread]);
        
        runLoop = CFRunLoopGetCurrent();
        
        CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
            &runLoopSourceScheduleRoutine,
            &runLoopSourceCancelRoutine,
            &runLoopSourcePerformRoutine};
        
        //CFAllocatorRef內(nèi)存分配器,默認NULL,CFIndex優(yōu)先索引,默認0,CFRunLoopSourceContext上下文
        runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
        CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
        
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

    }];
    [self.thread start];

然后我們需要在點擊的時候通知InputSource,并且喚醒runLoop

    //通知InputSource
    CFRunLoopSourceSignal(InputSource);
    //喚醒runLoop
    CFRunLoopWakeUp(runLoop);

然后點擊測試一下:

因為我們設(shè)置了超時時間,所以10秒以后,RunLoop就會退出。同時它的InputSource被自動移除。

以上,我們簡單的自己創(chuàng)建并添加了RunLoop的InputSource,實際開發(fā)中,我們可以對InputSource進行封裝,使用起來更方便。這里就不做這一步了,網(wǎng)上可以找到比較完善的例子。

RunLoop還有一個觀察者,可以讓我們監(jiān)聽到RunLoop的各種狀態(tài),它也需要用CFRunLoop來實現(xiàn),接下來,我們在上面的基礎(chǔ)上,對RunLoop添加觀察者進行監(jiān)聽:

// RunLoop監(jiān)聽回調(diào)
void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    NSString *activityDescription;
    switch (activity) {
        case kCFRunLoopEntry:
            activityDescription = @"kCFRunLoopEntry";
            break;
        case kCFRunLoopBeforeTimers:
            activityDescription = @"kCFRunLoopBeforeTimers";
            break;
        case kCFRunLoopBeforeSources:
            activityDescription = @"kCFRunLoopBeforeSources";
            break;
        case kCFRunLoopBeforeWaiting:
            activityDescription = @"kCFRunLoopBeforeWaiting";
            break;
        case kCFRunLoopAfterWaiting:
            activityDescription = @"kCFRunLoopAfterWaiting";
            break;
        case kCFRunLoopExit:
            activityDescription = @"kCFRunLoopExit";
            break;
        default:
            break;
    }
    NSLog(@"Run Loop activity: %@", activityDescription);
}

        //為runLoop添加觀察者
        CFRunLoopObserverContext  runLoopObserverContext = {0, NULL, NULL, NULL, NULL};
        CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(NULL,//內(nèi)存分配器,默認NULL
                                                                   kCFRunLoopAllActivities,//監(jiān)聽所有狀態(tài)
                                                                   YES,//是否循環(huán)
                                                                   0,//優(yōu)先索引,一般為0
                                                                   &currentRunLoopObserver,//回調(diào)方法
                                                                   &runLoopObserverContext//上下文
                                                                   );
        if (observer)
        {
            CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
        }

運行以后:

上面,我們對Runloop的使用做了簡單的分析,但是對我們好像還是沒什么卵用。接下來,我們通過一個實際的案例來運用RunLoop,讓它變成我們的法寶。

  • Run Loop的實際應(yīng)用

我們在實際開發(fā)中經(jīng)常會遇到TableView中有大量的圖片顯示,在滑動過程中,能明顯得感覺到卡頓。我們這里用TableView顯示多張大圖簡單模擬一下:

    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
    
    [self.view addSubview:self.tableView];

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 299;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 100;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSString *identifier = @"identifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
    }

    for (UIView *view in cell.subviews) {
        [view removeFromSuperview];
    }
    
    UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView1.frame = CGRectMake(10, 10, 100, 80);
        [cell addSubview:imageView1];
    
    UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView2.frame = CGRectMake(120, 10, 100, 80);
        [cell addSubview:imageView2];
    
    UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView3.frame = CGRectMake(230, 10, 100, 80);
        [cell addSubview:imageView3];
    
    return cell;
}

那么怎么做優(yōu)化呢?利用我們前面了解的RunLoop可以實現(xiàn)這個優(yōu)化。我們知道卡頓的主要原因是因為加載大量大圖是比較耗時的,而在主線程上處理耗時操作時,我們滑動或者點擊屏幕就會有卡頓的感覺,因為在同一條線程上的任務(wù)只能串行執(zhí)行。而我們滑動屏幕時,一瞬間要顯示很多張圖片,這就形成了一個耗時操作。

經(jīng)過思考,我們可以把這些圖片在每一次runLoop循環(huán)中添加一張,這樣的話,因為每次只添加一張圖片,時間大大縮短,就不會有卡頓的感覺了。

我們這里利用runLoop的觀察者來監(jiān)聽每一次runloop循環(huán),然后在監(jiān)聽事件里,添加一張圖片。我們這里把添加圖片當做任務(wù)放到一個數(shù)組里面,任務(wù)就是一個block,這樣我們在回調(diào)里面只需要拿出任務(wù)執(zhí)行就OK了:

//定義一個任務(wù)
typedef void(^RunLoopTask)(void);
//用來存放任務(wù)的數(shù)組
@property (nonatomic, strong) NSMutableArray<RunLoopTask> *tasks;
//最大任務(wù)數(shù)量
@property (nonatomic, assign) NSInteger maxTaskCount;

    //初始化數(shù)據(jù)
    self.maxTaskCount = 24;
    self.tasks = [NSMutableArray array];

//添加任務(wù)到數(shù)組
- (void)addTask:(RunLoopTask)task{
    
    [self.tasks addObject:task];
    
    //保證之前沒來得及顯示的圖片不會再繪制
    if (self.tasks.count > _maxTaskCount) {
        [self.tasks removeObjectAtIndex:0];
    }
}


//添加任務(wù)
    [self addTask:^{
        UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView1.frame = CGRectMake(10, 10, 100, 80);
        [cell addSubview:imageView1];
    }];
    
    [self addTask:^{
        UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView2.frame = CGRectMake(120, 10, 100, 80);
        [cell addSubview:imageView2];
    }];
    

    [self addTask:^{
        UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
        imageView3.frame = CGRectMake(230, 10, 100, 80);
        [cell addSubview:imageView3];
    }];

- (void)addObserverToMainRunLoop{
    //為runLoop添加觀察者
    CFRunLoopObserverContext  runLoopObserverContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(NULL,//內(nèi)存分配器,默認NULL
                                                               kCFRunLoopBeforeWaiting,//等待之前
                                                               YES,//是否循環(huán)
                                                               0,//優(yōu)先索引,一般為0
                                                               &currentRunLoopObserver,//回調(diào)方法
                                                               &runLoopObserverContext//上下文
                                                               );
    if (observer)
    {
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    }
    CFRelease(observer);
}

// RunLoop監(jiān)聽回調(diào)
static void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyViewController *vc = (__bridge MyViewController *)info;
    if (vc.tasks.count == 0) {
        return;
    }
    RunLoopTask task = vc.tasks.firstObject;
    task();
    [vc.tasks removeObjectAtIndex:0];
}

然后再運行,就很流暢了。當然代碼并不是完整代碼,篇幅有限,只能把主要代碼貼上來。
總結(jié)一下,我們可以把耗時的大量UI操作利用RunLoop分解,使界面保持流暢。

本文參考iOS多線程編程指南(三)Run Loop。
本文所涉及到所有的代碼點擊前往。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

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