三十一、ios界面優(yōu)化

資料來(lái)源iOS 保持界面流暢的技巧

屏幕顯示圖像的原理

image

從過(guò)去的 CRT 顯示器原理說(shuō)起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過(guò)程和系統(tǒng)的視頻控制器進(jìn)行同步,顯示器(或者其他硬件)會(huì)用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號(hào)。當(dāng)電子槍換到新的一行,準(zhǔn)備進(jìn)行掃描時(shí),顯示器會(huì)發(fā)出一個(gè)水平同步信號(hào)(horizonal synchronization),簡(jiǎn)稱 HSync而當(dāng)一幀畫面繪制完成后,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization),簡(jiǎn)稱 VSync。顯示器通常以固定頻率進(jìn)行刷新,這個(gè)刷新率就是VSync信號(hào)產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏,但原理仍然沒(méi)有變。

image

通常來(lái)說(shuō),計(jì)算機(jī)系統(tǒng)中 CPU、GPU、顯示器是以上面這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會(huì)按照 VSync 信號(hào)逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過(guò)可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。

卡頓產(chǎn)生的原因和解決方案

image

在 VSync 信號(hào)到來(lái)后,系統(tǒng)圖形服務(wù)會(huì)通過(guò)CADisplayLink(用于同步屏幕刷新頻率的計(jì)時(shí)器)等機(jī)制通知 App,App 主線程開(kāi)始在 CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去,由 GPU 進(jìn)行變換、合成、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒(méi)有完成內(nèi)容提交,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。這就是界面卡頓的原因。

垂直同步時(shí)間16.67ms,60幀就是1s。

CPU 和 GPU 不論哪個(gè)阻礙了顯示流程,都會(huì)造成掉幀現(xiàn)象。所以開(kāi)發(fā)時(shí),也需要分別對(duì) CPU 和 GPU 壓力進(jìn)行評(píng)估和優(yōu)化。

卡頓檢測(cè)

1. YYFPSLabel

