DSL與Runloop 分解UI渲染任務(wù)

前言

標(biāo)題里每一個單詞都可以用來長篇闊論一篇文章,我自己是參考了一些資料也才著筆。所以,本文對于一些編程思想或者是底層知識只是淺嘗輒止,反而更加著重于應(yīng)用。通常我們會將耗時操作放到子線程,但是更新UI只能在主線程操作,那么UI耗時操作怎么辦?

本文著重講解通過DSL將編程過程中一個“大”的任務(wù)(比如當(dāng)cell的圖片加載過多過大)細(xì)分成一個個小任務(wù)然后裝到runloop中,解決更新UI的耗時操作問題,在一定程度能夠有效的解決卡頓。

SingletonPattern(單例模式)

demo里面用的單例模式,這里不再贅述單例模式。如果想詳細(xì)了解的話,可以參考我之前寫過的文章。

用單例模式優(yōu)化本地存儲

iOS最實(shí)用的13種設(shè)計(jì)模式

DSL(本文簡單使用鏈?zhǔn)骄幊趟枷?

DSL與鏈?zhǔn)骄幊毯喗?/h4>
  • DSL(Domain Specific Language),特定領(lǐng)域表達(dá)式。在OC中,如果使用Masonry會經(jīng)常寫出類似下面的代碼。如果是Android或者是其它的什么語言,也會有相應(yīng)的表達(dá)方式。如果是基于鏈?zhǔn)骄幊趟枷氲脑挘韵麓a在各個平臺相似。如有雷同,純屬正常。
make.top.equalTo(superview).with.offset(10);
  • 鏈?zhǔn)骄幊趟枷耄菏菍⒍鄠€操作(多行代碼)通過點(diǎn)號(.)鏈接在一起成為一句代碼,使代碼可讀性提高。

  • 鏈?zhǔn)骄幊烫攸c(diǎn):方法的返回值是block,block必須返回對象本身(返回block時,block所在的方法調(diào)用者對象)block的參數(shù)是需要操作的值。

作為一個iOS程序員基本上都應(yīng)該接觸過Masonry這個自動布局庫。這個庫能夠極大程度地簡化自動布局的代碼。使用這個庫讓我感到驚嘆的不是如何能夠?qū)⑤^為復(fù)雜的傳統(tǒng)自動布局寫法精簡到如此程度,而是精簡后的代碼的書寫方式。本文的目的之一便是想將細(xì)分任務(wù)的代碼更加優(yōu)雅。

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

優(yōu)雅的編寫自己的DSL

如何優(yōu)雅地編寫自己的DSL,本文不贅述。不過給大家找到一遍很好的文章,強(qiáng)烈推薦美團(tuán)iOS技術(shù)專家臧成威《如何利用 Objective-C 寫一個精美的 DSL》

本文用到的鏈?zhǔn)秸{(diào)用

WPRunloopTasks.h

typedef void(^RunloopBlock) (void);

/**
 最大任務(wù)數(shù)
 */
@property (nonatomic, assign) NSInteger numOfRunloops;

/**
 鏈?zhǔn)秸{(diào)用添加任務(wù)
 */
@property (nonatomic, copy, readonly) WPRunloopTasks * (^addTask) (RunloopBlock runloopTask);

WPRunloopTasks.m

(具體的實(shí)現(xiàn)細(xì)節(jié)可以忽略,知道這個格式,或者參考相應(yīng)的格式即可)

/**
 鏈?zhǔn)秸{(diào)用添加task
 */
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
    __weak __typeof(&*self)weakSelf = self;
    return ^(RunloopBlock runloopTask) {
        [weakSelf.numOfRunloopTasks addObject:runloopTask];
        //保證之前沒有顯示出來的任務(wù),不再浪費(fèi)時間加載
        if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
            [weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
        }
        return weakSelf;
    };
}

Runloop

RunLoop 的概念

在新建 xcode 生產(chǎn)的工程中有如下代碼塊:

int main(int argc, char * argv[]) {
     @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([YourAppDelegate class]));
    }
}
  • 當(dāng)程序啟動時,以上代碼會被調(diào)用,主線程也隨之開始運(yùn)行,RunLoop 也會隨著啟動.
    在UIApplicationMain()方法里面完成了程序初始化,并設(shè)置程序的Delegate任務(wù),而且隨之開啟主線程的 RunLoop,就開始接受事件處理.

  • RunLoop 是一個循環(huán),在里面它接受線程的輸入,通過事件處理函數(shù)來處理事件.你的代碼中應(yīng)該提供 while or for 循環(huán)來驅(qū)動 runloop.在你的循環(huán)中,用 runloop 對象驅(qū)動事件處理相關(guān)的內(nèi)容,接受事件,并做響應(yīng)的處理.

  • RunLoop 接受的事件源有兩種大類: 異步的input sources, 同步的 Timer sources. 這兩種事件的處理方法,系統(tǒng)所規(guī)定.

  • RunLoop 從以下兩個不同的事件源中接受消息:
    InputSources : 用來投遞異步消息,通常消息來自另外的線程或者程序.在接受到消息并調(diào)用指定的方法時,線程對應(yīng)的 NSRunLoop 對象會通過執(zhí)行 runUntilDate:方法來退出。

    Timer Source: 用來投遞 timer 事件(Schedule 或者 Repeat)中的同步消息。在消息處理時,并不會退出 RunLoop。

    RunLoop 除了處理以上兩種 Input Soruce,它也會在運(yùn)行過程中生成不同的 notifications,標(biāo)識 runloop 所處的狀態(tài),因此可以給 RunLoop 注冊觀察者 Observer,以便監(jiān)控 RunLoop 的運(yùn)行過程,并在 RunLoop 進(jìn)入某些狀態(tài)時候進(jìn)行相應(yīng)的操作(本文即是運(yùn)用這一點(diǎn))。Apple 只提供了 Core Foundation 的 API來給 RunLoop 注冊觀察者Observer.

