OC-Run Loop的理解和使用

  • Run Loop是什么

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

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

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

  • Run Loop使用

說到使用RunLoop丧失,其實我們在使用NSTimer的時候就已經使用過它了,只不過那時候并沒有對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的時候,有一個參數叫Mode描验,這是RunLoop模式白嘁,不同模式處理不同類型輸入源的事件。

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

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

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

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

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


接下來馆匿,我們來玩一玩RunLoop。在OC中燥滑,有NSRunLoopCFRunLoop兩種方式來獲取并且操作RunLoop渐北。其中NSRunLoopCFRunLoop的封裝。我們以NSRunLoop為主對RunLoop進行使用铭拧。首先赃蛛,我們創(chuàng)建一個線程,然后開啟它的runloop搀菩,我們如何證明它的runloop已經開啟呢呕臂?結合上圖,我們只需要找一個runloop能夠處理的事件肪跋,然后讓它去處理就可以了歧蒋,我這里挑選了- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait ;方法:

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

- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化并開啟,在線程內部開啟它的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只需要調用其run方法阐虚。

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

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

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

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

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

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

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

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

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

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

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

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

/* Run Loop Source Context的三個回調方法厌衙,其實是C語言函數 */

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

}

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

// 如果使用CFRunLoopSourceInvalidate函數把輸入源從Run Loop里面移除的話,系統(tǒng)會回調該方法迅箩。
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內存分配器溉愁,默認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);

然后點擊測試一下:

因為我們設置了超時時間奕塑,所以10秒以后堂污,RunLoop就會退出。同時它的InputSource被自動移除龄砰。

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

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

// RunLoop監(jiān)聽回調
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,//內存分配器禽作,默認NULL
                                                                   kCFRunLoopAllActivities,//監(jiān)聽所有狀態(tài)
                                                                   YES,//是否循環(huán)
                                                                   0,//優(yōu)先索引尸昧,一般為0
                                                                   &currentRunLoopObserver,//回調方法
                                                                   &runLoopObserverContext//上下文
                                                                   );
        if (observer)
        {
            CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
        }

運行以后:

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

  • Run Loop的實際應用

我們在實際開發(fā)中經常會遇到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可以實現這個優(yōu)化朽褪。我們知道卡頓的主要原因是因為加載大量大圖是比較耗時的,而在主線程上處理耗時操作時无虚,我們滑動或者點擊屏幕就會有卡頓的感覺缔赠,因為在同一條線程上的任務只能串行執(zhí)行。而我們滑動屏幕時友题,一瞬間要顯示很多張圖片嗤堰,這就形成了一個耗時操作。

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

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

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

    //初始化數據
    self.maxTaskCount = 24;
    self.tasks = [NSMutableArray array];

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


//添加任務
    [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,//內存分配器型凳,默認NULL
                                                               kCFRunLoopBeforeWaiting,//等待之前
                                                               YES,//是否循環(huán)
                                                               0,//優(yōu)先索引,一般為0
                                                               &currentRunLoopObserver,//回調方法
                                                               &runLoopObserverContext//上下文
                                                               );
    if (observer)
    {
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    }
    CFRelease(observer);
}

// RunLoop監(jiān)聽回調
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];
}

然后再運行嘱函,就很流暢了啰脚。當然代碼并不是完整代碼,篇幅有限实夹,只能把主要代碼貼上來橄浓。
總結一下,我們可以把耗時的大量UI操作利用RunLoop分解亮航,使界面保持流暢荸实。

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末准给,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子重抖,更是在濱河造成了極大的恐慌露氮,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钟沛,死亡現場離奇詭異畔规,居然都是意外死亡,警方通過查閱死者的電腦和手機恨统,發(fā)現死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門叁扫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人畜埋,你說我怎么就攤上這事莫绣。” “怎么了悠鞍?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵对室,是天一觀的道長。 經常有香客問我咖祭,道長掩宜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任心肪,我火速辦了婚禮锭亏,結果婚禮上,老公的妹妹穿的比我還像新娘硬鞍。我一直安慰自己慧瘤,他們只是感情好戴已,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锅减,像睡著了一般糖儡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怔匣,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天握联,我揣著相機與錄音,去河邊找鬼每瞒。 笑死金闽,一個胖子當著我的面吹牛,可吹牛的內容都是我干的剿骨。 我是一名探鬼主播代芜,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浓利!你這毒婦竟也來了挤庇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤贷掖,失蹤者是張志新(化名)和其女友劉穎嫡秕,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體苹威,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡昆咽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了屠升。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潮改。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖腹暖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情翰萨,我是刑警寧澤脏答,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站亩鬼,受9級特大地震影響殖告,放射性物質發(fā)生泄漏。R本人自食惡果不足惜雳锋,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一黄绩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧玷过,春花似錦爽丹、人聲如沸筑煮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽真仲。三九已至,卻和暖如春初澎,著一層夾襖步出監(jiān)牢的瞬間秸应,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工碑宴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留软啼,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓延柠,卻偏偏與公主長得像祸挪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子捕仔,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容