@implementation YYFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
    
    NSTimeInterval _llll;
}

    _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;
    
    CGFloat progress = fps / 60.0;
    UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
    
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
    [text setColor:color range:NSMakeRange(0, text.length - 3)];
    [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
    text.font = _font;
    [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
    
    self.attributedText = text;
}

通過(guò)CADisplayLink類,每次屏幕刷新count++,再除以記錄時(shí)間書暗處fps。

2. RunLoop檢測(cè)


@interface LGBlockMonitor (){
    CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation LGBlockMonitor

+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)start{
    [self registerObserver];
    [self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
    monitor->activity = activity;
    // 發(fā)送信號(hào)
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 優(yōu)先級(jí)最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
    // 創(chuàng)建信號(hào)
    _semaphore = dispatch_semaphore_create(0);
    // 在子線程監(jiān)控時(shí)長(zhǎng)
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            // 超時(shí)時(shí)間是 1 秒,沒(méi)有等到信號(hào)量,st 就不等于 0, RunLoop 所有的任務(wù)
            long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
            if (st != 0)
            {
                if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                {
                    if (++self->_timeoutCount < 2){
                        NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                        continue;
                    }
                    // 一秒左右的衡量尺度 很大可能性連續(xù)來(lái) 避免大規(guī)模打印!
                    NSLog(@"檢測(cè)到超過(guò)兩次連續(xù)卡頓");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

創(chuàng)建個(gè)runloop觀察者,在回調(diào)中發(fā)送信號(hào)量,在子線程接受信號(hào)量,無(wú)線循環(huán)等待,間隔一秒接受信號(hào)量,檢測(cè)到事務(wù)執(zhí)行_timeoutCount++,超過(guò)指定時(shí)間打印。

3.微信卡頓檢測(cè)matrix

4.滴滴卡頓檢測(cè) DoraemonKit

CPU 資源消耗原因和解決方案

對(duì)象創(chuàng)建

對(duì)象的創(chuàng)建會(huì)分配內(nèi)存、調(diào)整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對(duì)象代替重量的對(duì)象,可以對(duì)性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應(yīng)觸摸事件的控件,用 CALayer 顯示會(huì)更加合適。如果對(duì)象不涉及 UI 操作,則盡量放到后臺(tái)線程去創(chuàng)建,但可惜的是包含有 CALayer 的控件,都只能在主線程創(chuàng)建和操作。通過(guò) Storyboard 創(chuàng)建視圖對(duì)象時(shí),其資源消耗會(huì)比直接通過(guò)代碼創(chuàng)建對(duì)象要大非常多,在性能敏感的界面里,Storyboard 并不是一個(gè)好的技術(shù)選擇。

盡量推遲對(duì)象創(chuàng)建的時(shí)間,并把對(duì)象的創(chuàng)建分散到多個(gè)任務(wù)中去,懶加載是個(gè)不錯(cuò)的選擇。盡管這實(shí)現(xiàn)起來(lái)比較麻煩,并且?guī)?lái)的優(yōu)勢(shì)并不多,但如果有能力做,還是要盡量嘗試一下。如果對(duì)象可以復(fù)用,并且復(fù)用的代價(jià)比釋放、創(chuàng)建新對(duì)象要小,那么這類對(duì)象應(yīng)當(dāng)盡量放到一個(gè)緩存池里復(fù)用。

對(duì)象調(diào)整

對(duì)象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方。這里特別說(shuō)一下 CALayer:CALayer 內(nèi)部并沒(méi)有屬性,當(dāng)調(diào)用屬性方法時(shí),它內(nèi)部是通過(guò)運(yùn)行時(shí) resolveInstanceMethod 為對(duì)象臨時(shí)添加一個(gè)方法,并把對(duì)應(yīng)屬性值保存到內(nèi)部的一個(gè) Dictionary 里,同時(shí)還會(huì)通知 delegate、創(chuàng)建動(dòng)畫等等,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實(shí)際上都是 CALayer 屬性映射來(lái)的,所以對(duì) UIView 的這些屬性進(jìn)行調(diào)整時(shí),消耗的資源要遠(yuǎn)大于一般的屬性。對(duì)此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改:例如無(wú)關(guān)鍵要的frame變化

當(dāng)視圖層次調(diào)整時(shí),UIView、CALayer 之間會(huì)出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時(shí),應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖

對(duì)象銷毀

對(duì)象的銷毀雖然消耗資源不多,但累積起來(lái)也是不容忽視的。通常當(dāng)容器類持有大量對(duì)象時(shí),其銷毀時(shí)的資源消耗就非常明顯。同樣的,如果對(duì)象可以放到后臺(tái)線程去釋放,那就挪到后臺(tái)線程去。例如:把對(duì)象捕獲到 block 中,然后扔到后臺(tái)隊(duì)列去隨便發(fā)送個(gè)消息以避免編譯器警告,就可以讓對(duì)象在后臺(tái)線程銷毀了。

NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
    [tmp class];
});

布局計(jì)算

視圖布局的計(jì)算是 App 中最為常見(jiàn)的消耗 CPU 資源的地方。如果能在后臺(tái)線程提前計(jì)算好視圖布局、并且對(duì)視圖布局進(jìn)行緩存,那么這個(gè)地方基本就不會(huì)產(chǎn)生性能問(wèn)題了。

不論通過(guò)何種技術(shù)對(duì)視圖進(jìn)行布局,其最終都會(huì)落到對(duì) UIView.frame/bounds/center 等屬性的調(diào)整上。上面也說(shuō)過(guò),對(duì)這些屬性的調(diào)整非常消耗資源,所以盡量提前計(jì)算好布局,在需要時(shí)一次性調(diào)整好對(duì)應(yīng)屬性,而不要多次、頻繁的計(jì)算和調(diào)整這些屬性。

Autolayout

Autolayout是蘋果本身提倡的技術(shù)在大部分情況下也能很好的提升開(kāi)發(fā)效率,但是Autolayout對(duì)于復(fù)雜視圖來(lái)說(shuō)常常會(huì)產(chǎn)生嚴(yán)重的性能問(wèn)題。隨著視圖數(shù)量的增長(zhǎng),Autolayout 帶來(lái)的 CPU 消耗會(huì)呈指數(shù)級(jí)上升。 如果你不想手動(dòng)調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見(jiàn)的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。

文本計(jì)算

如果一個(gè)界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計(jì)算會(huì)占用很大一部分資源,并且不可避免。如果對(duì)文本顯示沒(méi)有特殊要求,可以參考下 UILabel 內(nèi)部的實(shí)現(xiàn)方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來(lái)計(jì)算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來(lái)繪制文本。盡管這兩個(gè)方法性能不錯(cuò),但仍舊需要放到后臺(tái)線程進(jìn)行以避免阻塞主線程。

文本渲染

屏幕上能看到的所有文本內(nèi)容控件,包括UIWebView,在底層都是通過(guò)CoreText排版、繪制為Bitmap顯示的。常見(jiàn)的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進(jìn)行的,當(dāng)顯示大量文本時(shí),CPU 的壓力會(huì)非常大。對(duì)此解決方案只有一個(gè),那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對(duì)文本異步繪制。盡管這實(shí)現(xiàn)起來(lái)非常麻煩,但其帶來(lái)的優(yōu)勢(shì)也非常大,CoreText 對(duì)象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整 UILabel 大小時(shí)算一遍、UILabel 繪制時(shí)內(nèi)部再算一遍);CoreText 對(duì)象占用內(nèi)存較少,可以緩存下來(lái)以備稍后多次渲染。

