有一個場景:以 TioIM 的最近聊天會話列表爲例:每一個cell上的頭像圖片均須要裁剪,使用以下方式設置圓角算法
圖片是異步加載緩存
imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
複製代碼
當滑動列表時,且當數據量比較大時,快速滑動會發現列表卡頓,咱們進行以下操做,查看bash
開啓模擬器的離屏渲染,會發現網絡
imageView變成了黃色,說明出現了 離屏渲染。異步
咱們修改一下代碼,去掉裁剪masksToBounds佈局
imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
// imageView.layer.masksToBounds = YES;
複製代碼
結果以下:content(image)沒有裁剪,也沒有看到離屏渲染post
咱們再修改一下代碼,去掉backgroundColor// imageView.backgroundColor = [UIColor whiteColor];
imageView.layer.cornerRadius = 50;
imageView.layer.masksToBounds = YES;
複製代碼
黃色部分消失優化
因此說設置了 cornerRadius 未必會致使離屏渲染,動畫
沒有離屏渲染時的步驟ui
發生離屏渲染後的渲染步驟
渲染管線從Core Animation 到 Display 的工做流程
若是渲染的畫面比較複雜,如上面案例,背景色backgroundColor須要渲染,content(image)也須要裁剪渲染,若是不按離屏渲染 的步驟,根據畫家算法,先顯示遠處的背景(類比平時子控件佈局,最早顯示的在最下面):
因此,須要額外開闢一塊緩衝區,等待合成、裁剪完成後-->幀緩衝區-->屏幕顯示。 那麼,這個額外的處理複雜渲染數據的地方就是 離屏渲染緩衝區(Offscreen Buffer)。 因此,在幀緩衝區以前要多了一個離屏渲染緩衝區。
下面是RenderServer進入GPU到顯示前,以流程圖的形式將上面的文字直觀化出來:
直接循序漸進,按照畫家算法一層一層顯示到屏幕就OK,根本用不着等到離屏渲染緩衝區進行額外的處理完再顯示。
當咱們知道爲何會觸發離屏渲染,以及離屏渲染會在渲染流程哪一步進行,咱們再來看看tableview的卡頓問題。固然,促使tableview卡頓的緣由有不少,咱們只談由於離屏渲染致使的卡頓。
若是每個cell都會有一個甚至若干個控件會出現離屏渲染,其中每個渲染都要等到離屏渲染結束後纔給幀緩衝區等待顯示,看上圖的渲染管線的流程,GPU在沒有離屏渲染時是16.67ms,若是有離屏渲染處理,在大量cell快速滾動,出現的cell不斷從新渲染,內部的多個離屏渲染也在進行大量的耗時處理,這就太可怕了。因此在這種時候,咱們要優化離屏渲染。
不少同窗會很是熟悉文章開頭的場景cornerRadius+masksToBounds。
在前面的會話列表幾個不一樣狀況中,咱們已經能夠看到cornerRadius+masksToBounds並非必定會引起離屏渲染,只有當。關鍵是要看有幾層渲染數據:backgroundcolor、content(image、text等)。
// img1 content masksToBounds
UIImageView *img1 = [UIImageView.alloc initWithFrame:CGRectMake(100, 30, 100, 100)];
img1.image = [UIImage imageNamed:@"btn.png"];
img1.layer.cornerRadius = 50;
img1.layer.masksToBounds = YES;
[self.view addSubview:img1];
// img2 只有backgroundColor
UIImageView *img2 = [UIImageView.alloc initWithFrame:CGRectMake(100, 180, 100, 100)];
img2.layer.cornerRadius = 50;
img2.backgroundColor = UIColor.redColor;
[self.view addSubview:img2];
// content + backgroundColor
UIImageView *img3 = [UIImageView.alloc initWithFrame:CGRectMake(100, 320, 100, 100)];
img3.image = [UIImage imageNamed:@"btn.png"];
img3.backgroundColor = UIColor.redColor;
img3.layer.cornerRadius = 50;
[self.view addSubview:img3];
// content + backgroundColor + masksToBounds
UIImageView *img4 = [UIImageView.alloc initWithFrame:CGRectMake(100, 480, 100, 100)];
img4.image = [UIImage imageNamed:@"btn.png"];
img4.backgroundColor = UIColor.redColor;
img4.layer.cornerRadius = 50;
img4.layer.masksToBounds = YES;
[self.view addSubview:img4];
複製代碼
並且,咱們還能看到另外的一個知識點:cornerRadius並非對content(image)有效,根據蘋果官方解釋:cornerRadius 的文檔中明確說明對 cornerRadius 的設置只對 CALayer 的 backgroundColor 和 borderWidth、borderColor起做用。
cornerRadius+masksToBounds 只有在設置了content且背景不是透明時,纔會出現離屏渲染。
若是必定要使用cornerRadius+masksToBounds的方式裁切圖片,不要設置backgroundColor。
渲染的位圖並不能直接給幀緩存區等待顯示,而要通過模糊處理以後才能將最後的渲染數據 -> 幀緩衝區-> 顯示。
開啓陰影效果,首先要shadow是一個什麼樣的存在?他又是如何被渲染繪製?
shadow是一個矩形,是一個背景色,是layer的背景,因此是在layer的下面。shadow是根據layer而來,因此要先知道layer才能直到shadow的大小位置。
若是沒有離屏渲染,和前文同樣,按照畫家算法,必須先將shadow放入幀緩存區,先顯示。可是layer沒有,不可能先渲染出shadow,只能利用離屏渲染緩衝區,等待shadow、layer等渲染併合並完成後,再送入幀緩存區等待顯示。
CALayer的屬性 官方文檔給的是:
意思是若是開啓,會將layer最後的渲染,包括陰影、裁切等的最終效果變成位圖放入離屏緩衝區,等待複用。可是,離屏緩衝區的大小不能超過屏幕的2.5倍,不然被釋放;其次,layer若是不是靜態的,好比imageview的image須要改變,label的text會發生改變等會發生頻繁改變的,開啓shouldRasterize離屏渲染會影響效率;還有,離屏緩衝區是有時間限制的,超過100ms若是沒有被使用,也會被釋放。
因此,咱們要善用shouldRasterize:
masklayer 做爲遮罩,顯示在其所在的大layer以及大layer的全部子sublayer之上。masklayer可能也會帶有透明度、形狀(例如,顯示指定區域內的的內容)等。
面對上面的一種狀況,咱們必須在離屏渲染緩衝區內完成Image和Mask的裁切合並處理,才能將最終的Masked Image -> 幀緩衝區顯示。
我在實際工做當中,遇到特別是針對 異步加載後的網絡圖片進行圓角裁切, 具體寫在個人博客 SDWebImage網絡圖片的圓角裁切和不變形處理 有興趣的同窗能夠看一看。主要解決:
部分裁切的核心代碼
- (instancetype)imageWithCornerRadius:(CGFloat)cornerRadius size:(CGSize)newSize
{
UIImage *originImage = self;
// 開始裁切圓角
CGRect bounds = CGRectMake(0, 0, newSize.width, newSize.height);
UIGraphicsBeginImageContextWithOptions(newSize, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bounds
cornerRadius:cornerRadius];
CGContextAddPath(context, path.CGPath);
CGContextClip(context);
[originImage drawInRect:bounds];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
複製代碼