上一節(jié)主要講了RunLoop的理論的基礎(chǔ)知識, 這一節(jié)講一講實踐:
修正一點: 根據(jù)源碼,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等滾動的控件, 然后我們一直滾動他, 發(fā)現(xiàn)2秒過去,圖片還不加載, 松手后才加載..那么結(jié)合上一節(jié)的知識, 我們知道performSelector也是默認(rèn)在runloop的NSDefaultRunLoopMode
模式下
也就是說,上面的代碼寫全其實是:
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];
應(yīng)用場景: 如果我們在滾動tableView,如果想讓圖片顯示在tableView的imageView上,如果圖片比較大,渲染時間長,那時候就tableView滾動就會比較卡, 所以有的解決方案是:推遲image的顯示,滾動tableView的時候,雖然圖片下載完了,但是圖片暫時不讓它顯示,等手指松開,停止?jié)L動,再顯示圖片
2. 常駐線程
例如:想創(chuàng)建一個子線程,一直在后臺監(jiān)控用戶的一些行為,所以我們需要創(chuàng)建的這個線程一直不能死
首先我們看看線程是怎么工作的:
先繼承于NSThread, 創(chuàng)建一個我自己的線程(GYThread), 重寫dealloc方法,這樣這個線程如果被銷毀了,我們可以打印監(jiān)聽到
#import "GYThread.h"
- (void)dealloc
{
NSLog(@"%@-------dealloc",self);
}
我們看看下面線程的執(zhí)行:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
NSLog(@"----執(zhí)行任務(wù)run----");
}
打印結(jié)果如下:
2016-06-19 15:26:29.001 runloopDemo[14322:161704] ----執(zhí)行任務(wù)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] ----執(zhí)行任務(wù)run----
2016-06-19 15:26:30.479 runloopDemo[14322:161711] <GYThread: 0x7fafc3424e40>{number = 3, name = (null)}-------dealloc
則 發(fā)現(xiàn)每次執(zhí)行完任務(wù), Thread就會被dealloc, 而每次開啟內(nèi)存地址都不同
那我弄一個strong的全局變量記錄這個Thread,不讓他釋放, 每次點擊調(diào)用一下線程開始的方法怎么樣? 答案是否定的,第一次點擊完,任務(wù)執(zhí)行完,確實Thread不會被dealloc, 但是點擊第二次讓他直接開啟時,就會崩潰,因為執(zhí)行完任務(wù),雖然Thread沒有被釋放,還處于內(nèi)存中,但是它處于消亡狀態(tài), 蘋果不允許線程這樣做..會報錯attempt to start the thread again
(嘗試重新開啟線程)
// 下面這三句代碼是等價的, 這樣runloop跑起來會立刻退出,因為我們還要往runloop中添加observe,timer,source,否則runloop跑起來會立刻退出
// 如果不傳模式,不傳時間,默認(rèn)為NSDefaultRunLoopMode,過期時間為distantFuture(遙遠(yuǎn)的未來,不過期)
[[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(@"----執(zhí)行任務(wù)run----");
// 創(chuàng)建RunLoop,并讓runloop常駐
// 給runloop添加source或timer,才可以讓線程常駐
// 添加port就相當(dāng)于添加source,事件
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 這句打印就不會執(zhí)行了
NSLog(@"----任務(wù)結(jié)束run----");
}
關(guān)閉runloop
/* 應(yīng)用場景:
一直在后臺檢測用戶的行為,掃描用戶的操作,檢查操作,更新操作,檢查聯(lián)網(wǎng)狀態(tài)
*/
// 如果想退出runloop, 只要關(guān)閉這條線程,或者讓runloop中沒有port,source
// 方式一:
[NSThread exit];
// 方式二:
[[NSRunLoop currentRunLoop] removePort:[NSPort port] forMode:NSDefaultRunLoopMode];
奇葩的添加常駐線程的做法(不推薦)
// 在子線程的任務(wù)中添加, 想關(guān)閉的時候,讓flag=0即可
int flag = 1;
while (flag) {
[[NSRunLoop currentRunLoop] run];
NSLog(@"----runloop退出----");
}
缺點: 上面的代碼會一直打印
----runloop退出----
,說明子線程的runloop一直進入,然后退出,再進入再退出, 因為這個runloop中沒有timer,source的其中任何一個, 只有點擊了給他下達了任務(wù)(比如上面的-(void)run方法
, 或者[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
),才會給它一個事件(source),在這個時刻
, 就不會一直打印----runloop退出----
了, 這時候相當(dāng)于給這個runloop,添加了source,所以這個runloop會進入循環(huán), 就不會停止了,不會退出了
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];
// 添加到當(dāng)前線程中(子線程)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 當(dāng)前的runloop中有timer了, 所以這個子線程的runloop可以常駐了,不會退出了
[[NSRunLoop currentRunLoop] run];
// 方法二:
// 這個方法說明NSTimer加入到當(dāng)前的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. 自動釋放池
自動釋放池: 將一些對象扔到這個池子中, 當(dāng)這個池子被釋放的時候, 讓這個池子的所有對象都調(diào)用release方法
面試的時候經(jīng)常會問到自動釋放池什么時候死呢(被釋放呢)?
答案就是: runloop在睡眠之前會被釋放,因為runloop睡眠可能會睡很長時間,時間不定,如果睡眠時間很長,也不讓自動釋放池釋放掉,則內(nèi)存會堆扎,所以runloop在每次睡覺之前會被清理一次..
在runloop進入下一次循環(huán)被喚醒之前,又會創(chuàng)建一個新的釋放池, 中間創(chuàng)建的臨時變量就會放到這個池子中
一個runloop對應(yīng)一個線程, 所以我們在子線程中創(chuàng)建runloop的時候,最好用創(chuàng)建一個自動釋放池包裹住創(chuàng)建的runloop,如上面的代碼..
因為我們看main.m中 就是用一個自動釋放池包裹住的主線程的runloop, 這是一個安全的做法
說的詳細(xì)一點:
5. runloop面試題:
一些面試官會問一些runloop的問題- -!
比如:
-
1.什么是runloop?
- 從字面意思說是: 運行循環(huán), 跑圈
- 其實它的內(nèi)部是一個高級的do-while循環(huán), 在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(source, timer, observe)
- 一個線程對應(yīng)一個runloop, 源碼中有一個可變字典,key是線程,value是runloop對象
- 主線程的runloop默認(rèn)已經(jīng)啟動,在main函數(shù)中, 子線程需要自己手動啟動(調(diào)用run方法), 子線程的創(chuàng)建
[NSRunLoop currentRunLoop]
- runloop只能選擇一個模式啟動, 如果想用其他模式,只能退出當(dāng)前循環(huán),再進入新的模式, 如果當(dāng)前模式中, 沒有
source
,timer
其中任何一個,那么就直接退出runloop
-
2.在開發(fā)中如何使用runloop, 使用場景:
- 開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài), 等待其他線程發(fā)來的消息,處理其他事件)
- 在子線程中開啟一個定時器
- 在子線程中長期監(jiān)控一些行為(比如沙盒的檢測掃描)
- 可以控制定時器在那種模式下運行(Tranking,Default)
- 可以讓某些事件(行為,任務(wù)),在特定模式下執(zhí)行
- 可以添加observe監(jiān)聽runloop的一些狀態(tài)(我們可以在處理所有點擊事件,UI事件之前做一些事情)
- 我們可以自定義源(source)給他發(fā)送消息, CFRunLoopSourceCreate(..)函數(shù)創(chuàng)建source源 , 這個和
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
比較相似
3.自動釋放池什么時候釋放
自動釋放池釋放的時間和RunLoop的關(guān)系:
注意,這里的自動釋放池指的是主線程的自動釋放池,我們看不見它的創(chuàng)建和銷毀。自己手動創(chuàng)建@autoreleasepool {}是根據(jù)代碼塊來的,出了這個代碼塊就釋放了。
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準(zhǔn)備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。
在自己創(chuàng)建線程時,需要手動創(chuàng)建自動釋放池AutoreleasePool