圖片的解碼

用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí),圖片數(shù)據(jù)并不會(huì)立刻解碼。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會(huì)得到解碼。這一步是發(fā)生在主線程的,并且不可避免。如果想要繞開(kāi)這個(gè)機(jī)制,常見(jiàn)的做法是在后臺(tái)線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創(chuàng)建圖片。目前常見(jiàn)的網(wǎng)絡(luò)圖片庫(kù)都自帶這個(gè)功能。

圖像的繪制

圖像的繪制通常是指用那些以CG開(kāi)頭的方法把圖像繪制到畫布中,然后從畫布創(chuàng)建圖片并顯示這樣一個(gè)過(guò)程。這個(gè)最常見(jiàn)的地方就是[UIView drawRect:]里面了。由于CoreGraphic方法通常都是線程安全的,所以圖像的繪制就可以很容易的放到后臺(tái)線程進(jìn)行。一個(gè)簡(jiǎn)單的異步繪制的過(guò)程大致如下(實(shí)際情況會(huì)比這個(gè)復(fù)雜得多,但原理基本一致):

- (void)display {
    dispatch_async(backgroundQueue, ^{
        CGContextRef ctx = CGBitmapContextCreate(...);
        // draw in context...
        CGImageRef img = CGBitmapContextCreateImage(ctx);
        CFRelease(ctx);
        dispatch_async(mainQueue, ^{
            layer.contents = img;
        });
    });
}

GPU 資源消耗原因和解決方案

GPU能干的事情比較單一;接收提交的紋理(Texture)和頂點(diǎn)描述(三角形),應(yīng)用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內(nèi)容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。

紋理的渲染

所有的 Bitmap,包括圖片、文本、柵格化的內(nèi)容,最終都要由內(nèi)存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過(guò)程,還是GPU調(diào)整和渲染Texture的過(guò)程,都要消耗不少GPU資源。當(dāng)在較短時(shí)間顯示大量圖片時(shí)(比如 TableView 存在非常多的圖片并且快速滑動(dòng)時(shí)),CPU占用率很低,GPU占用非常高,界面仍然會(huì)掉幀。避免這種情況的方法只能是盡量減少在短時(shí)間內(nèi)大量圖片的顯示,盡可能將多張圖片合成為一張進(jìn)行顯示。

