使用RunLoop優(yōu)化TableView

最近在整理資料的時(shí)候看到了以前寫的代碼,在此整理下,用到的時(shí)候可以方便查看。
RunLoop 不熟悉的可以查看下《iOS官方文檔》和這篇博客《深入理解RunLoop》,講述的淋漓盡致,相信會(huì)對你有很大的幫助。

一、使用TableView時(shí)出現(xiàn)的問題:

平時(shí)開發(fā)中繪制 tableView 時(shí),我們使用的 cell 可能包含很多業(yè)務(wù)邏輯,比如加載網(wǎng)絡(luò)圖片、繪制內(nèi)容等等。如果我們不進(jìn)行優(yōu)化的話,在繪制 cell 時(shí)這些任務(wù)將同時(shí)爭奪系統(tǒng)資源,最直接的后果就是頁面出現(xiàn)卡頓,更嚴(yán)重的則會(huì) crash
我通過在 cell 上加載大的圖片(找的系統(tǒng)的壁紙,大小10M左右)并改變其大小來模擬 cell 的復(fù)雜業(yè)務(wù)邏輯。

系統(tǒng)的壁紙放在Mac的這個(gè)位置:
-> 前往 -> 前往文件夾 /Library/Desktop Pictures

cell 的繪制方法中實(shí)現(xiàn)如下:

CGFloat width = (self.view.bounds.size.width-4*kBorder_W) /3;

UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
                                                                  kBorder_W,
                                                                  width,
                                                                  kCell_H-kBorder_W)];
img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
[cell addSubview:img1];
UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
                                                                  kBorder_W,
                                                                  width,
                                                                  kCell_H-kBorder_W)];
img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
[cell addSubview:img2];
UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
                                                                  kBorder_W,
                                                                  width,
                                                                  kCell_H-kBorder_W)];
img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
[cell addSubview:img3];

tableView 在繪制 cell 的時(shí)候同時(shí)處理這么多資源,會(huì)導(dǎo)致頁面滑動(dòng)不流暢等問題。此處只是模擬,可能效果不明顯,但這都不是重點(diǎn)~

微信對 cell 的優(yōu)化方案是當(dāng)監(jiān)聽到列表滾動(dòng)時(shí),停止 cell 上的動(dòng)畫等方式,來提升用戶體驗(yàn)。

Q:那么問題來了,這個(gè)監(jiān)聽是怎么做到的呢?

  • 一種是通過 scrollViewdelegate 方法;
  • 另一種就是通過監(jiān)聽 runLoop

如果有其他方案,歡迎告知~

二、下面就分享下通過監(jiān)聽RunLoop來優(yōu)化TableView:

步驟如下:

(1).獲取當(dāng)前主線程的 runloop

 CFRunLoopRef runloop = CFRunLoopGetCurrent();

(2).創(chuàng)建觀察者 CFRunLoopObserverRef , 來監(jiān)聽 runloop

  • 創(chuàng)建觀察者用到的核心函數(shù)就是
    CFRunLoopObserverCreate
// allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認(rèn)的分配器kCFAllocatorDefault。
// activities:要監(jiān)聽runloop的狀態(tài)
/*
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即將進(jìn)入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
        kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
        kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件
    };
*/
// repeats:是否重復(fù)監(jiān)聽
//   order:觀察者優(yōu)先級,當(dāng)Run Loop中有多個(gè)觀察者監(jiān)聽同一個(gè)運(yùn)行狀態(tài)時(shí),根據(jù)該優(yōu)先級判斷,0為最高優(yōu)先級別。
// callout:觀察者的回調(diào)函數(shù),在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包。
// context:觀察者的上下文。
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator,
                                                       CFOptionFlags activities,
                                                       Boolean repeats,
                                                       CFIndex order,
                                                       CFRunLoopObserverCallBack callout,
                                                       CFRunLoopObserverContext *context);
a).創(chuàng)建觀察者
// 1.定義上下文
CFRunLoopObserverContext context = {
    0,
    (__bridge void *)(self),
    &CFRetain,
    &CFRelease,
    NULL
};
// 2.定義觀察者
static CFRunLoopObserverRef defaultModeObserver;
// 3.創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopBeforeWaiting,
                                              YES,
                                              0,
                                              &callBack,
                                              &context);
