1、工具背景
- 在性能優(yōu)化的過程中,除了方法耗時優(yōu)化,任務調度執(zhí)行的時機也很重要,有些任務必須要在主線程執(zhí)行,有些任務可以在子線程執(zhí)行,不論是主線程任務還是子線程任務,我們都需要合理管理我們?nèi)蝿請?zhí)行的時機
- 在主線程執(zhí)行的任務執(zhí)行時機過早,那就會導致后面的任務等待,在啟動優(yōu)化惹谐、卡頓優(yōu)化過程中,這些任務比較多
- 在子線程的任務,我們不能一味的開啟子線程操作,線程越多,內(nèi)存資源,cpu資源也會越多
2、實現(xiàn)機制
- 針對上述問題,在恰當?shù)臅r機觸發(fā)任務執(zhí)行顯得尤為重要,一般情況下針,對于任務調度時機我們都是在代碼流程或者延時操作處理,但是這樣不僅工作量繁瑣,而且效果也不太好
- iOS的Runloop用于管理App的運行狀態(tài),并且擁有良好的狀態(tài)切換機制,當runloop沒事干的時候會進入waiting狀態(tài),而且runloop API提供了狀態(tài)切換監(jiān)聽方法,在觸發(fā)
kCFRunLoopBeforeWaiting
時,在回調里喚醒runloop執(zhí)行任務
- 代碼部分
在一些第三方框架或者一些runloop的高級使用方案中,我們也不乏看見作者會添加runloop的監(jiān)聽,這里貼上我對主線程runloop的監(jiān)聽代碼:
- (void)addRunloopObserver {
CFRunLoopObserverContext context = {
0,
( __bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//監(jiān)聽runloop即將進入睡眠狀態(tài)
static CFRunLoopObserverRef defaultModeObsever;
defaultModeObsever = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting,
YES,
NSIntegerMax,
&Callback,
&context
);
//添加RunLoop的觀察者
CFRunLoopAddObserver(CFRunLoopGetMain(), defaultModeObsever, kCFRunLoopCommonModes);
CFRelease(defaultModeObsever);
}
-
Callback
為runloop狀態(tài)發(fā)生變化后的回調方法
-
CFRunLoopGetMain()
這里添加的監(jiān)聽是MainRunloop:
-
kCFRunLoopCommonModes
這里的原理同NSTimer添加到CommonModes一樣,這樣同樣可以監(jiān)聽runloop在TrackingMode下的變化
3、實現(xiàn)步驟
- 將需要管理的任務直接調用工具add方法添加進來,需要在子線程中執(zhí)行的任務,需要把觸發(fā)子線程的操作也算包含在task中
- 添加進來后要考慮采用什么數(shù)據(jù)類型去管理task,這里參考了SDWebImage的MemoryCache緩存原理,創(chuàng)建一個
NSMapTable
進行target和task的管理,這里傳入target主要是考慮到某些task在后續(xù)會取消,NSMapTable的Key對應target,指針類型是weak,這樣不會造成target的引用計數(shù)增加,而且當target釋放的時候,對應的task也可自動移除
- 一個Target可能不止對應一個task,所以
NSMapTable
的value我采用的是一個可變字典,key用于標識一個task,value為task實體,key的生成有兩種:
- 外部傳入
uniqueKey
,這樣在某些target重復利用的過程中可以防止相同任務的重復添加,避免錯誤的task執(zhí)行
- 自動生成的key,由Tool自定義生成規(guī)則,這里我才用task自增長id和字符串拼接的方式,可以添加多個任務
- 代碼部分
+ (void)addTask:(void(^)(void))task {
[self addTarget:nil task:task];
}
+ (void)addTarget:(id)target task:(void(^)(void))task {
[self addTarget:target uniqueKey:nil task:task];
}
//添加任務
+ (void)addTarget:(id)target uniqueKey:(NSString *)uniqueKey task:(void(^)(void))task {
if (![NSThread isMainThread]) {
//在子線程中添加的任務,最終執(zhí)行的時候不是在原來的子線程,為了避免不必要的線程處理問題,請在mainThread中添加task
return;
}
GLRunloopTaskTool *taskTool = [GLRunloopTaskTool shareInstance];
//如果target存在task,查看是否需要檢查uniqueKey
id tar = target ? : taskTool;
NSMutableDictionary *taskInfo = [taskTool.taskCache objectForKey:tar];
if (!taskInfo) {
taskInfo = [NSMutableDictionary dictionary];
[taskTool.taskCache setObject:taskInfo forKey:tar];
}
if (uniqueKey) {
[taskInfo setObject:task forKey:uniqueKey];
} else {
NSString *key = [NSString stringWithFormat:@"gl_runloop_task_tool_value_%d",taskTool.increase_Index % INT_MAX];
[taskInfo setObject:task forKey:key];
}
taskTool.increase_Index++;
}
+ (void)removeTasks:(id)target {
GLRunloopTaskTool *taskTool = [GLRunloopTaskTool shareInstance];
id tar = target ? : taskTool;
[taskTool.taskCache removeObjectForKey:tar];
}
+ (void)removeAllTasks {
[[GLRunloopTaskTool shareInstance].taskCache removeAllObjects];
}
4、Observer回調/Task執(zhí)行
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
GLRunloopTaskTool *taskTool = (__bridge GLRunloopTaskTool *)(info);
if (taskTool.taskCache.count) {
NSLog(@"start task");
id target = taskTool.taskCache.keyEnumerator.allObjects.firstObject;
NSLog(@"target = %@",target);
NSMutableDictionary *taskInfo = [taskTool.taskCache objectForKey:target];
NSLog(@"taskInfo start = %@",taskInfo);
NSString *taskKey = taskInfo.allKeys.firstObject;
void(^task)(void) = [taskInfo objectForKey:taskKey];
if (task) {
//執(zhí)行任務
task();
NSLog(@"excute task=%@",task);
}
[taskInfo removeObjectForKey:taskKey];
NSLog(@"taskInfo end = %@",taskInfo);
if (taskInfo.count == 0) {
[taskTool.taskCache removeObjectForKey:target];
NSLog(@"taskInfo.count== %lu,taskCache.count=%lu",(unsigned long)taskInfo.count,(unsigned long)taskTool.taskCache.count);
}
if (taskTool.taskCache.count) {
CFRunLoopWakeUp(CFRunLoopGetMain());
}
}
}
- 獲取當前的runloop狀態(tài),如果activity為
kCFRunLoopBeforeWaiting
,即表示runloop即將進入空閑狀態(tài),準備休息了,這個時候如果有活要干,這是時機最好不過了,在上面添加監(jiān)聽的方法中,也可以只監(jiān)聽kCFRunLoopBeforeWaiting
狀態(tài),這樣在回調里不需要判斷狀態(tài)是否為kCFRunLoopBeforeWaiting
- 當需要執(zhí)行任務的時候,runloop會被喚醒,
狀態(tài)切換為
kCFRunLoopAfterWaiting`,然后去執(zhí)行task
- 為了避免還有任務未執(zhí)行完,我在執(zhí)行任務時通過
CFRunLoopWakeUp(CFRunLoopGetMain());
手動喚醒runloop,這樣可以確保runloop喚醒,如果不手動喚醒在執(zhí)行某些任務時可能不會觸發(fā)Runloop的喚醒操作,這樣剩下的task就無法執(zhí)行
- task執(zhí)行后,從對應的target移除,如果target待執(zhí)行任務為0,那么從cache中刪除管理的target
改進的地方晕城、有疑問的地方盡情留言
Demo地址
最后編輯于 :
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者