上一節主要講了RunLoop的理論的基礎知識, 這一節講一講實踐:
修正一點: 根據源碼,runloop要跑起來先判斷mode是否為空,如果為空退出,
然后判斷source0是否為空,如果為空退出,然后判斷source1是否為空,如果為空退出,然后判斷是否有timer,如果沒有就退出,并沒有判斷是否有observer,所以runloop如果要跑起來,必須有source或者timer的其中一個
源碼如下:
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
CHECK_FOR_FORK();
if (NULL == rlm) return true;
#if DEPLOYMENT_TARGET_WINDOWS
if (0 != rlm->_msgQMask) return false;
#endif
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
struct _block_item *item = rl->_blocks_head;
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
} else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
}
if (doit) return false;
}
return true;
}
1. imageView
如果我們想讓圖片延時加載, 我們一般這樣寫:
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0];
如果界面上有個TextView等滾動的控件, 然后我們一直滾動他, 發現2秒過去,圖片還不加載, 松手后才加載..那么結合上一節的知識, 我們知道performSelector也是默認在runloop的NSDefaultRunLoopMode
模式下
也就是說,上面的代碼寫全其實是:
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];
應用場景: 如果我們在滾動tableView,如果想讓圖片顯示在tableView的imageView上,如果圖片比較大,渲染時間長,那時候就tableView滾動就會比較卡, 所以有的解決方案是:推遲image的顯示,滾動tableView的時候,雖然圖片下載完了,但是圖片暫時不讓它顯示,等手指松開,停止滾動,再顯示圖片
2. 常駐線程
例如:想創建一個子線程,一直在后臺監控用戶的一些行為,所以我們需要創建的這個線程一直不能死
首先我們看看線程是怎么工作的:
先繼承于NSThread, 創建一個我自己的線程(GYThread), 重寫dealloc方法,這樣這個線程如果被銷毀了,我們可以打印監聽到
#import "GYThread.h"
- (void)dealloc
{
NSLog(@"%@-------dealloc",self);
}
我們看看下面線程的執行:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
NSLog(@"----執行任務run----");
}
打印結果如下:
2016-06-19 15:26:29.001 runloopDemo[14322:161704] ----執行任務run----
2016-06-19 15:26:29.003 runloopDemo[14322:161704] <GYThread: 0x7fafc3705490>{number = 2, name = (null)}-------dealloc
2016-06-19 15:26:30.478 runloopDemo[14322:161711] ----執行任務run----
2016-06-19 15:26:30.479 runloopDemo[14322:161711] <GYThread: 0x7fafc3424e40>{number = 3, name = (null)}-------dealloc
則 發現每次執行完任務, Thread就會被dealloc, 而每次開啟內存地址都不同
那我弄一個strong的全局變量記錄這個Thread,不讓他釋放, 每次點擊調用一下線程開始的方法怎么樣? 答案是否定的,第一次點擊完,任務執行完,確實Thread不會被dealloc, 但是點擊第二次讓他直接開啟時,就會崩潰,因為執行完任務,雖然Thread沒有被釋放,還處于內存中,但是它處于消亡狀態, 蘋果不允許線程這樣做..會報錯attempt to start the thread again
(嘗試重新開啟線程)
// 下面這三句代碼是等價的, 這樣runloop跑起來會立刻退出,因為我們還要往runloop中添加observe,timer,source,否則runloop跑起來會立刻退出
// 如果不傳模式,不傳時間,默認為NSDefaultRunLoopMode,過期時間為distantFuture(遙遠的未來,不過期)
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
正確的添加常駐線程的做法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
NSLog(@"----執行任務run----");
// 創建RunLoop,并讓runloop常駐
// 給runloop添加source或timer,才可以讓線程常駐
// 添加port就相當于添加source,事件
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 這句打印就不會執行了
NSLog(@"----任務結束run----");
}
關閉runloop
/* 應用場景:
一直在后臺檢測用戶的行為,掃描用戶的操作,檢查操作,更新操作,檢查聯網狀態
*/
// 如果想退出runloop, 只要關閉這條線程,或者讓runloop中沒有port,source
// 方式一:
[NSThread exit];
// 方式二:
[[NSRunLoop currentRunLoop] removePort:[NSPort port] forMode:NSDefaultRunLoopMode];
奇葩的添加常駐線程的做法(不推薦)
// 在子線程的任務中添加, 想關閉的時候,讓flag=0即可
int flag = 1;
while (flag) {
[[NSRunLoop currentRunLoop] run];
NSLog(@"----runloop退出----");
}
缺點: 上面的代碼會一直打印
----runloop退出----
,說明子線程的runloop一直進入,然后退出,再進入再退出, 因為這個runloop中沒有timer,source的其中任何一個, 只有點擊了給他下達了任務(比如上面的-(void)run方法
, 或者[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
),才會給它一個事件(source),在這個時刻
, 就不會一直打印----runloop退出----
了, 這時候相當于給這個runloop,添加了source,所以這個runloop會進入循環, 就不會停止了,不會退出了
3. 給子線程添加NSTimer
- (void)viewDidLoad {
[super viewDidLoad];
// 給子線程添加NSTimer
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAddTimer) object:nil];
[thread start];
}
// 給子線程添加NSTimer
- (void)threadAddTimer
{
@autoreleasepool {
// 方法一:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
// 添加到當前線程中(子線程)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 當前的runloop中有timer了, 所以這個子線程的runloop可以常駐了,不會退出了
[[NSRunLoop currentRunLoop] run];
// 方法二:
// 這個方法說明NSTimer加入到當前的runloop中的NSDefaultRunLoopMode的模式中,所以再加上一句runloop啟動就和上面的方法一樣了
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] run];
}
}
- (void)addTimer
{
NSLog(@"----這是子線程的定時器----");
}
給子線程添加了NSTimer, 如果我再滑動TableView,則子線程的NSTimer還是正常運行的..這種方式也解決了以前滑動定時器不好使的問題
子線程的定時器的模式跑在NSDefaultRunLoopMode
模式下,
滑動TableView是使主線程跑在了UITrackingRunLoopMode
模式下, 兩個線程影響
4. 自動釋放池
自動釋放池: 將一些對象扔到這個池子中, 當這個池子被釋放的時候, 讓這個池子的所有對象都調用release方法
面試的時候經常會問到自動釋放池什么時候死呢(被釋放呢)?
答案就是: runloop在睡眠之前會被釋放,因為runloop睡眠可能會睡很長時間,時間不定,如果睡眠時間很長,也不讓自動釋放池釋放掉,則內存會堆扎,所以runloop在每次睡覺之前會被清理一次..
在runloop進入下一次循環被喚醒之前,又會創建一個新的釋放池, 中間創建的臨時變量就會放到這個池子中
一個runloop對應一個線程, 所以我們在子線程中創建runloop的時候,最好用創建一個自動釋放池包裹住創建的runloop,如上面的代碼..
因為我們看main.m中 就是用一個自動釋放池包裹住的主線程的runloop, 這是一個安全的做法
說的詳細一點:
5. runloop面試題:
一些面試官會問一些runloop的問題- -!
比如:
-
1.什么是runloop?
- 從字面意思說是: 運行循環, 跑圈
- 其實它的內部是一個高級的do-while循環, 在這個循環內部不斷的處理各種任務(source, timer, observe)
- 一個線程對應一個runloop, 源碼中有一個可變字典,key是線程,value是runloop對象
- 主線程的runloop默認已經啟動,在main函數中, 子線程需要自己手動啟動(調用run方法), 子線程的創建
[NSRunLoop currentRunLoop]
- runloop只能選擇一個模式啟動, 如果想用其他模式,只能退出當前循環,再進入新的模式, 如果當前模式中, 沒有
source
,timer
其中任何一個,那么就直接退出runloop
-
2.在開發中如何使用runloop, 使用場景:
- 開啟一個常駐線程(讓一個子線程不進入消亡狀態, 等待其他線程發來的消息,處理其他事件)
- 在子線程中開啟一個定時器
- 在子線程中長期監控一些行為(比如沙盒的檢測掃描)
- 可以控制定時器在那種模式下運行(Tranking,Default)
- 可以讓某些事件(行為,任務),在特定模式下執行
- 可以添加observe監聽runloop的一些狀態(我們可以在處理所有點擊事件,UI事件之前做一些事情)
- 我們可以自定義源(source)給他發送消息, CFRunLoopSourceCreate(..)函數創建source源 , 這個和
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
比較相似
3.自動釋放池什么時候釋放
自動釋放池釋放的時間和RunLoop的關系:
注意,這里的自動釋放池指的是主線程的自動釋放池,我們看不見它的創建和銷毀。自己手動創建@autoreleasepool {}是根據代碼塊來的,出了這個代碼塊就釋放了。
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush()創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。
在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。
在自己創建線程時,需要手動創建自動釋放池AutoreleasePool