一.前記:
一直知道有Runloop這個東西,但做了不少項目了,卻從來沒有在項目里自己用過,有用到也是系統或者第三方框架.前段時間有幸項目里有用的到的地方.故而研究了幾天,于是記下這篇有關自己理解.
二.先附上代碼:
1.子線程創建RunLoop并執行任務
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runloopMethod) object:nil];
[thread start];
}
- (void)runloopMethod {
NSTimer*timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(actionMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}
///子線程定時器執行的方法
- (void)actionMethod {
//在子線程執行,可以是耗時操作,與界面相關時切換到主線程刷新UI即可
}
2.使用CFRunLoop
static void Callback (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
NSLog(@"Runloop callback");
}
- (void)addObserverByRunloop {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
0,
(__bridge void*)(self),
&CFRetain,
&CFRelease,
NULL
};
static CFRunLoopObserverRef defaultModeObserver;
defaultModeObserver = CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);
CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);
CFRelease(defaultModeObserver);
}
三.說明:
1.NSRunLoop類淺析
1.1 NSRunLoop類用官方文檔(Xcode 8.3)查看,隸屬于Objective-C下Foundation下的一個類。
1.2 第一個重要屬性currentRunLoop
對外是只讀的:
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
文檔的說明,也清晰明了
Returns the run loop for the current thread.
返回當前線程的運行循環
Return Value
The NSRunLoop object for the current thread.
返回的是當前線程的一個NSRunLoop對象
Discussion
If a run loop does not yet exist for the thread, one is created and returned.
如果線程不存在運行循環,則創建并返回一個
應用:我們使用以下語法,能輕松獲取當前線程的RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
1.3 currentMode
聲明:
@property (nullable, readonly, copy) NSRunLoopMode currentMode;
文檔說明:
The receiver's current input mode.
得到當前運行循環的模式
Discussion
The receiver's current input mode. This method returns the current input mode only while the receiver is running; otherwise, it returns nil.
只能獲得已經跑起來的運行循環的模式,否則返回空。
☆☆☆ 重點 ☆☆☆
1.只有主線程的運行循環默認是在跑的,也就是說創建的子線程,運行循環并沒有跑起來。
☆所以在主線程創建定時器,可以正常運行,而在子線程創建定時器,壓根就不跑,原因就是子線程的運行循環并沒有開啟,執行了任務后,子線程直接被釋放了。故而子線程如果不手動開啟運行循環,定時器是失效的!
往事回顧:
項目里一直有用到一個框架,名字叫GCDAsyncSocket
里面有一個方法如下(大約在6202行):
+ (void)listenerThread { @autoreleasepool
{
LogInfo(@"ListenerThread: Started");
// We can't run the run loop unless it has an associated input source or a timer.
// So we'll just create a timer that will never fire - unless the server runs for a decades.
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(ignore:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
LogInfo(@"ListenerThread: Stopped");
}}
之前看到這段代碼時,迷迷糊糊,有點費解,現在不敢說完全懂了,但似乎心中有了一絲明悟
1.4 既然上面提到主線程默認有運行循環,那也應該有個方法可以直接得到主線程的運行循環
聲明:
@property (class, readonly, strong) NSRunLoop *mainRunLoop NS_AVAILABLE(10_5, 2_0);
Returns the run loop of the main thread.
Return Value
An object representing the main thread’s run loop.
得到主線程的運行循環
1.5 可以在運行循環上添加定時器 addTimer:forMode:
聲明:
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
Registers a given timer with a given input mode.
用指定的模式給指定的定時器注冊
其實:可以給定時器不止一種模式的!
1.6 以上說到了模式,這里說明下
☆☆☆ 運行循環一共有五種模式 ☆☆☆
我們能用到的模式只有以下兩種:
-
NSDefaultRunLoopMode 默認模式
-
UITrackingRunLoopMode UI模式
因為模式使用時可以<b>同時使用不同的模式</b>(本質上是不能同時使用的,但使用這種模式時會在兩種模式間切換,所以可以簡單理解為是同時使用),故而產生以下模式:
-
NSRunLoopCommonModes 占位模式,其實就是默認模式和UI模式一起使用。
還有兩種模式是使用不到的:App創建時的模式和內核模式,都由系統調度。
2.CFRunLoop類
2.1 CFRunLoop類的隸屬:CFRunLoop類是Objective-C下Core Foundation的類
2.2 因為目前只用到了一個使用場景,就根據這個使用場景,簡單說明下
- 使用場景:往往大家都知道耗時操作在子線程,UI操作在主線程,而遇到<b>UI操作是耗時操作</b>時就會產生卡頓的感覺(UI操作是耗時操作時,理應放在主線程執行)。
- 卡頓原因:運行循環在一個循環過程中有大量工作要做(耗時),導致下一次循環需要等待!
- 解決方案:給運行循環添加監聽事件,把耗時操作拆分成很多小的任務,在運行循環每一次循環時只執行一個很小的任務。<b> 這樣,耗時操作也在執行,運行循環也沒有卡住(無卡頓感)</b>。
2.3 其實,解決方案很簡單!只需要使用CFRunLoop的CFRunLoopAddObserver就行了!也就是觀察運行循環。不過有個難點是CFRunLoop是C語法。
聲明:
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
這個函數的作用其實就是給運行循環添加一個觀察者對象。
麻煩的是有三個參數,需要弄懂才好填。參數往下看,分開說明。
2.4 參數一:CFRunLoopRef rl
這個參數其實就是要問:你需要給哪個運行循環添加觀察者呢?
我們直接獲得當前運行循環就行了。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
2.5 參數二:CFRunLoopObserverRef observer
這個參數其實就是要問:觀察者是誰呢?
那我們直接創建一個觀察者就行啦。
☆ 注意點:為了避免觀察者意外死亡(過了這個方法,就被釋放了),所以使用靜態變量!
static CFRunLoopObserverRef defaultModeObserver;
有兩個方法供我們創建觀察者:
CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block)(CFRunLoopObserverRef observer, CFRunLoopActivity activity));
創建觀察者我們使用CFRunLoopObserverCreate就行了!第二個是使用Block,差別不大。
臉黑?? 的是創建一個觀察者,有六個參數!!!(這也是盡管功能強大,但使用的人很少的原因之一吧)
<b>創建觀察者參數一:CFAllocatorRef allocator</b>
The allocator to use to allocate memory for the new object. Pass NULL or kCFAllocatorDefault to use the current default allocator.
文檔是以上這么描述的:傳個NULL或者kCFAllocatorDefault就行了,我們點進去查看kCFAllocatorDefault是什么的時候,文檔說:This is a synonym for NULL. 意思是:kCFAllocatorDefault其實就是NULL。蘋果這么玩,深深的無奈!
<b>創建觀察者參數二:CFOptionFlags activities</b>
Set of flags identifying the activity stages of the run loop during which the observer should be called. See CFRunLoopActivityfor the list of stages. To have the observer called at multiple stages in the run loop, combine the CFRunLoopActivity values using the bitwise-OR operator.
上述文檔意思就是問:你要觀察運行循環的哪個階段(CFRunLoopActivity)?
無奈,繼續查看CFRunLoopActivity是什么鬼,聲明如下:
typedef enum CFRunLoopActivity : CFOptionFlags {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
} CFRunLoopActivity;
- kCFRunLoopEntry:
The entrance of the run loop, before entering the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.
簡言之:就是運行循環的入口
- kCFRunLoopBeforeTimers:
Inside the event processing loop before any timers are processed.
意思是:在事件處理循環定時器處理之前。
- kCFRunLoopBeforeSources:
Inside the event processing loop before any sources are processed.
意思是:在事件處理循環之前處理來源。
- kCFRunLoopBeforeWaiting:
Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. It also does not occur in a particular iteration of the event processing loop if a version 0 source fires.
意思是:運行循環在等待之前。
- kCFRunLoopAfterWaiting:
Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. This activity occurs only if the run loop did in fact go to sleep during the current loop.
意思是:運行循環在等待之后。
- kCFRunLoopExit:
The exit of the run loop, after exiting the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.
意思是:運行循環退出了。
稍微分析一下,我們是要處理耗時操作的,選擇kCFRunLoopBeforeWaiting看起來比較合適,每當運行循環要休息的時候,就給它點事情做做。
<b>創建觀察者參數三:Boolean repeats</b>
終于逮著一個簡單的參數:問是否需要重復?
毫不猶豫回答:YES
<b>創建觀察者參數四:CFIndex order</b>
A priority index indicating the order in which run loop observers are processed. When multiple run loop observers are scheduled in the same activity stage in a given run loop mode, the observers are processed in increasing order of this parameter. Pass 0 unless there is a reason to do otherwise.
上述文檔說明看得有點暈,只能看懂一個大概,結合查閱資料以及結合翻譯。個人的簡單理解就是:需要填寫一個優先級,填0基本是就是不執行了,數值越高,優先級越高!擔憂填最高的優先級會出現意外,所以選擇一個穩妥點的方式:NSInteger的最大值然后減去一個999
于是準備填:NSIntegerMax - 999
<b>創建觀察者參數五:CFRunLoopObserverCallBack callout</b>
回調:需要真實執行的任務,就靠這個回調了!
The callback function invoked when the observer runs.
改怎么寫這個回調函數呢?只好點進去看聲明(因為是C語法,之前沒看聲明,所以遇到了C語法無法和OC語法需要執行的任務無法順利轉換的問題):
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
又是三個參數-_-!
前兩個解釋過了,第三個,是這整個CFRunLoop核心中的核心,因為void *info其實就是OC語法中的id info
<b>創建觀察者參數六:CFRunLoopObserverContext *context</b>
CFRunLoopObserverContext:
A structure that contains program-defined data and callbacks with which you can configure a CFRunLoopObserver object’s behavior.
這個CFRunLoopObserverContext是一個結構體:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;
以上void * info是關鍵!
既然這個結構體中有一個void *也就是任意類型,那我們就可以完成與OC的橋梁了。
結構體其余參數簡述:CFIndex version,顯然說的是版本。第三個第四個參數,看到retain和release就知道是和內存相關的了。第五個參數看到一個單詞Description,那就是描述的字符串吧。
那這個完整的結構體就清晰了。我寫的代碼如下:
CFRunLoopObserverContext context = {
0,
(__bridge void*)(self),
&CFRetain,
&CFRelease,
NULL
};
版本,直接填了0
因為C與OC轉換需要__bridge橋接一下,因為可以接收任意類型,我直接把self也就是控制器傳過去
CF內存CFRetain
CF內存釋放CFRelease
描述直接給個NULL
至此,觀察者所有參數填完也就創建好了。
static CFRunLoopObserverRef defaultModeObserver;
defaultModeObserver =CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);
2.6 參數三:CFRunLoopMode mode
模式:kCFRunLoopCommonModes
綜上所述:添加觀察者也就完成了
CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);
因為是C語法,需要手動釋放內存!
CFRelease(defaultModeObserver);
總結以上所有代碼,寫出來如下:
static void Callback (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
NSLog(@"Runloop callback");
}
- (void)addObserverByRunloop {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
0,
(__bridge void*)(self),
&CFRetain,
&CFRelease,
NULL
};
static CFRunLoopObserverRef defaultModeObserver;
defaultModeObserver = CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);
CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);
CFRelease(defaultModeObserver);
}