基于Runloop的任務管理器

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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市窖贤,隨后出現(xiàn)的幾起案子砖顷,更是在濱河造成了極大的恐慌贰锁,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滤蝠,死亡現(xiàn)場離奇詭異豌熄,居然都是意外死亡,警方通過查閱死者的電腦和手機物咳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門锣险,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人览闰,你說我怎么就攤上這事芯肤。” “怎么了压鉴?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵崖咨,是天一觀的道長。 經(jīng)常有香客問我晴弃,道長掩幢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任上鞠,我火速辦了婚禮际邻,結果婚禮上,老公的妹妹穿的比我還像新娘芍阎。我一直安慰自己世曾,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布谴咸。 她就那樣靜靜地躺著轮听,像睡著了一般。 火紅的嫁衣襯著肌膚如雪岭佳。 梳的紋絲不亂的頭發(fā)上血巍,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音珊随,去河邊找鬼述寡。 笑死,一個胖子當著我的面吹牛叶洞,可吹牛的內(nèi)容都是我干的鲫凶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼衩辟,長吁一口氣:“原來是場噩夢啊……” “哼螟炫!你這毒婦竟也來了?” 一聲冷哼從身側響起艺晴,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤昼钻,失蹤者是張志新(化名)和其女友劉穎掸屡,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體然评,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡折晦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了沾瓦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡谦炒,死狀恐怖贯莺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宁改,我是刑警寧澤缕探,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站还蹲,受9級特大地震影響爹耗,放射性物質發(fā)生泄漏。R本人自食惡果不足惜谜喊,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一潭兽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斗遏,春花似錦山卦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逾一,卻和暖如春铸本,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背遵堵。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工箱玷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鄙早。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓汪茧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親限番。 傳聞我的和親對象是個殘疾皇子舱污,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348