前言
標(biāo)題里每一個單詞都可以用來長篇闊論一篇文章,我自己是參考了一些資料也才著筆。所以,本文對于一些編程思想或者是底層知識只是淺嘗輒止,反而更加著重于應(yīng)用。通常我們會將耗時操作放到子線程,但是更新UI只能在主線程操作,那么UI耗時操作怎么辦?
本文著重講解通過DSL將編程過程中一個“大”的任務(wù)(比如當(dāng)cell的圖片加載過多過大)細(xì)分成一個個小任務(wù)然后裝到runloop中,解決更新UI的耗時操作問題,在一定程度能夠有效的解決卡頓。
SingletonPattern(單例模式)
demo里面用的單例模式,這里不再贅述單例模式。如果想詳細(xì)了解的話,可以參考我之前寫過的文章。
DSL(本文簡單使用鏈?zhǔn)骄幊趟枷?
DSL與鏈?zhǔn)骄幊毯喗?/h4>
- DSL(Domain Specific Language),特定領(lǐng)域表達(dá)式。在OC中,如果使用
Masonry
會經(jīng)常寫出類似下面的代碼。如果是Android或者是其它的什么語言,也會有相應(yīng)的表達(dá)方式。如果是基于鏈?zhǔn)骄幊趟枷氲脑挘韵麓a在各個平臺相似。如有雷同,純屬正常。
make.top.equalTo(superview).with.offset(10);
鏈?zhǔn)骄幊趟枷耄菏菍⒍鄠€操作(多行代碼)通過點(diǎn)號(.)鏈接在一起成為一句代碼,使代碼可讀性提高。
鏈?zhǔn)骄幊烫攸c(diǎn):方法的返回值是block,block必須返回對象本身
(返回block時,block所在的方法調(diào)用者對象)block的參數(shù)是需要操作的值。
Masonry
會經(jīng)常寫出類似下面的代碼。如果是Android或者是其它的什么語言,也會有相應(yīng)的表達(dá)方式。如果是基于鏈?zhǔn)骄幊趟枷氲脑挘韵麓a在各個平臺相似。如有雷同,純屬正常。make.top.equalTo(superview).with.offset(10);
鏈?zhǔn)骄幊趟枷耄菏菍⒍鄠€操作(多行代碼)通過點(diǎn)號(.)鏈接在一起成為一句代碼,使代碼可讀性提高。
鏈?zhǔn)骄幊烫攸c(diǎn):方法的返回值是block,block必須返回對象本身
(返回block時,block所在的方法調(diào)用者對象)block的參數(shù)是需要操作的值。
作為一個iOS程序員基本上都應(yīng)該接觸過Masonry
這個自動布局庫。這個庫能夠極大程度地簡化自動布局的代碼。使用這個庫讓我感到驚嘆的不是如何能夠?qū)⑤^為復(fù)雜的傳統(tǒng)自動布局寫法精簡到如此程度,而是精簡后的代碼的書寫方式。本文的目的之一便是想將細(xì)分任務(wù)的代碼更加優(yōu)雅。
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top);
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
優(yōu)雅的編寫自己的DSL
如何優(yōu)雅地編寫自己的DSL,本文不贅述。不過給大家找到一遍很好的文章,強(qiáng)烈推薦美團(tuán)iOS技術(shù)專家臧成威《如何利用 Objective-C 寫一個精美的 DSL》。
本文用到的鏈?zhǔn)秸{(diào)用
WPRunloopTasks.h
typedef void(^RunloopBlock) (void);
/**
最大任務(wù)數(shù)
*/
@property (nonatomic, assign) NSInteger numOfRunloops;
/**
鏈?zhǔn)秸{(diào)用添加任務(wù)
*/
@property (nonatomic, copy, readonly) WPRunloopTasks * (^addTask) (RunloopBlock runloopTask);
WPRunloopTasks.m
(具體的實(shí)現(xiàn)細(xì)節(jié)可以忽略,知道這個格式,或者參考相應(yīng)的格式即可)
/**
鏈?zhǔn)秸{(diào)用添加task
*/
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
__weak __typeof(&*self)weakSelf = self;
return ^(RunloopBlock runloopTask) {
[weakSelf.numOfRunloopTasks addObject:runloopTask];
//保證之前沒有顯示出來的任務(wù),不再浪費(fèi)時間加載
if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
[weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
}
return weakSelf;
};
}
Runloop
RunLoop 的概念
在新建 xcode 生產(chǎn)的工程中有如下代碼塊:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
}
}
當(dāng)程序啟動時,以上代碼會被調(diào)用,主線程也隨之開始運(yùn)行,RunLoop 也會隨著啟動.
在UIApplicationMain()方法里面完成了程序初始化,并設(shè)置程序的Delegate任務(wù),而且隨之開啟主線程的 RunLoop,就開始接受事件處理.RunLoop 是一個循環(huán),在里面它接受線程的輸入,通過事件處理函數(shù)來處理事件.你的代碼中應(yīng)該提供 while or for 循環(huán)來驅(qū)動 runloop.在你的循環(huán)中,用 runloop 對象驅(qū)動事件處理相關(guān)的內(nèi)容,接受事件,并做響應(yīng)的處理.
RunLoop 接受的事件源有兩種大類: 異步的input sources, 同步的 Timer sources. 這兩種事件的處理方法,系統(tǒng)所規(guī)定.
-
RunLoop 從以下兩個不同的事件源中接受消息:
InputSources : 用來投遞異步消息,通常消息來自另外的線程或者程序.在接受到消息并調(diào)用指定的方法時,線程對應(yīng)的 NSRunLoop 對象會通過執(zhí)行 runUntilDate:方法來退出。Timer Source: 用來投遞 timer 事件(Schedule 或者 Repeat)中的同步消息。在消息處理時,并不會退出 RunLoop。
RunLoop 除了處理以上兩種 Input Soruce,它也會在運(yùn)行過程中生成不同的 notifications,標(biāo)識 runloop 所處的狀態(tài),因此可以給 RunLoop 注冊觀察者 Observer,以便監(jiān)控 RunLoop 的運(yùn)行過程,并在 RunLoop 進(jìn)入某些狀態(tài)時候進(jìn)行相應(yīng)的操作(本文即是運(yùn)用這一點(diǎn))。Apple 只提供了 Core Foundation 的 API來給 RunLoop 注冊觀察者Observer.
Runloop的mode
apple暴露的只有以下兩種模式
kCFRunLoopDefaultMode 默認(rèn)模式,一般用于處理timer
kCFRunLoopCommonModes 占位模式(既是默認(rèn)模式又是交互模式,這一點(diǎn)很重要,使用這種模式在默認(rèn)模式和交互模式都可以觸發(fā)。)
注:交互模式
默認(rèn)是處理UI事件的。
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // set,非內(nèi)核事件,比如點(diǎn)擊按鈕/屏幕
CFMutableSetRef _sources1; // set,系統(tǒng)內(nèi)核事件
CFMutableArrayRef _observers; // Array,觀察者
CFMutableArrayRef _timers; // Array,時鐘
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
Runloop深入理解
有關(guān)runloop的深入理解,推薦ibireme的《深入理解RunLoop》
Runloop總結(jié)
在目前iOS開發(fā)中,幾乎用不到!!但是對于一些高級的功能,我們會涉及到!!
- 保證程序不退出!!
- 負(fù)責(zé)監(jiān)聽(處理)所有的事件: 觸摸,時鐘,網(wǎng)絡(luò)事件等等...
- 負(fù)責(zé)渲染我們的UI,Runloop一次循環(huán)渲染整個界面!!
- 如果沒有事件發(fā)生,那么"睡覺"
DSL+Runloop
在init方法中創(chuàng)建觀察者,在觀察者的回調(diào)中執(zhí)行任務(wù)并刪除已經(jīng)執(zhí)行的任務(wù)
/**
添加觀察者
*/
- (void)addRunloopObserver {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
static CFRunLoopObserverRef defaultModeServer;
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL,
};
defaultModeServer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
//運(yùn)行循環(huán) 觀察者 Runloop占位模式
CFRunLoopAddObserver(runloop, defaultModeServer, kCFRunLoopCommonModes);
CFRelease(defaultModeServer);
}
/**
回調(diào)函數(shù),一次runloop運(yùn)行一次
@param observer 觀察者
@param activity 活動
@param info info
*/
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//這里的info經(jīng)過打印知道是self,所以可以通過info拿到property
WPRunloopTasks *runloop = (__bridge WPRunloopTasks *)info;
if(runloop.numOfRunloopTasks.count) {
//取出任務(wù)
RunloopBlock task = runloop.numOfRunloopTasks.firstObject;
//執(zhí)行任務(wù)
task();
//干掉第一個任務(wù)
[runloop.numOfRunloopTasks removeObjectAtIndex:0];
}
}
鏈?zhǔn)秸{(diào)用添加任務(wù)
/**
鏈?zhǔn)秸{(diào)用添加task
*/
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
__weak __typeof(&*self)weakSelf = self;
return ^(RunloopBlock runloopTask) {
[weakSelf.numOfRunloopTasks addObject:runloopTask];
//保證之前沒有顯示出來的任務(wù),不再浪費(fèi)時間加載
if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
[weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
}
return weakSelf;
};
}
模擬卡頓
demo中的圖片是3072*2304高清大圖。在渲染的時候,為了更加直觀感受效果,用了0.3s的動畫。每一個cell有3張圖片,屏幕上至少會出現(xiàn)6個cell。先來看一下最后的調(diào)用代碼:
[WPRunloopTasks shareRunloop].addTask(^{
[UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
[cell.contentView addSubview:imageView1];
} completion:nil];
}).addTask(^{
[UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
[cell.contentView addSubview:imageView2];
} completion:nil];
}).addTask(^{
[UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
[cell.contentView addSubview:imageView3];
} completion:nil];
});
效果對比
源碼
總結(jié)
- 如果連更新UI耗時的操作都可以優(yōu)化,我想只要是不涉及到更加底層的東西,都是可以優(yōu)化的很好的。本問在“外功”方面已經(jīng)做的可以了,至于“內(nèi)功”比如圖片的解碼問題等等就不是本文的范疇了。
- runloop功能比較強(qiáng)大,設(shè)計(jì)到高級功能的應(yīng)該是會用到的。
- NSRunloop是對CFRunLoop的封裝,是線程不安全的,而CFRunLoop是線程安全的。