// 4.給當(dāng)前runloop添加觀察者
// kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的。
// UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。
// UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用。
// GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
// kCFRunLoopCommonModes: 這是一個(gè)占位的 Mode,沒有實(shí)際作用。
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
// 5.內(nèi)存管理
CFRelease(defaultModeObserver);
b).實(shí)現(xiàn) callBack 函數(shù),只要檢測到對應(yīng)的runloop狀態(tài),該函數(shù)就會(huì)得到響應(yīng)。
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    
    ViewController *vc = (__bridge ViewController *)info;
    
    //無任務(wù)  退出
    if (vc.tasksArr.count == 0) return;
    
    //從數(shù)組中取出任務(wù)
    runloopBlock block = [vc.tasksArr firstObject];
    
    //執(zhí)行任務(wù)
    if (block) {
        block();
    }
    
    //執(zhí)行完任務(wù)之后移除任務(wù)
    [vc.tasksArr removeObjectAtIndex:0];
    
}

c).從上面的函數(shù)實(shí)現(xiàn)中我們看到了block、arr等對象,下面解析下:
  • 使用 Array 來存儲需要執(zhí)行的任務(wù);
- (NSMutableArray *)tasksArr {
    if (!_tasksArr) {
        _tasksArr = [NSMutableArray array];
    }
    return _tasksArr;
}
  • 定義參數(shù) maxTaskCount 來表示最大任務(wù)數(shù),優(yōu)化項(xiàng)目;
//最大任務(wù)數(shù)
@property (nonatomic, assign) NSUInteger maxTaskCount;
...
// 當(dāng)超出最大任務(wù)數(shù)時(shí),以前的老任務(wù)將從數(shù)組中移除
self.maxTaskCount = 50;
  • 使用 block代碼塊 來包裝一個(gè)個(gè)將要執(zhí)行的任務(wù),便于 callBack 函數(shù)中分開執(zhí)行任務(wù),減少同時(shí)執(zhí)行對系統(tǒng)資源的消耗。
//1. 定義一個(gè)任務(wù)block
typedef void(^runloopBlock)();
//2. 定義一個(gè)添加任務(wù)的方法,將任務(wù)裝在數(shù)組中
- (void)addTasks:(runloopBlock)task {
    //保存新任務(wù)
    [self.tasksArr addObject:task];
    //如果超出最大任務(wù)數(shù) 丟棄之前的任務(wù)
    if (self.tasksArr.count > _maxTaskCount) {
        [self.tasksArr removeObjectAtIndex:0];
    }
}
//3. 將任務(wù)添加到代碼塊中
// 耗時(shí)操作放在任務(wù)中
[self addTasks:^{
    UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
                                                                      kBorder_W,
                                                                      width,
                                                                      kCell_H-kBorder_W)];
    img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
    [cell addSubview:img1];
}];
[self addTasks:^{
    UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
                                                                      kBorder_W,
                                                                      width,
                                                                      kCell_H-kBorder_W)];
    img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
    [cell addSubview:img2];
}];
[self addTasks:^{
    UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
                                                                      kBorder_W,
                                                                      width,
                                                                      kCell_H-kBorder_W)];
    img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
    [cell addSubview:img3];
}];

(3).使 runloop 不進(jìn)入休眠狀態(tài)。

Q:按照上面步驟實(shí)現(xiàn)的情況下:我有500行的cell,為什么才顯示這么一點(diǎn)點(diǎn)呢?
A:runloop 在加載完 cell 時(shí)沒有其他事情做了,為了節(jié)省資源消耗,就進(jìn)入了休眠狀態(tài),等待有任務(wù)時(shí)再次被喚醒。在我們觀察者的 callBack 函數(shù)中任務(wù)被一個(gè)個(gè)取出執(zhí)行,還沒有執(zhí)行完,runloop 就切換狀態(tài)了(休眠了), callBack 函數(shù)不再響應(yīng)。導(dǎo)致出現(xiàn)上面的情況。
解決方法:
創(chuàng)建定時(shí)器 (保證runloop回調(diào)函數(shù)一直在執(zhí)行)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                         selector:@selector(notDoSomething)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
...
- (void)notDoSomething {
    // 不做事情,就是為了讓 callBack() 函數(shù)一直相應(yīng)
}

此時(shí),對tableView的優(yōu)化就大功告成了!
代碼放在GitHub上了。

千里之行,始于足下~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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