當(dāng)圖片過(guò)大,超過(guò) GPU 的最大紋理尺寸時(shí),圖片需要先由 CPU 進(jìn)行預(yù)處理,這對(duì) CPU 和 GPU 都會(huì)帶來(lái)額外的資源消耗。目前來(lái)說(shuō),iPhone 4S 以上機(jī)型,紋理尺寸上限都是 4096×4096,所以盡量不要讓圖片和視圖的大小超過(guò)這個(gè)值。

視圖的混合(Composing)

當(dāng)多個(gè)視圖(或者說(shuō) CALayer)重疊在一起顯示時(shí),GPU 會(huì)首先把他們混合到一起。如果視圖結(jié)構(gòu)過(guò)于復(fù)雜,混合的過(guò)程也會(huì)消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應(yīng)用應(yīng)當(dāng)盡量減少視圖數(shù)量和層次,并在不透明的視圖里標(biāo)明 opaque 屬性以避免無(wú)用的 Alpha 通道合成。當(dāng)然,這也可以用上面的方法,把多個(gè)視圖預(yù)先渲染為一張圖片來(lái)顯示。

圖形的生成

CALayer的border、圓角、陰影、遮罩(mask),CASharpLayer的矢量圖形顯示,通常會(huì)觸發(fā)離屏渲染(offscreen rendering),而離屏渲染通常發(fā)生在 GPU 中。當(dāng)一個(gè)列表視圖中出現(xiàn)大量圓角的 CALayer,并且快速滑動(dòng)時(shí),可以觀察到 GPU 資源已經(jīng)占滿,而 CPU 資源消耗很少。這時(shí)界面仍然能正?;瑒?dòng),但平均幀數(shù)會(huì)降到很低。為了避免這種情況,可以嘗試開(kāi)啟CALayer.shouldRasterize(指示在合成之前圖層是否呈現(xiàn)為位圖) 屬性,但這會(huì)把原本離屏渲染的操作轉(zhuǎn)嫁到CPU上去。對(duì)于只需要圓角的某些場(chǎng)合,也可以用一張已經(jīng)繪制好的圓角圖片覆蓋到原本視圖上面來(lái)模擬相同的視覺(jué)效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺(tái)線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。

預(yù)排版

當(dāng)獲取到API JSON數(shù)據(jù)后,把每條Cell需要的數(shù)據(jù)都在后臺(tái)線程計(jì)算并封裝為一個(gè)布局對(duì)象CellLayout。CellLayout包含所有文本的CoreText排版結(jié)果、Cell內(nèi)部每個(gè)控件的高度、Cell的整體高度。每個(gè) CellLayout 的內(nèi)存占用并不多,所以當(dāng)生成后,可以全部緩存到內(nèi)存,以供稍后使用。這樣,TableView 在請(qǐng)求各個(gè)高度函數(shù)時(shí),不會(huì)消耗任何多余計(jì)算量;當(dāng)把 CellLayout 設(shè)置到 Cell 內(nèi)部時(shí),Cell 內(nèi)部也不用再計(jì)算布局了。

對(duì)于通常的 TableView 來(lái)說(shuō),提前在后臺(tái)計(jì)算好布局結(jié)果是非常重要的一個(gè)性能優(yōu)化點(diǎn)。為了達(dá)到最高性能,你可能需要犧牲一些開(kāi)發(fā)速度,盡量不要用 Autolayout 等技術(shù),少用 UILabel 等文本控件。但如果你對(duì)性能的要求并不那么高,可以嘗試用 TableView 的預(yù)估高度的功能,并把每個(gè) Cell 高度緩存下來(lái)。這里有個(gè)來(lái)自百度知道團(tuán)隊(duì)的開(kāi)源項(xiàng)目可以很方便的幫你實(shí)現(xiàn)這一點(diǎn):FDTemplateLayoutCell。

預(yù)渲染

