Runloop在實際中到底有什么用免胃?

在面試中經常會被問到關于Runloop的問題音五,比如:

  • runloop和線程有什么關系?

  • runloop的mode作用是什么羔沙?

  • 猜想runloop內部是如何實現的躺涝?

等等諸如此類~~~

既然面試中問到這么多關于Runloop的問題,那Runloop在實際應用中到底有什么用呢扼雏?


先來看一個在實際中遇到的問題

TableView的每一行Cell都有三張圖片坚嗜,在剛進入到這個頁面的時候,根本滑不動诗充。因為系統(tǒng)要繪制非常多的圖片苍蔬,如果此時的圖片很大,那么就會出現動圖中的情況蝴蜓,卡頓碟绑。

出現這個問題的原因很簡單,就是同時繪制了過多的大型圖片茎匠。那么這個問題大家平時怎么解決呢格仲?這個問題也是大家平時說的 如何優(yōu)化TableView卡頓 的問題。

  • 異步加載數據诵冒?

  • 異步繪制凯肋?

本篇介紹的方法是使用Runloop來優(yōu)化TableView。原理非常簡單造烁,就是監(jiān)聽Runloop的空閑狀態(tài)否过,在Runloop即將休眠時(空閑時)再去繪制圖片,這樣就不會像動圖中那么卡頓了惭蟋。


初始化最簡單的TableView和Cell

首先在ViewController中構造好最簡單的TableView苗桂。TableView行高定為 70,行數隨數據源的數量而變告组。使用延遲執(zhí)行模擬網絡請求來獲取數據源煤伟。cell使用自定義的 TestTableViewCell

//
//  ViewController.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import "ViewController.h"
#import "TestTableViewCell.h"

@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self configTableView];
    [self requestData];
}

- (void)requestData {
    NSLog(@"請求數據中...");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        for (int i = 0; i < 100; i++) {
            NSMutableArray *arrM = [NSMutableArray array];
            for (int i = 0; i < 3; i++) {
                NSString *imgName = [NSString stringWithFormat:@"img%d.jpg", i+3];
                [arrM addObject:imgName];
            }
            [self.dataArray addObject:arrM];
        }
        [self.tableView reloadData];
    });
}

- (void)configTableView {
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    [self.tableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:@"TestTableViewCell"];
    [self.view addSubview:self.tableView];
    self.tableView.contentInset = UIEdgeInsetsMake(-20, 0, 0, 0);
}

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TestTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestTableViewCell" forIndexPath:indexPath];
    [cell setData:self.dataArray[indexPath.row]];
    return cell;
}

- (NSMutableArray *)dataArray {
    if (_dataArray == nil) {
        _dataArray = [NSMutableArray array];
    }
    return _dataArray;
}


@end

dataArray的數據結構是:

[
    [@"imgName1",@"imgName2",@"imgName3"],
    [@"imgName1",@"imgName2",@"imgName3"],
    [@"imgName1",@"imgName2",@"imgName3"]
]

接下來是cell的實現

//
//  TestTableViewCell.h
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface TestTableViewCell : UITableViewCell

- (void)setData:(NSArray *)dataArray;

@end

//
//  TestTableViewCell.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import "TestTableViewCell.h"

@interface TestTableViewCell()

@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) NSMutableArray *imgViewArray;

@end

@implementation TestTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.imgViewArray = [NSMutableArray array];
        
        NSInteger count = 3;
        for (int i = 0; i < count; i++) {
            UIImageView *imgView = [[UIImageView alloc] init];
            [self.imgViewArray addObject:imgView];
            [self.contentView addSubview:imgView];
        }
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    CGFloat screenWidth = self.contentView.bounds.size.width;
    CGFloat width = (screenWidth - (self.imgViewArray.count+1)*10.0f) / self.imgViewArray.count;
    CGFloat height = self.contentView.bounds.size.height;
    for (int i = 0; i < self.imgViewArray.count; i++) {
        UIImageView *imgView = self.imgViewArray[i];
        imgView.frame = CGRectMake( (i+1)*10 + i*width, 0, width, height);
    }
}

- (void)setData:(NSArray *)dataArray {
    _dataArray = dataArray;
    for (int i = 0; i < 3; i++) {
        UIImageView *imgView = weakSelf.imgViewArray[i];
        UIImage *img = [UIImage imageNamed:dataArray[i]];
        imgView.image = img;
    }
}

@end

這樣實現的就是動圖中卡頓的TableView木缝。

構造Runloop的工具類

接下來介紹便锨,怎么樣構造一個基于Runloop的工具。

首先我碟,在工具類的初始化方法中開啟一個timer放案,保證Runloop一直在循環(huán)。否則監(jiān)聽到Runloop進入休眠的狀態(tài)時矫俺,我們的代碼執(zhí)行過一次后Runloop就進入休眠了吱殉。

- (instancetype)init
{
    self = [super init];
    if (self) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
    }
    return self;
}
- (void)timerFiredMethod {
    // 這個方法不用任何實現掸冤,只是保證Runloop一直在循環(huán)中。
}


