頁面卡頓的優化–圓角

Peter_shenlipingUpUp發表於2019-02-27

今天產品經理告訴我:誒,那個誰,這個介面很卡誒!!!你看看什麼情況。。。於是我掏出了Instrument裡的Core Animation看看FPS,發現滑動的時候FPS特別低Orz!

產生卡頓的原因

首先,查閱資料看下為什麼會產生卡頓的原因。

渲染機制.jpg

在 iOS 系統中,影像內容展示到螢幕的過程需要 CPU 和 GPU 共同參與。CPU 負責計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。之後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時螢幕會保留之前的內容不變。這就是介面卡頓的原因。
那GPU什麼時候會消耗呢,相對於CPU,GPU做的事情比較單一,接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合並渲染,然後輸出到螢幕上。寬泛的說,大多數 CALayer 的屬性都是用 GPU 來繪製。以下操作會降低GPU的效能:

大量幾何結構

所有的 Bitmap,包括圖片、文字、柵格化的內容,最終都要由記憶體提交到視訊記憶體,繫結為 GPU Texture。不論是提交到視訊記憶體的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片並且快速滑動時),CPU 佔用率很低,GPU 佔用非常高,介面仍然會掉幀。避免這種情況的方法只能是儘量減少在短時間內大量圖片的顯示,儘可能將多張圖片合成為一張進行顯示。
另外當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。

檢視的混合

當多個檢視(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果檢視結構過於複雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當儘量減少檢視數量和層次,並且減少不必要的透明檢視。

離屏渲染

離屏渲染是指圖層在被顯示之前是在當前螢幕緩衝區以外開闢的一個緩衝區進行渲染操作。
離屏渲染需要多次切換上下文環境:先是從當前螢幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上又需要將上下文環境從離屏切換到當前螢幕,而上下文環境的切換是一項高開銷的動作。
會造成 offscreen rendering 的原因有:
陰影(UIView.layer.shadowOffset/shadowRadius/…)
圓角(當 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用時)
圖層蒙板
開啟光柵化(shouldRasterize = true)

圓角優化

所以我們找到了原因,由於我們的專案是基於地圖的,在地圖上直接新增頁面上去,之前地圖就有很多圓角設定,加上cell上的圓角,會造成GPU效能的損耗,所以相對來說頁面的滑動會損耗GPU啦。
於是回到了老生常談的問題了,關於圓角的優化。
之前看文章有說這樣去優化:

+ (void)cutRadiousWithView:(UIView *)view radious:(CGFloat)radious {
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radious, radious)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
    //設定大小
    maskLayer.frame = view.bounds;
    //設定圖形樣子
    maskLayer.path = maskPath.CGPath;
    view.layer.mask = maskLayer;
}
複製程式碼

然而發現並沒有什麼用,其實mask遮罩還是會發生離屏渲染的。而且親測這樣的FPS貌似更低???(黑人問號臉)

圖片圓角優化

於是我繼續查資料,發現圖片的圓角可以將圖片進行進行重繪,得到一張新的圖片。方法如下:

+ (UIImage *)cutCircleImageWithImage:(UIImage *)image size:(CGSize)size radious:(CGFloat)radious {
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);

    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    CGContextAddPath(UIGraphicsGetCurrentContext(),
                 [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radious].CGPath);
    CGContextClip(UIGraphicsGetCurrentContext());

    [image drawInRect:rect];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}
複製程式碼

用這個方法可以得到新的圖片,對於圖片的圓角處理可以這樣做,但是對於View的呢?

View圓角優化

View的話可以給他蓋一層ImageView,設定下Imageview的圖片,方法如下:

+ (void)cutCicleViewWithView:(UIView *)view radious:(CGFloat)radius {
    CGSize size = view.frame.size;
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    UIColor *bkColor = view.backgroundColor;

    UIImage *image = [[UIImage alloc] init];
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, bkColor.CGColor);
    CGContextAddPath(context,
                 [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
    CGContextDrawPath(context, kCGPathFill);
    [image drawInRect:rect];
    UIImage *output = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:rect];
    imageView.image = output;
    [view insertSubview:imageView atIndex:0];
    view.backgroundColor = [UIColor clearColor];
}
複製程式碼

總結

相信學過演算法的同學都知道,越快的演算法,可能空間複雜度越高。當你為了某一效能去優化,必然會損耗其他的效能,所謂的演算法優化是為了達到最佳實現效果。所以這樣優化GPU的結果就是會損耗CPU。
過早的優化是魔鬼,但是到問題出來的時候 ,有些東西就有必要去做了!

參考資料:

http://www.reviewcode.cn/article.html?reviewId=7
http://ios.jobbole.com/92237/

相關文章