對(duì)于TableView來(lái)說(shuō),Cell內(nèi)容的離屏渲染會(huì)帶來(lái)較大的GPU消耗。為了圖省事兒用到了不少layer的圓角屬性,在低性能的設(shè)備(比如 iPad 3)上快速滑動(dòng)一下這個(gè)列表,能感受到雖然列表并沒(méi)有較大的卡頓,但是整體的平均幀數(shù)降了下來(lái)。用 Instument 查看時(shí)能夠看到 GPU 已經(jīng)滿負(fù)荷運(yùn)轉(zhuǎn),而 CPU 卻比較清閑。為了避免離屏渲染,你應(yīng)當(dāng)盡量避免使用 layer 的 border、corner、shadow、mask 等技術(shù),而盡量在后臺(tái)線程預(yù)先繪制好對(duì)應(yīng)內(nèi)容。

圖片加載流程.png

二進(jìn)制流decode屬于UI的prepare,依賴于圖形編解碼插件。
sdwebimage 關(guān)于解碼的處理

// decode the image in coder queue
dispatch_async(self.coderQueue, ^{
    @autoreleasepool {
        UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
        CGSize imageSize = image.size;
        if (imageSize.width == 0 || imageSize.height == 0) {
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
        } else {
            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
        }
        [self done];
    }
});

異步解碼處理

按需加載VVeboTableView

UITableView持有一個(gè)needLoadArr數(shù)組,它保存著需要刷新的cell的NSIndexPath。

我們先來(lái)看一下needLoadArr是如何使用的:

  1. 在cellForRow:方法里只加載可見(jiàn)cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ...
    [self drawCell:cell withIndexPath:indexPath];
    ...
}

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{
    
    NSDictionary *data = [datas objectAtIndex:indexPath.row];
    
    ...
    cell.data = data;
    
    //當(dāng)前的cell的indexPath不在needLoadArr里面,不用繪制
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }
    
    //將要滾動(dòng)到頂部,不繪制
    if (scrollToToping) {
        return;
    }
    
    //真正繪制cell的代碼
    [cell draw];
}

2 監(jiān)聽(tīng)tableview的快速滾動(dòng),保存目標(biāo)滾動(dòng)范圍的前后三行的索引
知道了如何使用needLoadArr,我們看一下needLoadArr里面的元素是如何被添加和刪除的。

添加元素NSIndexPath
//按需加載 - 如果目標(biāo)行與當(dāng)前行相差超過(guò)指定行數(shù),只在目標(biāo)滾動(dòng)范圍的前后指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    
    //targetContentOffset : 停止后的contentOffset
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    
    //當(dāng)前可見(jiàn)第一行row的index
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    
    //設(shè)置最小跨度,當(dāng)滑動(dòng)的速度很快,超過(guò)這個(gè)跨度時(shí)候執(zhí)行按需加載
    NSInteger skipCount = 8;
    
    //快速滑動(dòng)(跨度超過(guò)了8個(gè)cell)
    if (labs(cip.row-ip.row)>skipCount) {
        
        //某個(gè)區(qū)域里的單元格的indexPath
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        
        if (velocity.y<0) {
            
            //向上滾動(dòng)
            NSIndexPath *indexPath = [temp lastObject];
            
            //超過(guò)倒數(shù)第3個(gè)
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        
        } else {
            
            //向下滾動(dòng)
            NSIndexPath *indexPath = [temp firstObject];
            //超過(guò)正數(shù)第3個(gè)
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        //添加arr里的內(nèi)容到needLoadArr的末尾
        [needLoadArr addObjectsFromArray:arr];
    }
}

知道了如何向needLoadArr里添加元素,現(xiàn)在看一下何時(shí)(重置)清理這個(gè)array:

移除元素NSIndexPath
//用戶觸摸時(shí)第一時(shí)間加載內(nèi)容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    
    if (!scrollToToping) {
        [needLoadArr removeAllObjects];
        [self loadContent];
    }
    return [super hitTest:point withEvent:event];
}


- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    [needLoadArr removeAllObjects];
}

//將要滾動(dòng)到頂部
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}