Runloop的mode

apple暴露的只有以下兩種模式

kCFRunLoopDefaultMode 默認(rèn)模式,一般用于處理timer

kCFRunLoopCommonModes 占位模式(既是默認(rèn)模式又是交互模式,這一點(diǎn)很重要,使用這種模式在默認(rèn)模式和交互模式都可以觸發(fā)。)
注:交互模式默認(rèn)是處理UI事件的。

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // set,非內(nèi)核事件,比如點(diǎn)擊按鈕/屏幕
    CFMutableSetRef _sources1;    // set,系統(tǒng)內(nèi)核事件
    CFMutableArrayRef _observers; // Array,觀察者
    CFMutableArrayRef _timers;    // Array,時鐘
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

Runloop深入理解

有關(guān)runloop的深入理解,推薦ibireme的《深入理解RunLoop》

Runloop總結(jié)

在目前iOS開發(fā)中,幾乎用不到!!但是對于一些高級的功能,我們會涉及到!!

  • 保證程序不退出!!
  • 負(fù)責(zé)監(jiān)聽(處理)所有的事件: 觸摸,時鐘,網(wǎng)絡(luò)事件等等...
  • 負(fù)責(zé)渲染我們的UI,Runloop一次循環(huán)渲染整個界面!!
  • 如果沒有事件發(fā)生,那么"睡覺"

DSL+Runloop

在init方法中創(chuàng)建觀察者,在觀察者的回調(diào)中執(zhí)行任務(wù)并刪除已經(jīng)執(zhí)行的任務(wù)

/**
 添加觀察者
 */
- (void)addRunloopObserver {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    static CFRunLoopObserverRef defaultModeServer;
    
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL,
    };
    
    defaultModeServer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    //運(yùn)行循環(huán) 觀察者 Runloop占位模式
    CFRunLoopAddObserver(runloop, defaultModeServer, kCFRunLoopCommonModes);
    
    CFRelease(defaultModeServer);
}

/**
 回調(diào)函數(shù),一次runloop運(yùn)行一次
 
 @param observer 觀察者
 @param activity 活動
 @param info info
 */
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//這里的info經(jīng)過打印知道是self,所以可以通過info拿到property
    WPRunloopTasks *runloop = (__bridge WPRunloopTasks *)info;
    if(runloop.numOfRunloopTasks.count) {
        //取出任務(wù)
        RunloopBlock task = runloop.numOfRunloopTasks.firstObject;
        //執(zhí)行任務(wù)
        task();
        //干掉第一個任務(wù)
        [runloop.numOfRunloopTasks removeObjectAtIndex:0];
    }
}

鏈?zhǔn)秸{(diào)用添加任務(wù)

/**
 鏈?zhǔn)秸{(diào)用添加task
 */
- (WPRunloopTasks * (^)(RunloopBlock runloopTask))addTask {
    __weak __typeof(&*self)weakSelf = self;
    return ^(RunloopBlock runloopTask) {
        [weakSelf.numOfRunloopTasks addObject:runloopTask];
        //保證之前沒有顯示出來的任務(wù),不再浪費(fèi)時間加載
        if (weakSelf.numOfRunloopTasks.count > weakSelf.numOfRunloops) {
            [weakSelf.numOfRunloopTasks removeObjectAtIndex:0];
        }
        return weakSelf;
    };
}

模擬卡頓

demo中的圖片是3072*2304高清大圖。在渲染的時候,為了更加直觀感受效果,用了0.3s的動畫。每一個cell有3張圖片,屏幕上至少會出現(xiàn)6個cell。先來看一下最后的調(diào)用代碼:

[WPRunloopTasks shareRunloop].addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView1];
        } completion:nil];
    }).addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView2];
        } completion:nil];
    }).addTask(^{
        [UIView transitionWithView:cell.contentView duration:0.3 options:(UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionCrossDissolve) animations:^{
            [cell.contentView addSubview:imageView3];
        } completion:nil];
    });

效果對比

沒有runloop優(yōu)化.gif
runloop優(yōu)化.gif

源碼

runloop demo

總結(jié)

  1. 如果連更新UI耗時的操作都可以優(yōu)化,我想只要是不涉及到更加底層的東西,都是可以優(yōu)化的很好的。本問在“外功”方面已經(jīng)做的可以了,至于“內(nèi)功”比如圖片的解碼問題等等就不是本文的范疇了。
  2. runloop功能比較強(qiáng)大,設(shè)計(jì)到高級功能的應(yīng)該是會用到的。
  3. NSRunloop是對CFRunLoop的封裝,是線程不安全的,而CFRunLoop是線程安全的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,268評論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 1 RunLoop簡介 神秘的RunLoop。一個應(yīng)用開始運(yùn)行以后放在那里,如果不對它進(jìn)行任何操作,這個應(yīng)用就像靜...
    Claire_wu閱讀 1,789評論 3 30
  • 一句經(jīng)常反問自己又時時刻刻在對號入座的話 .可是你有想過你有對得起自己的初心么?人生的路永遠(yuǎn)無法按照自己最開始設(shè)下...
    女漢子心里的萌妹閱讀 725評論 0 0
  • 最近欠下的文越積越多, 感覺心里像壓了塊巨石, 不清不快。 索性就從9月9日小朋友的聚會寫起吧。 說是巧合吧, 本...
    采采卷耳QY閱讀 1,166評論 4 3