功能核心

監(jiān)聽Runloop需要創(chuàng)建Runloop的觀察者 CFRunLoopObserverRef友雳,這個觀察者可以根據需要監(jiān)聽Runloop的各種狀態(tài)稿湿,包括七個枚舉值:

  • kCFRunLoopEntry 即將進入RunLoop

  • kCFRunLoopBeforeTimers 即將處理Timer

  • kCFRunLoopBeforeSources 即將處理Source事件源

  • kCFRunLoopBeforeWaiting 即將進入休眠

  • kCFRunLoopAfterWaiting 剛從休眠中喚醒

  • kCFRunLoopExit 即將退出RunLoop

  • kCFRunLoopAllActivities 監(jiān)聽全部的活動類型

下面是創(chuàng)建觀察者的源碼

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    // 我們監(jiān)聽了 kCFRunLoopBeforeWaiting 即將休眠這一個狀態(tài),就是Runloop處于空閑的狀態(tài)押赊,
    // 當Runloop處于kCFRunLoopBeforeWaiting狀態(tài)就會觸發(fā)這個回調
    // 在這里可以做我們想做的任務了
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);

CFRunLoopObserverCreateWithHandler() 函數中的各項參數:

  • 第一個參數 CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault() 默認分配

  • 第二個參數 CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopBeforeWaiting 監(jiān)聽即將休眠的狀態(tài)

  • 第三個參數 Boolean repeatsYES:持續(xù)監(jiān)聽 NO:不持續(xù)

  • 第四個參數 CFIndex order:優(yōu)先級饺藤,一般填0即可

  • 第五個參數 :回調兩個參數 observer:監(jiān)聽者 activity:監(jiān)聽的事件



CFRunLoopAddObserver() 函數中的參數:

  • 第一個參數 CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop

  • 第二個參數 CFRunLoopObserverRef observer 監(jiān)聽者

  • 第三個參數 CFStringRef mode 要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)

創(chuàng)建了監(jiān)聽者并且給當前Runloop設置后,就可以正常的監(jiān)聽Runloop的各種狀態(tài)了流礁。為了我們優(yōu)化TableView的目的涕俗,我們需要做的是在監(jiān)聽的回調中執(zhí)行最耗性能的操作,即給cell中的三個 imageView 賦值大圖崇棠。

把這個功能包裝成一個單例工具類咽袜,所有耗性能的操作保存在一個數組(taskArray)中,注意:要把這個數組理解成 隊列 去使用枕稀。然后監(jiān)聽Runloop的空閑狀態(tài)询刹,在Runloop空閑的時候去一件一件的做這些耗性能的操作。

上源碼:

//
//  GCRunloopObserver.h
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface GCRunloopObserver : NSObject

+ (instancetype)runloopObserver;

- (void)addTask:(void(^)(void))task;

@end

//
//  GCRunloopObserver.m
//  RunloopOptimizeTableView
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import "GCRunloopObserver.h"

@interface GCRunloopObserver(){
    NSTimer *timer;
}

@property (nonatomic, strong) NSMutableArray *taskArray;

@end

@implementation GCRunloopObserver

+ (instancetype)runloopObserver {
    static dispatch_once_t once;
    static GCRunloopObserver *observer;
    dispatch_once(&once, ^{
        observer = [[GCRunloopObserver alloc] init];
    });
    return observer;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
        [self runloopBeforeWaiting];
    }
    return self;
}

- (void)addTask:(void(^)(void))task {
    if (task) {
        [self.taskArray addObject:task];
    }
}

- (void)runloopBeforeWaiting {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (self.taskArray.count == 0) {
            return;
        }
        // 取出耗性能的任務
        void(^task)(void) = self.taskArray.firstObject;
        // 執(zhí)行任務
        task();
        // 第一個任務出隊列
        [self.taskArray removeObjectAtIndex:0];
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
}

- (void)timerFiredMethod {
    
}

- (NSMutableArray *)taskArray {
    if (_taskArray == nil) {
        _taskArray = [NSMutableArray array];
    }
    return _taskArray;
}

@end


工具類的思路

任務數組中保存的是用戶的耗性能操作萎坷,用Block傳遞過來凹联。工具類本身是一個單例,所以任務數組是唯一的哆档,所有操作都在保存在這個像 “隊列” 一樣的數組(taskArray)中蔽挠,按照先進先出的原則,在Runloop空閑的時候逐個完成瓜浸。這樣這些耗性能的操作不會在Runloop需要完成其它操作的時候來搶占CPU資源澳淑,卡頓的情況就會明顯得到緩解。

另外插佛,監(jiān)聽Runloop選擇的模式(RunloopMode) 也有很大關系杠巡。比如我們的APP需求是剛進入頁面時用戶的操作就要保持流暢,不能出現無法滑動的卡頓雇寇,所以我監(jiān)聽的 RunloopModekCFRunLoopDefaultMode氢拥,這樣在用戶滑動的時候是不加載圖片的,所以用戶的滑動操作會很流暢锨侯。如果這里選擇 kCFRunLoopCommonModes 嫩海,那么在滑動期間仍然會加載圖片,還是會有一些卡頓的情況囚痴。

