離屏渲染(Offscreen rendering)對iOS開發者來講不是一個陌生的東西,項目中或多或少都會存在離屏渲染,也是面試中常常考察的知識點。通常來講,大多數人都能知道設置圓角、mask、陰影等會觸發離屏渲染,但咱們深刻的探究一下,你們可以很清楚的知道下面幾個問題嗎?ios
今天我就帶着這幾個問題探究一下離屏渲染。面試
這是在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419)中有這樣一張圖,咱們能夠看到,在Application這一層中主要是CPU在操做,而到了Render Server這一層,CoreAnimation會將具體操做轉換成發送給GPU的draw calls(之前是call OpenGL ES,如今慢慢轉到了Metal),顯然CPU和GPU雙方同處於一個流水線中,協做完成整個渲染工做。咱們也能夠把iOS下的Core Animation能夠理解爲一個複合引擎,主要職責包含:渲染、構建和實現動畫。算法
但在某些場景下「畫家算法」雖然能夠逐層輸出,可是沒法在某一層渲染完成後,在回過頭來擦除/修改某一部分,由於這一層以前的layer像素數據已經被永久覆蓋了。這就意味着對於每一層的layer要麼可以經過單次遍歷就能完成渲染,要麼就只能令開闢一塊內存做爲臨時中轉區來完成複雜的修改/裁剪等操做。緩存
舉例說明: 對圖3進行圓角和裁剪:imageView.clipsToBounds = YES,imageView.layer.cornerRadius=10時,這就不是簡單的圖層疊加了, 圖1,圖2,圖3渲染完成後, 還要進行裁減,並且 子視圖layer由於父視圖有圓角,也須要被裁剪, 沒法在某一層渲染完成以後,再回過頭來擦除/改變其中的某個部分。因此不能按照正常的流程,所以蘋果會先渲染好每一層,存入一個緩衝區中,即 離屏緩衝區 ,而後通過層疊加和處理後,再存儲到幀緩存去中,而後繪製到屏幕上,這種處理方式叫作 離屏渲染
使用Simulator檢測項目中觸發離屏渲染的圖層,以下圖:bash
打開 Color Off-screen Rendered,同時咱們能夠藉助Xcode或 Reveal 清楚的看到那些圖層觸發了離屏渲染。併發
關於常見的設置圓角觸發離屏渲染示例說明:app
如上圖示例代碼中(btn.png是一個200x300的本地圖片),框架
解釋:btn1和img1觸發了離屏渲染,緣由是btn1是由它的layer和UIImageView的layer混合起來的效果(UIButton有imageView),因此設置圓角的時候會觸發離屏渲染。img1設置cornerRadius和masksToBounds是不會觸發離屏渲染的,若是再對img1設置背景色,則會觸發離屏渲染。
根據示例能夠得出只是控件設置了圓角或(圓角+裁剪)並不會觸發離屏渲染,同時須要知足父layer須要裁剪時,子layer也由於父layer設置了圓角也須要被裁剪(即視圖contents有內容併發生了多圖層被裁剪)時纔會觸發離屏渲染。post
蘋果官方文檔對於cornerRadius
的描述:性能
Setting the radius to a value greater than
0.0
causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’scontents
property; it applies only to the background color and border of the layer. However, setting themasksToBounds
property totrue
causes the content to be clipped to the rounded corners.
設置cornerRadius
大於0時,只爲layer的backgroundColor
和border
設置圓角;而不會對layer的contents
設置圓角,除非同時設置了layer.masksToBounds
爲true
(對應UIView的clipsToBounds
屬性)。
一旦咱們 爲contents設置了內容 ,不管是圖片、繪製內容、有圖像信息的子視圖等,再加上圓角+裁剪,就會觸發離屏渲染。
- 採用了光柵化的 layer (layer.shouldRasterize)
- 使用了 mask 的 layer (layer.mask)
- 須要進行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
- 設置了組透明度爲 YES,而且透明度不爲 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
- 使用了高斯模糊
- 添加了投影的 layer (layer.shadow*)
- 繪製了文字的 layer (UILabel, CATextLayer, Core Text 等)
shouldRasterize開啓後,會將layer做爲位圖保存下來,下次直接與其餘內容進行混合。這個保存的位置就是OffscreenBuffer中。這樣下次須要再次渲染的時候,就能夠直接拿來使用了。
shouldRasterize使用建議:
離屏渲染增大了系統的負擔,會形象App性能。主要表如今如下幾個方面:
雖然離屏渲染會須要多開闢出新的臨時緩存區來存儲中間狀態,可是對於屢次出如今屏幕上的數據,能夠提早渲染好,從而進行復用,這樣CPU/GPU就不用作一些重複的計算。
特殊產品需求,爲實現一些特殊動效果,須要多圖層以及離屏緩存區保存中間狀態,這種狀況下就不得不使用離屏渲染。好比產品須要實現高斯模糊,不管自定義高斯模糊仍是調用系統API都會觸發離屏渲染。
方案一
self.view.layer.clipsToBounds = YES;self.view.layer.cornerRadius = 4.f;複製代碼
- clipsToBounds:UIView中的屬性,其值主要決定了在視圖上的子視圖,超出父視圖的部分是否截取,默認爲NO,即不裁剪子視圖超出部分。
- masksToBounds:CALayer中的屬性,其值主要決定了視圖的圖層上的子圖層,超出父圖層的部分是否須要裁減掉。默認NO。
方案二
若是產品設計圓角+陰影的卡片,可使用切圖實現圓角+陰影,避免觸發離屏渲染
方案三
貝塞爾曲線繪製圓角
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{
/* 當前UIImage的可見繪製區域 */
CGRect rect = (CGRect){0.f,0.f,size};
/* 建立基於位圖的上下文 */
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
/* 在當前位圖上下文添加圓角繪製路徑 */
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
/* 當前繪製路徑和原繪製路徑相交獲得最終裁剪繪製路徑 */
CGContextClip(UIGraphicsGetCurrentContext());
/* 繪製 */
[self drawInRect:rect];
/* 取得裁剪後的image */
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
/* 關閉當前位圖上下文 */
UIGraphicsEndImageContext();
return image;
}複製代碼
方案四
CAShapeLayer + UIBezierPath 繪製圓角來實現UITableViewCell圓角並繪製邊框顏色(這種方式比直接設置圓角方式好,但也會觸發離屏渲染),代碼以下:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = CGRectMake(0, 0, cell.width, cell.height);
CAShapeLayer *borderLayer = [CAShapeLayer layer];
borderLayer.frame = CGRectMake(0, 0, cell.width, cell.height);
borderLayer.lineWidth = 1.f;
borderLayer.strokeColor = COLOR_LINE.CGColor;
borderLayer.fillColor = [UIColor clearColor].CGColor;
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, cell.width, cell.height) cornerRadius:kRadiusCard];
maskLayer.path = bezierPath.CGPath;
borderLayer.path = bezierPath.CGPath;
[cell.contentView.layer insertSublayer:borderLayer atIndex:0];
[cell.layer setMask:maskLayer];
}
複製代碼
YYKit是開發中常常用的三方庫,YYImage對圖片圓角的處理方法是值得推薦的,附上實現源碼:
- (UIImage *)imageByRoundCornerRadius:(CGFloat)radius
corners:(UIRectCorner)corners
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
borderLineJoin:(CGLineJoin)borderLineJoin {
if (corners != UIRectCornerAllCorners) {
UIRectCorner tmp = 0;
if (corners & UIRectCornerTopLeft) tmp |= UIRectCornerBottomLeft;
if (corners & UIRectCornerTopRight) tmp |= UIRectCornerBottomRight;
if (corners & UIRectCornerBottomLeft) tmp |= UIRectCornerTopLeft;
if (corners & UIRectCornerBottomRight) tmp |= UIRectCornerTopRight;
corners = tmp;
}
UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);
CGFloat minSize = MIN(self.size.width, self.size.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
[path closePath];
CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, self.CGImage);
CGContextRestoreGState(context);
}
if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) {
CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat strokeRadius = radius > self.scale / 2 ? radius - self.scale / 2 : 0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, borderWidth)];
[path closePath];
path.lineWidth = borderWidth;
path.lineJoinStyle = borderLineJoin;
[borderColor setStroke];
[path stroke];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
複製代碼
iOS圓角的離屏渲染,你真的弄明白了嗎:關於圓角觸發離屏渲染更詳細的分析
關於iOS離屏渲染的深刻研究:即刻技術團隊對離屏渲染的解析