人生就像RunLoop料皇,不斷的循環(huán)谓松、不斷的往復(fù)。當(dāng)線程被殺掉践剂,當(dāng)生命結(jié)束鬼譬,RunLoop就消失了,人生也就結(jié)束了逊脯。在有限的生命里优质,為何不讓自己像RunLoop一樣優(yōu)雅的活著,享受每一個(gè)循環(huán)军洼。
作為一個(gè)有強(qiáng)迫癥患者巩螃,在自己的app中容不下一絲雜質(zhì)。最近了解了一下Runloop,在這里更大家分享下匕争。
這里有一個(gè)模擬卡頓的Demo避乏,包含卡頓與優(yōu)化后的代碼,雖然工作中很少遇到卡頓,(有人會(huì)說就算卡頓也是因?yàn)榇a沒寫好,是的甘桑,我這里只是模擬一個(gè)卡頓的現(xiàn)象并通過Runloop解決拍皮,重點(diǎn)是Runloop解決問題的方式歹叮,不是這個(gè)代碼不該卡頓):
一、卡頓分析
最根本的原因是RunLoop轉(zhuǎn)一圈的時(shí)間太長了春缕,因?yàn)橐淮蜶unLoop循環(huán)需要解析24張大圖盗胀,很卡
既然一次RunLoop加載24張圖卡艘蹋,那能不能一次循環(huán)加載1張呢锄贼?
二、分析RunLoop的運(yùn)行機(jī)制
RunLoop:運(yùn)行循環(huán)
-保證(線程)不退出
-負(fù)責(zé)監(jiān)聽所有事件:時(shí)鐘女阀、觸摸宅荤、網(wǎng)絡(luò)事件,沒有事件就睡眠
-每一條線程上面都有一個(gè)RunLoop浸策,但是子線程的RunLoop默認(rèn)不運(yùn)行
RunLoop的事件處理:每當(dāng)有時(shí)鐘冯键、觸摸、網(wǎng)絡(luò)事件發(fā)生的時(shí)候庸汗,RunLoop蘇醒惫确,執(zhí)行一次循環(huán),循環(huán)執(zhí)行完畢馬上進(jìn)入睡眠狀態(tài)蚯舱。
可以猜想:能不能通過NSTimer來每間隔一定時(shí)間執(zhí)行一個(gè)任務(wù)改化,這樣RunLoop每間隔一定時(shí)間就會(huì)蘇醒一次。每蘇醒一次就執(zhí)行加載一張圖片枉昏。當(dāng)圖片加載完成讓NSTimer釋放陈肛,當(dāng)列表每滑動(dòng)一次,讓NSTimer重新執(zhí)行任務(wù)兄裂。
如下圖句旱,RunLoop 想要跑起來,必須有 Mode 對象支持晰奖,而 Mode 里面必須有
(NSSet *)Source谈撒、 (NSArray *)Timer ,源和定時(shí)器匾南。
至于另外一個(gè)類(NSArray *)observer是用于監(jiān)聽 RunLoop 的狀態(tài)港华,因此不會(huì)激活RunLoop。
observer是用于監(jiān)聽 RunLoop 的狀態(tài)午衰,我們就可以通過observer來監(jiān)聽runloop的蘇醒
偽代碼
1立宜、創(chuàng)建一個(gè)定時(shí)器:每間隔0.001s執(zhí)行一個(gè)空方法來喚醒RunLoop
2、將加載圖片的方法裝入block臊岸,將block加入數(shù)組
3橙数、監(jiān)聽RunLoop的蘇醒,蘇醒回掉就執(zhí)行一次就從數(shù)組中取出一個(gè)事件帅戒,執(zhí)行完的事件從數(shù)組中刪除
說干就干灯帮。
三崖技、代碼實(shí)現(xiàn)
可以先下載Demo
1、創(chuàng)建一個(gè)定時(shí)器:每間隔0.001s執(zhí)行一個(gè)空方法來喚醒RunLoop(這里存在質(zhì)疑钟哥,后面已經(jīng)回答了質(zhì)疑)
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
-(void)timerMethod{
//啥都不干!!
}
(這里要感謝DreamTracer和大神Q提的建議)
那這里添加定時(shí)器又是為了干什么呢迎献?應(yīng)不應(yīng)該加呢?下面會(huì)講到腻贰。
2吁恍、將加載圖片的方法裝入block,將block加入數(shù)組
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = [[MyTableViewCell alloc] cellWithTableView:tableView withID:@"cell"];
NSLog(@"current:%ld",(long)indexPath.row);
...
cell.myImageView.image = nil;
cell.secondLImage.image = nil;
cell.thirdyLImage.image = nil;
cell.fouthLImage.image = nil;
##### //添加事件
//添加文字
[self addTask:^{
cell.myLabel.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
}];
[self addTask:^{
cell.thirdLabel.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
}];
NSString *path1 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
//添加圖片
[self addTask:^{
UIImage *image1 = [UIImage imageWithContentsOfFile:path1];
cell.myImageView.image = image1;
}];
[self addTask:^{
UIImage *image2 = [UIImage imageWithContentsOfFile:path1];
cell.secondLImage.image = image2;
}];
[self addTask:^{
UIImage *image3 = [UIImage imageWithContentsOfFile:path1];
cell.thirdyLImage.image = image3;
}];
[self addTask:^{
UIImage *image4 = [UIImage imageWithContentsOfFile:path1];
cell.fouthLImage.image = image4;
}];
return cell;
}
-(void)addTask:(RunloopBlock)task{
if (!self.timer) {//這是優(yōu)化
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
}
//添加任務(wù)到數(shù)組!!
[self.tasks addObject:task];
}
3播演、監(jiān)聽RunLoop的蘇醒冀瓦,蘇醒回掉就執(zhí)行一次就從數(shù)組中取出一個(gè)事件,執(zhí)行完的事件從數(shù)組中刪除
#pragma mark - <RunLoop>
//添加RunLoop觀察者!! CoreFoundtion 里面 Ref (引用)指針!!
-(void)addRunloopObserver{
//拿到當(dāng)前的runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定義一個(gè)context
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL,
};
//定義觀察
static CFRunLoopObserverRef defaultModeObserver;
//創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &Callback, &context);
//添加當(dāng)前runloop的觀察者!!
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
//C 語言里面Create相關(guān)的函數(shù)!創(chuàng)建出來的指針!需要釋放
CFRelease(defaultModeObserver);
}
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSLog(@"gemelaile ");
//拿到控制器
ViewController * vc = (__bridge ViewController *)info;
if (vc.tasks.count == 0) {//任務(wù)執(zhí)行完成就清掉timer
[vc.timer invalidate];
vc.timer = nil;
return;
}
RunloopBlock task = vc.tasks.firstObject;
task();
//干掉第一個(gè)任務(wù)
[vc.tasks removeObjectAtIndex:0];
}
如上代碼做到了監(jiān)聽RunLoop的蘇醒写烤,每次蘇醒都會(huì)回掉Callback方法
4 打開demo翼闽,分別引入
ViewController(優(yōu)化過的)與ViewControllerNo(未優(yōu)化過的)運(yùn)行,看是不是完美解決卡頓洲炊。(可以看看內(nèi)存感局,cpu)
敲黑板
那么我們現(xiàn)在來講講為什么前面我們要添加定時(shí)器。
我們把加載圖片的事件放進(jìn)數(shù)組中暂衡,每次runloop循環(huán)一次就執(zhí)行一次事件询微。每次有拖動(dòng)事件發(fā)生,runloop都會(huì)自動(dòng)執(zhí)行古徒,runloop執(zhí)行幾次呢拓提,我不知道。所以為了安全起見隧膘,這里我加了個(gè)定時(shí)器代态。當(dāng)然這里可以添加優(yōu)化,例如滑動(dòng)結(jié)束后初始化定時(shí)器疹吃,事件執(zhí)行完就清理定時(shí)器蹦疑。
完美解決卡頓問題,RunLoop是不是很強(qiáng)大萨驶。
是不是以為這篇文章就結(jié)束了歉摧,那你就太小看我了。
每次有拖動(dòng)事件發(fā)生腔呜,runloop都會(huì)自動(dòng)執(zhí)行叁温,runloop執(zhí)行幾次呢,我不知道核畴。最后我進(jìn)入了深入的實(shí)驗(yàn)了解膝但。
真正的重點(diǎn)來了
//創(chuàng)建runloop的即將處理 Source的觀察者
static CFRunLoopObserverRef defaultModeObserver1;
defaultModeObserver1 = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeSources, YES, 0, & sourceTodo, &context);
//添加當(dāng)前runloop的觀察者!!
CFRunLoopAddObserver(runloop, defaultModeObserver1, kCFRunLoopCommonModes);
//C 語言里面Create相關(guān)的函數(shù)!創(chuàng)建出來的指針!需要釋放
CFRelease(defaultModeObserver1);
static void sourceTodo(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSLog(@"sourceTodo");
}
這樣每次有事件就會(huì)調(diào)用sourceTodo的事件
在callback也加上輸出log,同時(shí)把[vc.tasks removeObjectAtIndex:0];注銷掉,那樣vc.tasks就一直有事件谤草,看看到底callBack會(huì)走多少次跟束。這樣是不是就解決了我們的疑惑呢
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//拿到控制器
ViewController * vc = (__bridge ViewController *)info;
NSLog(@"CallbackNoTask");//這里還沒有執(zhí)行事件
if (vc.tasks.count == 0) {
return;
}
RunloopBlock task = vc.tasks.firstObject;
task();
//干掉第一個(gè)任務(wù)
// [vc.tasks removeObjectAtIndex:0];
NSLog(@"CallbackHasTask");//這里執(zhí)行了事件
}
一個(gè)驚人的發(fā)現(xiàn)莺奸,callback一直停不下來。這是為什么?
RunloopBlock task = vc.tasks.firstObject;
task();
task()執(zhí)行了什么冀宴,難道里面包含source或timer事件
+(void)addImage1With:(UITableViewCell *)cell{
//第一張
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];
imageView.tag = 1;
NSString *path1 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:path1];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = image;
[UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
[cell.contentView addSubview:imageView];
} completion:nil];
}
[UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
[cell.contentView addSubview:imageView];
} completion:nil];
這是一個(gè)timer事件
那這樣果斷去掉定時(shí)器灭贷。
把注銷的代碼[vc.tasks removeObjectAtIndex:0]打開,運(yùn)行看看
事件執(zhí)行完后略贮,runloop還是會(huì)跑幾次就結(jié)束了甚疟。
功夫不負(fù)苦心人,終于算了解決了這個(gè)疑惑刨肃。再次感謝之前對我的文章提出質(zhì)疑的大神們古拴,是你們讓我有了動(dòng)力來解決這些疑惑箩帚。
有沒有了解的欲望U嬗选!紧帕!
完整的Demo
重點(diǎn)在runloop的用法盔然,不在為什么會(huì)卡頓
下面是RunLoop的一些基礎(chǔ)知識(shí),希望對你有幫助
RunLoop入門
一是嗜、簡介
首先愈案,先象征性的講下RunLoop的概念
從字面上看,就可以看出就是兜圈圈鹅搪,就是一個(gè)死循環(huán)嘛站绪。
二、作用
1.保持程序運(yùn)行
2.處理app的各種事件(比如觸摸丽柿,定時(shí)器等等)
3.節(jié)省CPU資源恢准,提高性能。
三甫题、枯燥知識(shí)
下面是關(guān)于RunLoop的一些使用簡述馁筐。也許有點(diǎn)枯燥,但是也是必須要知道的W狗恰(敲黑板ing)敏沉,我盡量說的通俗易懂一點(diǎn)。
1.兩個(gè)API
首先要知道iOS里面有兩套API可以訪問和使用RunLoop:
Foundation
NSRunLoop
Core Foundation
CFRunLoopRef
上面兩套都可以使用炎码,但是要知道CFRunLoopRef是用c語言寫的盟迟,是開源的,相比于NSRunLoop更加底層潦闲,而NSRunLoop其實(shí)是對CFRunLoopRef的一個(gè)簡單的封裝攒菠。便于使用而已。這樣說來矫钓,顯然CFRunLoopRef的性能要高一點(diǎn)要尔。
2.RunLoop與線程(形象)
1.每條線程都有唯一的與之對應(yīng)的RunLoop對象舍杜。
2.主線程的RunLoop已經(jīng)創(chuàng)建好了,而子線程的需要手動(dòng)創(chuàng)建赵辕。(也就是說子線程的RunLoop默認(rèn)是關(guān)閉的既绩,因?yàn)橛袝r(shí)候開了個(gè)線程但卻沒有必要開一個(gè)RunLoop,不然反而浪費(fèi)了資源还惠。 )
3.RunLoop在第一次獲取時(shí)創(chuàng)建饲握,在線程結(jié)束時(shí)銷毀。(這就相當(dāng)于 線程是一個(gè)類蚕键,RunLoop是類里的實(shí)例變量救欧,這樣便于理解)
3.獲取RunLoop對象
Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
4.RunLoop相關(guān)類
在Core Foundation中有RunLoop的五個(gè)類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
這五個(gè)類的關(guān)系如下
由圖中可以得出以下幾點(diǎn):
1.CFRunLoopModeRef代表的是RunLoop的運(yùn)行模式。
2.一個(gè) RunLoop 包含若干個(gè) Mode锣光,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer笆怠。
3.每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode誊爹,這個(gè)Mode被稱作 CurrentMode蹬刷。
4.如果需要切換 Mode,只能退出 Loop频丘,再重新指定一個(gè) Mode 進(jìn)入办成。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響搂漠。
CFRunLoopModeRef
系統(tǒng)默認(rèn)注冊了5個(gè)mode
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惊科,不是一種真正的Mode
至于CFRunLoopModeRef的使用我會(huì)在 下面的實(shí)驗(yàn)三timer的使用中 詳細(xì)說到拍摇。
四、實(shí)驗(yàn)講解
這里開始之前馆截,希望您跟著新建一個(gè)工程充活。實(shí)操最清晰。
一蜡娶、main函數(shù)的實(shí)驗(yàn)
再來做個(gè)試驗(yàn):將main的代碼添加一個(gè)輸出NSLog混卵,如下
int main(int argc, char * argv[]) {
@autoreleasepool {
int res = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"-----");
return res;
}
}
你猜會(huì)輸出 “-----” 嗎?答案是否定的窖张,你會(huì)發(fā)現(xiàn)程序始終不會(huì)到NSLog(@"-----");這一行來幕随。這就說明了程序一直在運(yùn)行著。其實(shí)這都是RunLoop的功勞宿接,它的其中一個(gè)功能就是保持程序的持續(xù)運(yùn)行。有了RunLoop,main里面相當(dāng)于是這樣的代碼(偽代碼):
BOOL running = YES;
do {
// 執(zhí)行各種操作
} while (running);
return 0;
程序是始終在while里面的退渗,是一個(gè)死循環(huán)。
說到這里你肯定又會(huì)疑惑走诞,RunLoop是什么時(shí)候創(chuàng)建的。其實(shí)在UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))這個(gè)函數(shù)的內(nèi)部就已經(jīng)啟動(dòng)了一個(gè)RunLoop蛤高,所以函數(shù)一直沒有返回蚣旱,這才使得程序保持運(yùn)行。
(注意:這個(gè)默認(rèn)啟動(dòng)的RunLoop是和主線程相關(guān)的!!!)
二戴陡、NSTimer的使用
在項(xiàng)目中用的NSTimer其實(shí)也和RunLoop有關(guān)系塞绿,下面我們來做個(gè)實(shí)驗(yàn)
實(shí)驗(yàn)一 scheduledTimer方法
新建一個(gè)工程,在ViewController中添加一個(gè)UIButton恤批,增加button的響應(yīng)以及timerTest方法异吻,代碼如下
- (IBAction)ButtonDidClick:(id)sender {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"timerTest----");
}
點(diǎn)擊button可以看到輸出臺(tái)每隔一秒鐘就打印"timerTest----"。
實(shí)驗(yàn)二 timerWithTime方法
代碼如下:
- (IBAction)ButtonDidClick:(id)sender {
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"timerTest----");
}
但是實(shí)驗(yàn)結(jié)果是开皿,點(diǎn)擊button后沒有反應(yīng)涧黄。為什么呢篮昧?
噢~原來是少加了一句話赋荆,添加后的代碼如下:
- (IBAction)ButtonDidClick:(id)sender {
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
NSLog(@"timerTest----");
}
可是,為什么實(shí)驗(yàn)二比實(shí)驗(yàn)一要多加一句話呢懊昨?解:那是因?yàn)閟cheduledTimer方法會(huì)自動(dòng)添加到當(dāng)前的runloop里面去窄潭,而且runloop的運(yùn)行模式kCFRunLoopDefaultMode,也就是說實(shí)驗(yàn)一已經(jīng)將timer自動(dòng)加入到了一個(gè)運(yùn)行模式為kCFRunLoopDefaultMode的runloop中酵颁。
實(shí)驗(yàn)三 有scrollView的情況下使用Timer
首先嫉你,按鈕響應(yīng)以及timerTest的方法如下:
- (IBAction)ButtonDidClick:(id)sender {
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
NSLog(@"timerTest----");
}
然后在vc中加一個(gè)textView,run起來躏惋,
然后點(diǎn)擊按鈕幽污,隨后滑動(dòng)textView,根據(jù)打印結(jié)果可以看出滑動(dòng)textView的時(shí)候是不打印的簿姨,奇怪吧距误。其實(shí)說到底還是RunLoop搞的鬼”馕唬可以看到准潭,我們把timer加到了NSDefaultRunLoopMode的runLoop中,而在滑動(dòng)textview的時(shí)候域仇,RunLoop就切換到UITrackingRunLoopMode模式刑然,而上面有提到說:在每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode暇务,這個(gè)Mode被稱作 CurrentMode泼掠。 所以定時(shí)器就不起作用了怔软。
現(xiàn)在可以思考一下解決方法了!(敲黑板ing)
提示一下择镇,問題出在了模式上面爽雄,是不是修改一下模式就好了呢。
解決方法:
上面有提到過五個(gè)mode
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,不是一種真正的Mode
其實(shí)如果把mode改為kCFRunLoopCommonModes的話就可以既支持kCFRunLoopDefaultMode又支持UITrackingRunLoopMode了憔涉。
修改如下:
修改mode類型
- (IBAction)ButtonDidClick:(id)sender {
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
然后run發(fā)現(xiàn)就算滾動(dòng)textView也不會(huì)影響打印订框。
寫在最后:
希望這篇文章對您有幫助。當(dāng)然如果您發(fā)現(xiàn)有可以優(yōu)化的地方兜叨,希望您能慷慨的提出來穿扳。最后祝您工作愉快!