//停止?jié)L動(dòng)
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

//滾動(dòng)到了頂部
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

我們可以看到,當(dāng)手指觸碰到tableview時(shí) 和 開(kāi)始拖動(dòng)tableview的時(shí)候就要清理這個(gè)數(shù)組。
而且在手指觸碰到tableview時(shí)和 tableview停止?jié)L動(dòng)后就會(huì)執(zhí)行l(wèi)oadContent方法,用來(lái)加載可見(jiàn)區(qū)域的cell。

loadContent方法的具體實(shí)現(xiàn):
- (void)loadContent{
    
    //正在滾動(dòng)到頂部
    if (scrollToToping) {
        return;
    }
    
    //可見(jiàn)cell數(shù)
    if (self.indexPathsForVisibleRows.count<=0) {
        return;
    }
    
    //觸摸的時(shí)候刷新可見(jiàn)cell
    if (self.visibleCells&&self.visibleCells.count>0) {
        for (id temp in [self.visibleCells copy]) {
            VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
            [cell draw];
        }
    }
}

在這里注意一下,tableview的visibleCells屬性是可見(jiàn)的cell的數(shù)組。

異步繪制

當(dāng) TableView 快速滑動(dòng)時(shí),會(huì)有大量異步繪制任務(wù)提交到后臺(tái)線程去執(zhí)行。但是有時(shí)滑動(dòng)速度過(guò)快時(shí),繪制任務(wù)還沒(méi)有完成就可能已經(jīng)被取消了。如果這時(shí)仍然繼續(xù)繪制,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無(wú)法完成。如果這時(shí)仍然繼續(xù)繪制,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無(wú)法完成。我的做法是盡量快速、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前,我都會(huì)調(diào)用 isCancelled() 來(lái)進(jìn)行判斷,保證被取消的任務(wù)能及時(shí)退出,不至于影響后續(xù)操作。

全局并發(fā)控制
用 concurrent queue 來(lái)執(zhí)行大量繪制任務(wù)時(shí),偶爾會(huì)遇到線程鎖住的問(wèn)題。
大量的任務(wù)提交到后臺(tái)隊(duì)列時(shí),某些任務(wù)會(huì)因?yàn)槟承┰颍ù颂幨?CGFont 鎖)被鎖住導(dǎo)致線程休眠,或者被阻塞,concurrent queue 隨后會(huì)創(chuàng)建新的線程來(lái)執(zhí)行其他任務(wù)。當(dāng)這種情況變多時(shí),或者 App 中使用了大量 concurrent queue 來(lái)執(zhí)行較多任務(wù)時(shí),App 在同一時(shí)刻就會(huì)存在幾十個(gè)線程同時(shí)運(yùn)行、創(chuàng)建、銷毀。CPU 是用時(shí)間片輪轉(zhuǎn)來(lái)實(shí)現(xiàn)線程并發(fā)的,盡管 concurrent queue 能控制線程的優(yōu)先級(jí),但當(dāng)大量線程同時(shí)創(chuàng)建運(yùn)行銷毀時(shí),這些操作仍然會(huì)擠占掉主線程的 CPU 資源。
使用 concurrent queue 時(shí)不可避免會(huì)遇到這種問(wèn)題,但使用 serial queue 又不能充分利用多核 CPU 的資源。辦法是為不同優(yōu)先級(jí)創(chuàng)建和 CPU 數(shù)量相同的 serial queue,每次從 pool 中獲取 queue 時(shí),會(huì)輪詢返回其中一個(gè) queue。把 App 內(nèi)所有異步操作,包括圖像解碼、對(duì)象釋放、異步繪制等,都按優(yōu)先級(jí)不同放入了全局的 serial queue 中執(zhí)行,這樣盡量避免了過(guò)多線程導(dǎo)致的性能問(wèn)題。

Graver

從文本計(jì)算、樣式排版渲染、圖片解碼,再到繪制,實(shí)現(xiàn)了全程異步化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容