-
RunLoop
- 一個運行循環(huán)
- 保持程序的持續(xù)運行
- 監(jiān)聽處理 APP 各種事件(觸摸,定時器,selector)
- 節(jié)省 CPU 資源,提高程序性能(有事做的時候做事,沒事做就休息)
-
main函數(shù)中的RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"哈哈哈哈");
return a;
}
}
如果我么在 main.m 文件中添加一句輸出
運行以后"哈哈哈哈"是不會打印的
因為系統(tǒng)默認(rèn)在 UIApplicationMain 函數(shù)中為主線程開啟了 RunLoop
RunLoop 一直在運行處理各種事件或者等待事件的到來
UIApplicationMain 就不會返回
-
RunLoop結(jié)構(gòu):
iOS中有2套API來訪問和使用 RunLoop
分別是:
Foundation 框架中的 NSRunLoop
Core Foundation 框架中的 CFRunLoopRef
NSRunLoop和CFRunLoopRef 都代表著 RunLoop對象
NSRunLoop 是基于 CFRunLoopRef的一層 OC 包裝
所以要了解 RunLoop 內(nèi)部結(jié)構(gòu)
需要多研究 CFRunLoopRef 層面的 API(Core Foundation 層面)
-
RunLoop中的Mode
kCFRunLoopDefaultMode:App的默認(rèn) Mode,通常主線程是在這個 Mode 下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,當(dāng)界面有scrollView滾動, 切入到該模式, 保證界面滑動時不受其他 Mode 影響,
UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode个初,通常用不到
kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
上代碼 :
創(chuàng)建項目, 在控制器中添加定時器:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *time = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
}
- (void)run{
NSLog(@"run");
}
運行程序后
run是正常打印的
可是當(dāng)我們在控制器的 view 添加一個TextView再運行
run 還是正常打印
一旦開始滾動TextView
run 就停止打印了
因為這時候定時器被暫停了
為什么呢?
我們來打印一下 TextView 在滾動和不滾動兩種狀態(tài)下的RunLoopMode:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"一開始我的模式是: %@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(@"當(dāng)我開始滾動時的模式: %@",[[NSRunLoop currentRunLoop] currentMode]);
}
打印結(jié)果如下:
打印中可以看出
TextView 一開始滾動
RunLoop 的 Mode 就由 kCFRunLoopDefaultMode 切換到 UITrackingRunLoopMode
由于定時器是添加到 NSDefaultRunLoopMode
所以 TextView 滾動進(jìn)入 UITrackingRunLoopMode 模式后
定時器就停止了
如果我們想在scrollView滾動的時候做一些事情
可以使用這中方法
如果我們想讓定時器在兩種模式下都工作
就要讓定時器加入到 kCFRunLoopCommonModes 模式下
kCFRunLoopCommonModes 模式不是一個具體的模式
他只是一個標(biāo)記
加入該模式后, 會運行在所有標(biāo)記為 kCFRunLoopCommonModes 模式的模式下
打印一下 RunLoo 可以看出
標(biāo)記為 kCFRunLoopCommonModes 的模式分別是
kCFRunLoopDefaultMode 和 UITrackingRunLoopMode
就是默認(rèn)模式和滾動模式
-
RunLoop中的Source
CFRunLoopSourceRef是事件源(輸入源)
根據(jù)蘋果文檔的分類 :
Port-Based Sources : 來自內(nèi)核或者其他線程的一些事件
Custom Input Sources : 自定義輸入源
Cocoa Perform Selector Sources : 處理performSelector方法中的事件(觸摸 , 點擊等等)
根據(jù)函數(shù)調(diào)用棧的分類 :
Source0:非基于Port的
Source1:基于Port的, 接收內(nèi)核或者其他線程發(fā)過來的事件, 分發(fā)給Source0處理
-
RunLoop中的Observer
CFRunLoopObserverRef 是觀察者
能夠監(jiān)聽 RunLoop 的狀態(tài)改變
可以監(jiān)聽的時間點有一下幾點:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即將進(jìn)入 RunLoop
kCFRunLoopEntry = (1UL << 0),
//即將處理 Timer
kCFRunLoopBeforeTimers = (1UL << 1),
//即將處理Source
kCFRunLoopBeforeSources = (1UL << 2),
//即將進(jìn)入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
//即將被喚醒
kCFRunLoopAfterWaiting = (1UL << 6),
//即將退出
kCFRunLoopExit = (1UL << 7),
// 所有都監(jiān)聽
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
上代碼 :
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
});
// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
最后需要自己釋放:
CFRelease(observer);
-
RunLoop運轉(zhuǎn)流程 :
首先,進(jìn)入 RunLoop (順便通知Observer我要進(jìn)入了)
然后就去處理 Timer (順便通知 Observer 我要處理 Timer)
然后處理 Source (順便通知 Observer 我要處理 Source)
然后判斷 Source1 還有沒有沒有分發(fā)的任務(wù)?
--- 有的話就去處理 Source1 收到的任務(wù)包括 Timer 和Source
--- 沒有的話, 就要去睡覺了
任務(wù)都處理完了,進(jìn)入休眠(順便通知 Observer 我要休眠)
休眠的時候如果有新的任務(wù)進(jìn)入, RunLoop被喚醒....
-
RunLoop實踐 :
-
1:tableView滾動的時候不進(jìn)行耗時事件 :
例如進(jìn)行大圖顯示的時候,可能系統(tǒng)會渲染事件過長, 如果這時候用戶正在拖動tableView,會造成卡頓
可以利用RunLoop來處理
只在 NSDefaultRunLoopMode 模式下顯示圖片
tableView滾動的時候是 UITrackingRunLoopMode 模式
不會產(chǎn)生沖突
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
-
2:常駐線程 :
子線程默認(rèn)情況下, 在任務(wù)執(zhí)行結(jié)束后, 線程就會死掉
如果想開啟新的任務(wù), 就要重新創(chuàng)建線程
如果想經(jīng)常在子線程處理一些耗時操作
頻繁的創(chuàng)建線程是不可取的
那么就需要一條不會死掉的常駐線程
上代碼 :
- (void)viewDidLoad {
[super viewDidLoad];
//保住線程的命
self.thread = [[XMGThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
//在子線程讓Runloop跑起來 保住線程的命
- (void)run
{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"------------");
}
在子線程中的 RunLoop 跑起來以后
RunLoop 會一直監(jiān)聽子線程的事件
"------------" 這句是不會打印的
因為 RunLoop 中的do-while循環(huán)一直在運行
需要注意的是 : 線程一定要加一些東西, 例如Port
Port 就是 RunLoop 中的 Source
有了 Source 以后
RunLoop就會一直等待 Source 給他事件
如果不添加的話
RunLoop 中沒有 Source 也沒有 Timer
RunLoop就會自動退出
-
3:子線程添加定時器 :
上代碼 :
- (void)viewDidLoad {
[super viewDidLoad];
//子線程添加定時器
self.thread = [[XMGThread alloc] initWithTarget:self selector:@selector(execute2) object:nil];
[self.thread start];
}
- (void)execute2
{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
和第二種實踐方法一樣
在子線程中啟動 RunLoop
因為 RunLoop 中已經(jīng)有了 Timer
所有 RunLoop 不會死掉
這樣線程不會死, 定時器也正常運行
-
4:自動釋放池 :
作用 :
將一些對象扔到池子中去
當(dāng)池子釋放的時候
讓池中所有對象調(diào)用一次 release 方法
自動釋放池什么時候死 :
在 RunLoop 睡覺之前死
因為睡覺可能睡很久
如果不讓自動釋放池死掉
會占用很多內(nèi)存
當(dāng) RunLoop 被再次喚醒的時候
剛剛所有調(diào)用 release 的對象又會放到釋放池中
在子線程開 RunLoop 的時候
可以添加釋放池 :
//在子線程添加定時器
- (void)execute
{
@autoreleasepool {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}
還有一個問題需要記錄一下
就是當(dāng)舊的釋放池在銷毀以后,新的釋放池什么時候創(chuàng)建呢?
我們可以打印一下主線程 RunLoop 看釋放池都監(jiān)聽了 RunLoop 那些狀態(tài)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
找出相關(guān) autoreleasepool 的部分 :
observers = (
"<CFRunLoopObserver 0x600000134320 [0x10e23a9b0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e3e6ede), context = <CFArray 0x6000004558d0 [0x10e23a9b0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8a7c802048>\n)}}",
"<CFRunLoopObserver 0x6000001343c0 [0x10e23a9b0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e3e6ede), context = <CFArray 0x6000004558d0 [0x10e23a9b0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8a7c802048>\n)}}"
),
autoreleasepool 利用 observers 監(jiān)聽了以下狀態(tài):
activities = 0x1 (相當(dāng)于十進(jìn)制的 1 )
activities = 0xa0 (相當(dāng)于十進(jìn)制的 160)
這是 C 語言的位移枚舉 (位移枚舉的解釋)
這兩個 activities 對應(yīng) RunLoop 以下三個狀態(tài):
kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入
kCFRunLoopBeforeWaiting = (1UL << 5), //即將休眠
kCFRunLoopExit = (1UL << 7), //即將推出
可以看出 autoreeaseepool 利用 observers 監(jiān)聽了RunLoop三個狀態(tài)
所以工作流程應(yīng)該是這樣 :
剛進(jìn)入 RunLoop 的時候創(chuàng)建釋放池
監(jiān)聽到即將進(jìn)入休眠銷毀釋放池釋放變量,并創(chuàng)建新的釋放池以供下次被喚醒時使用
因為 autoreleasepool 沒有監(jiān)聽 RunLoop 即將喚醒的狀態(tài)
所以在休眠之前創(chuàng)建好
但是如果 RunLoop 不被喚醒了 , 最后一次創(chuàng)建的釋放池就不會被銷毀
所以監(jiān)聽 kCFRunLoopExit 狀態(tài)
在最后退出的時候銷毀最后一次創(chuàng)建的釋放吃
感謝閱讀
你的支持是我寫作的唯一動力