最近在整理資料的時(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)聽是怎么做到的呢?
- 一種是通過
scrollView
的delegate
方法; - 另一種就是通過監(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上了。
千里之行,始于足下~