本文是基于UIKit性能調(diào)優(yōu)實戰(zhàn)講解 - 簡書這篇文章的總結(jié)詳細請看原文
實例講解可以參考:小心別讓圓角成了你列表的幀數(shù)殺手 - CocoaChina_讓移動開發(fā)更簡單
fps表示frames per second,也就是每秒鐘顯示多少幀畫面。對于靜止不變的內(nèi)容,我們不需要考慮它的刷新率,但在執(zhí)行動畫或滑動時,fps的值直接反映出滑動的流暢程度
調(diào)試優(yōu)化:
1.圖層混合:
首先理解像素:像素就是屏幕上的每一個點由RGB三種顏色構(gòu)成有時候會帶有alpha,如果某一個區(qū)域覆蓋了多個layer,最后的顯示效果就會收到影響,顯色混合會消耗一定的GPU,當把最上層的顏色透明度設(shè)置為100%時GPU就會忽略下面所有的layer節(jié)省不必要的運算
第一個調(diào)試選項"Color Blended Layers"正是用于檢測哪里發(fā)生了圖層混合,并用紅色標記出來。因此我們需要盡可能減少看到的紅色區(qū)域。一旦發(fā)現(xiàn)應(yīng)該想法設(shè)法消除它
解決方案:a.將控件的opaque = true 原理是希望避免圖層混合,但是作用不大,因為UIView的這個屬性默認就是true只要不是人為的設(shè)置為透明都不會出現(xiàn)圖層混合,但對于UIImageView他本身和圖片都不能為透明的,圖片自身的性質(zhì)也可能會對結(jié)果產(chǎn)生影響,比opaque屬性更重要的是backgroundColor屬性,如果不設(shè)置這個屬性,控件依然被認為是透明的,所以我們做的是將他設(shè)置為白色
PS:如果label文字有中文,依然會出現(xiàn)圖層混合,這是因為此時label多了一個sublayer,如果有好的解決辦法歡迎告訴我。
2.光柵化光柵化會導致離屏渲染
光柵化是將一個layer預先渲染成位圖(bitmap),然后加入緩存中。如果對于陰影效果這樣比較消耗資源的靜態(tài)內(nèi)容進行緩存,可以得到一定幅度的性能提升。demo中的這一行代碼表示將label的layer光柵化:
label.layer.shouldRasterize?=?true
Instrument中,第二個調(diào)試選項是“Color Hits Green and Misses Red”,它表示如果命中緩存則顯示為綠色,否則顯示為紅色,顯然綠色越多越好,紅色越少越好。
光柵化的緩存機制是一把雙刃劍,先寫入緩存再讀取有可能消耗較多的時間。因此光柵化僅適用于較復雜的、靜態(tài)的效果。通過Instrument的調(diào)試發(fā)現(xiàn),這里使用光柵化經(jīng)常出現(xiàn)未命中緩存的情況,如果沒有特殊需要則可以關(guān)閉光柵化
3.顏色格式:
像素在內(nèi)存中的布局和它在磁盤中的存儲方式并不相同??紤]一種簡單的情況:每個像素有R、G、B和alpha四個值,每個值占用1字節(jié),因此每個像素占用4字節(jié)的內(nèi)存空間。一張1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600個像素,因此占用了超過8Mb的內(nèi)存。但是一張同樣分辨率的PNG格式或JPEG格式的圖片一般情況下不會有這么大。這是因為JPEG將像素數(shù)據(jù)進行了一種非常復雜且可逆的轉(zhuǎn)化。
比如應(yīng)用中有一些從網(wǎng)絡(luò)下載的圖片,而GPU恰好不支持這個格式,這就需要CPU預先進行格式轉(zhuǎn)化
第三個選項“Color Copied Images”就用來檢測這種實時的格式轉(zhuǎn)化,如果有則會將圖片標記為藍色。
4.圖片大小
第五個選項“Color Misaligned Images”。它表示如果圖片需要縮放則標記為黃色,如果沒有像素對齊則標記為紫色
第三個優(yōu)化是調(diào)整所有圖片的像素大小以避免不必要的縮放。
5.離屏渲染
正常的渲染通道:OpenGL提交一個命令到Command Buffer,隨后GPU開始渲染,渲染結(jié)果放到Render Buffer中,這是正常的渲染流程
有一些復雜的效果無法直接渲染出結(jié)果,它需要分步渲染最后再組合起來,比如添加一個蒙版(mask):GPU分別得到了紋理(texture,也就是那個相機圖標)和layer(藍色的蒙版)的渲染結(jié)果。但這兩個渲染結(jié)果沒有直接放入Render Buffer中,也就表示這是離屏渲染。直到第三個渲染通道,才把兩者組合起來放入Render Buffer中。離屏渲染意味著把渲染結(jié)果臨時保存,等用到時再取出,因此相對于普通渲染更占用資源。
第六個選項“Color Offscreen-Rendered Yellow”會把需要離屏渲染的地方標記為黃色,大部分情況下我們需要盡可能避免黃色的出現(xiàn)。離屏渲染可能會自動觸發(fā),也可以手動觸發(fā)。以下情況可能會導致觸發(fā)離屏渲染:
重寫drawRect方法
有mask或者是陰影(layer.masksToBounds, layer.shadow*),模糊效果也是一種mask
layer.shouldRasterize = true
前兩者會自動觸發(fā)離屏渲染,第三種方法是手動開啟離屏渲染。
第四個優(yōu)化,在設(shè)置陰影效果的四行代碼下面添加一行:
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
這行代碼制定了陰影路徑,如果沒有手動指定,Core Animation會去自動計算,這就會觸發(fā)離屏渲染。如果人為指定了陰影路徑,就可以免去計算,從而避免產(chǎn)生離屏渲染。
設(shè)置cornerRadius本身并不會導致離屏渲染,但很多時候它還需要配合layer.masksToBounds = true使用。根據(jù)之前的總結(jié),設(shè)置masksToBounds會導致離屏渲染。解決方案是盡可能在滑動時避免設(shè)置圓角,如果必須設(shè)置圓角,可以使用光柵化技術(shù)將圓角緩存起來:
//?設(shè)置圓角 ? ?ps:用后用后無效果
label.layer.masksToBounds?=?true
label.layer.cornerRadius?=?8
label.layer.shouldRasterize?=?true
label.layer.rasterizationScale?=?layer.contentsScale
6.快速路徑:
之前將離屏渲染和渲染路徑時的示意圖么,離屏渲染的最后一步是把此前的多個路徑組合起來。如果這個組合過程能由CPU完成,就會大量減少GPU的工作。這種技術(shù)在繪制地圖中可能用到。
第七個選項“Color Compositing Fast-Path Blue”用于標記由硬件繪制的路徑,藍色越多越好。
7.變化區(qū)域
刷新視圖時,我們應(yīng)該把需要重繪的區(qū)域盡可能縮小。對于未發(fā)生變化的內(nèi)容則不應(yīng)該重繪,第八個選項“Flash updated Regions”用于標記發(fā)生重繪的區(qū)域。一個典型的例子是系統(tǒng)的時鐘應(yīng)用,絕大多數(shù)時候只有顯示秒針的區(qū)域需要重繪:
總結(jié)
如果你一步一步做到了這里,我想一定會有不少收益。不過,學而不思則罔,思而不學則殆。動手實踐后還是應(yīng)該總結(jié)提煉,優(yōu)化滑動性能主要涉及三個方面:
避免圖層混合
確保控件的opaque屬性設(shè)置為true,確保backgroundColor和父視圖顏色一致且不透明
如無特殊需要,不要設(shè)置低于1的alpha值
確保UIImage沒有alpha通道
避免臨時轉(zhuǎn)換
確保圖片大小和frame一致,不要在滑動時縮放圖片
確保圖片顏色格式被GPU支持,避免勞煩CPU轉(zhuǎn)換
慎用離屏渲染
絕大多數(shù)時候離屏渲染會影響性能
重寫drawRect方法,設(shè)置圓角、陰影、模糊效果,光柵化都會導致離屏渲染
設(shè)置陰影效果是加上陰影路徑
滑動時若需要圓角效果,開啟光柵化
實戰(zhàn)
本文的demo可以在我的Github上下載,然后一步一步自己體驗優(yōu)化過程。但demo畢竟是刻意搭建的一個環(huán)境,我會在我自己的仿寫的簡書app上不斷進行實戰(zhàn)優(yōu)化,歡迎共同學習交流。
代碼:
UIView圓角:
#import "UIView+AddCorner.h"
@implementation UIView (AddCorner)
-(double)roundbyuint:(double) num anUint:(double )unit{
double remain = modf(num, &unit);
if (remain > unit / 2.0) {
return [self ceilbyuint:num andUint:unit];
} else {
return [self floorbyuint:num andUint:unit];
}
}
-(double)ceilbyuint:(double)num andUint:(double)unit{
return num - modf(num, &unit) + unit;
//將浮點數(shù)num分解成整數(shù)部分和小數(shù)部分
}
-(double)floorbyuint:(double)num andUint:(double)unit{
return num - modf(num, &unit) ;
}
-(double)piexl:(double)num{
double unit;
switch ((int)[UIScreen mainScreen].scale) {
case 1: unit = 1.0/1.0; break;
case 2: unit = 1.0/2.0; break;
case 3: unit = 1.0/3.0; break;
default: unit = 0.0;
break;
}
return [self roundbyuint:num anUint:unit];
}
-(void)kt_viewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor{
UIImageView * imageView = [[UIImageView alloc]initWithImage:[self ViewAddCorner:radius andBorderWidth:borderWidth andBorderColor:borderColor andBackgroundColor:backgroundColor]] ;
[self insertSubview:imageView atIndex:0];
}
-(UIImage *)ViewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor {
CGSize sizeToFit = CGSizeMake((double)CGRectGetWidth(self.frame), (double)CGRectGetHeight(self.frame));
CGFloat halfBorderWidth = (CGFloat)borderWidth / 2.0;
UIGraphicsBeginImageContextWithOptions(sizeToFit, false, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGFloat width = sizeToFit.width, height = sizeToFit.height;
CGContextMoveToPoint(context, width - halfBorderWidth, radius + halfBorderWidth);? // 開始坐標右邊開始
CGContextAddArcToPoint(context, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius);? // 右下角角度
CGContextAddArcToPoint(context, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius); // 左下角角度
CGContextAddArcToPoint(context, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius); // 左上角
CGContextAddArcToPoint(context, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius) ;// 右上角
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
UIImage? *output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output;
}
@end
UIImageView圓角:
#import "UIImageView+AddCorner.h"
@implementation UIImageView (AddCorner)
-(void)kt_ImageViewAddCornerWithRadius:(CGFloat)radius{
// self.image = self.image?:[self.image imageAddCornerWithRadius:radius andSize:self.bounds.size];
//注意第三個選項的設(shè)置
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);
//在繪制之前先裁剪出一個圓形
[[UIBezierPath bezierPathWithRoundedRect:self.bounds
cornerRadius:radius] addClip];
//圖片在設(shè)置的圓形里面進行繪制
[self.image drawInRect:self.bounds];
//獲取圖片
self.image = UIGraphicsGetImageFromCurrentImageContext();
//結(jié)束繪制
UIGraphicsEndImageContext();
}
@end
UIImage圓角:
#import "UIImage+ImageRoundedCorner.h"
@implementation UIImage (ImageRoundedCorner)
- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CGContextAddPath(ctx,path.CGPath);
CGContextClip(ctx);
[self drawInRect:rect];
CGContextDrawPath(ctx, kCGPathFillStroke);
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
@end