使用工具類

說完道理叁怪,我們來看看怎么使用吧!創(chuàng)建完這個工具類深滚,只要一步就可以實現優(yōu)化奕谭。把cell給三個 ImageView 賦值的操作提出去耳璧,放到Runloop空閑時再做,因為卡頓就是因為它展箱,所以接下來需要對cell的 - (void)setData:(NSArray *)dataArray 進行改造。先找到耗性能的操作是哪些蹬昌。

這三行是耗性能的元兇:

UIImageView *imgView = self.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;

誰耗性能混驰,就把誰放到Block中:

__weak typeof(self) weakSelf = self;
[[GCRunloopObserver runloopObserver] addTask:^{
    UIImageView *imgView = weakSelf.imgViewArray[i];
    UIImage *img = [UIImage imageNamed:dataArray[i]];
    imgView.image = img;
}];

所以cell的 - (void)setData:(NSArray *)dataArray 方法改造完是這樣的:

- (void)setData:(NSArray *)dataArray {
    _dataArray = dataArray;
    for (int i = 0; i < 3; i++) {
        __weak typeof(self) weakSelf = self;
        [[GCRunloopObserver runloopObserver] addTask:^{
            UIImageView *imgView = weakSelf.imgViewArray[i];
            UIImage *img = [UIImage imageNamed:dataArray[i]];
            imgView.image = img;
        }];
    }
}



運行情況



總結

可以看到卡頓情況得到明顯緩解,一進入頁面的時候滑動不會卡頓皂贩,圖片加載中時滑動也不會卡頓栖榨,只有圖片的加載過程是緩慢的。但是如果同時兼顧滑動和加載圖片那就一定會卡頓明刷,所以看你的需求具體是什么樣的了婴栽。

最后要說,這種方式不僅可以用在優(yōu)化TableView中辈末,還可以應用到你所有出現卡頓的情況當中去愚争。把耗性能的操作放到Runloop隊列中去,等Runloop空閑時一件一件的做挤聘,就不會造成體驗不佳的情況轰枝。

GitHub源碼

GCRunloopObserver

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市组去,隨后出現的幾起案子鞍陨,更是在濱河造成了極大的恐慌,老刑警劉巖从隆,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诚撵,死亡現場離奇詭異,居然都是意外死亡键闺,警方通過查閱死者的電腦和手機寿烟,發(fā)現死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艾杏,“玉大人韧衣,你說我怎么就攤上這事」荷#” “怎么了畅铭?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長勃蜘。 經常有香客問我硕噩,道長,這世上最難降的妖魔是什么缭贡? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任炉擅,我火速辦了婚禮辉懒,結果婚禮上,老公的妹妹穿的比我還像新娘谍失。我一直安慰自己眶俩,他們只是感情好,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布快鱼。 她就那樣靜靜地躺著颠印,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抹竹。 梳的紋絲不亂的頭發(fā)上线罕,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音窃判,去河邊找鬼钞楼。 笑死,一個胖子當著我的面吹牛袄琳,可吹牛的內容都是我干的询件。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼唆樊,長吁一口氣:“原來是場噩夢啊……” “哼雳殊!你這毒婦竟也來了?” 一聲冷哼從身側響起窗轩,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夯秃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痢艺,有當地人在樹林里發(fā)現了一具尸體仓洼,經...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年堤舒,在試婚紗的時候發(fā)現自己被綠了色建。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡舌缤,死狀恐怖箕戳,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情国撵,我是刑警寧澤陵吸,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站介牙,受9級特大地震影響壮虫,放射性物質發(fā)生泄漏。R本人自食惡果不足惜环础,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一囚似、第九天 我趴在偏房一處隱蔽的房頂上張望剩拢。 院中可真熱鬧,春花似錦饶唤、人聲如沸徐伐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呵晨。三九已至,卻和暖如春熬尺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谓罗。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工粱哼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人檩咱。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓揭措,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刻蚯。 傳聞我的和親對象是個殘疾皇子绊含,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,109評論 1 32
  • 目錄 Runloop RunLoop 與線程 個人理解總結 應用場景 1. 什么是RunLoop 基本作用 保持程...
    Ryan___閱讀 1,502評論 0 13
  • OC語言基礎 1.類與對象 類方法 OC的類方法只有2種:靜態(tài)方法和實例方法兩種 在OC中,只要方法聲明在@int...
    奇異果好補閱讀 4,282評論 0 11
  • 概述 RunLoop作為iOS中一個基礎組件和線程有著千絲萬縷的關系炊汹,同時也是很多常見技術的幕后功臣躬充。盡管在平時多...
    陽明先生_X自主閱讀 1,105評論 0 17
  • 說明iOS中的RunLoop使用場景1.保持線程的存活,而不是線性的執(zhí)行完任務就退出了<1>不開啟RunLoop的...
    野生塔塔醬閱讀 6,800評論 15 109