編輯:關於Android編程
APP頁面優化對小編來說一直是難題,最近一直在不斷的學習和總結 ,發現APP頁面優化說到底離不開view的繪制和渲染機制。網上有很多精彩的博客,小編借鑒之前N多大牛研究成果,同時結合自己遇到的一些問題,寫了這篇博客。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; ZYYView *view = [[ZYYView alloc] init]; view.backgroundColor = [UIColor whiteColor]; view.bounds = CGRectMake(0, 0, 100, 100); view.center = CGPointMake(100, 100); [self.view addSubview:view]; } @end
@implementation ZYYView - (void)drawRect:(CGRect)rect { CGContextRef con = UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con, CGRectMake(0,0,100,200)); CGContextSetRGBFillColor(con, 0, 0, 1, 1); CGContextFillPath(con); } @end
當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全局的容器去。
蘋果注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,並更新 UI 界面。
這個函數內部的調用棧大概是這樣的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
我們上圖的堆棧信息 截圖 ,看到巴拉巴拉一大堆調用堆棧信息,其實這就是個函數做的孽 。如何不能理解,那直接看下面的流程圖吧。
iOS的mainRunloop是一個60fps的回調,也就是說每16.7ms會繪制一次屏幕,這個時間段內要完成view的緩沖區創建,view內容的繪制(如果重寫了drawRect),這些CPU的工作。然後將這個緩沖區交給GPU渲染,這個過程又包括多個view的拼接(compositing),紋理的渲染(Texture)等,最終顯示在屏幕上。整個過程就是我們上面畫的流程圖。 因此,如果在16.7ms內完不成這些操作,比如,CPU做了太多的工作,或者view層次過於多,圖片過於大,導致GPU壓力太大,就會導致“卡”的現象,也就是丟幀
首先我們假設有這樣一個需求:實現下面的橢圓效果:
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; ZYYView *view = [[ZYYView alloc] init]; view.backgroundColor = [UIColor whiteColor]; view.bounds = CGRectMake(0, 0, 100, 100); view.center = CGPointMake(100, 100); [self.view addSubview:view]; } @end
@implementation ZYYView - (void)drawRect:(CGRect)rect { CGContextRef con = UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con, CGRectMake(0,0,100,200)); CGContextSetRGBFillColor(con, 0, 0, 1, 1); CGContextFillPath(con); } @end
1、 在[ZYYView drawRect:] 方法之前,先調用了 [UIView(CALayerDelegate) drawLayer:inContext:] 和 [CALayer drawInContext:]
2、如果 [self.view addSubview:view]; 被注銷掉 則 drawRect 不執行。可以肯定 drawRect
方法是由 addSubview 函數觸發的。
代碼示例
CGContextRef con = UIGraphicsGetCurrentContext();
堆棧展示
底層原理
每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊緩存,叫做backing store
當UIView被繪制時(從 CA::Transaction::commit:以後),CPU執行drawRect,通過context將數據寫入backing store
當backing store寫完後,通過render server交給GPU去渲染,將backing store中的bitmap數據顯示在屏幕上
所以在 drawRect 方法中 要首先獲取 context
代碼1
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; ZYYLayer *layer = [ZYYLayer layer]; layer.bounds = CGRectMake(0, 0, 100, 100); layer.position = CGPointMake(100, 100); [layer setNeedsDisplay]; [self.view.layer addSublayer:layer]; } @end
@implementation ZYYLayer - (void)drawInContext:(CGContextRef)ctx { CGContextAddEllipseInRect(ctx, CGRectMake(0,0,100,200)); CGContextSetRGBFillColor(ctx, 0, 0, 1, 1); CGContextFillPath(ctx); } @end
代碼二
#import@interface ViewController : UIViewController @end
#import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; CALayer *layer = [CALayer layer]; layer.bounds = CGRectMake(0, 0, 100, 100); layer.position = CGPointMake(100, 100); layer.delegate = self; [layer setNeedsDisplay]; [self.view.layer addSublayer:layer]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { CGContextAddEllipseInRect(ctx, CGRectMake(0,0,100,200)); CGContextSetRGBFillColor(ctx, 0, 0, 1, 1); CGContextFillPath(ctx); } @end
綜合以上2種不同的繪制函數加上uiview下的drawrect方法 一起區別 :
不能再將某個UIView設置為CALayer的delegate,因為UIView對象已經是它內部根層的delegate,再次設置為其他層的delegate就會出問題。
在設置代理的時候,它並不要求我們遵守協議,說明這個方法為非正式協議,就不需要再額外的顯示遵守協議了
我們回過頭思考 圖形的上下文 CGContextRef的創建歷程。
? addsubview 的時候 觸發的
? CPU會為layer分配一塊內存用來繪制bitmap,叫做backing store
? layer創建指向這塊bitmap緩沖區的指針,叫做CGContextRef
? 通過CoreGraphic的api,也叫Quartz2D,繪制bitmap
? 將layer的content指向生成的bitmap
其實 CGContextRef 的創建過程 就是CPU的工作過程
CPU這一塊最耗時的地方往往在Core Graphic的繪制上,關於Core Graphic的性能優化是另一個話題了,又會牽扯到很多東西,就不在這裡討論了
CPU 將view變成了bitmap 完成自己工作,剩下就是GPU的工作了。
GPU處理的單位是Texture
基本上我們控制GPU都是通過OpenGL來完成的,但是從bitmap到Texture之間需要一座橋梁,Core Animation正好充當了這個角色:
Core Animation對OpenGL的api有一層封裝,當我們的要渲染的layer已經有了bitmap content的時候,這個content一般來說是一個CGImageRef,CoreAnimation會創建一個OpenGL的Texture並將CGImageRef(bitmap)和這個Texture綁定,通過TextureID來標識。
這個對應關系建立起來之後,剩下的任務就是GPU如何將Texture渲染到屏幕上了。
整個過程也就是一件事:CPU將准備好的bitmap放到RAM裡,GPU去搬這快內存到VRAM中處理。
而這個過程GPU所能承受的極限大概在16.7ms完成一幀的處理,所以最開始提到的60fps其實就是GPU能處理的最高頻率。
因此,GPU的挑戰有兩個:
? 將數據從RAM搬到VRAM中
? 將Texture渲染到屏幕上
這兩個中瓶頸基本在第二點上。渲染Texture基本要處理這麼幾個問題:
Compositing是指將多個紋理拼到一起的過程,對應UIKit,是指處理多個view合到一起的情況,如
[self.view addsubview : subview]
如果view之間沒有疊加,那麼GPU只需要做普通渲染即可。 如果多個view之間有疊加部分,GPU需要做blending。
加入兩個view大小相同,一個疊加在另一個上面,那麼計算公式如下:
R = S+D*(1-Sa)
R: 為最終的像素值
S: 代表 上面的Texture(Top Texture)
D: 代表下面的Texture(lower Texture)
Sa代表Texture的alpha值。
其中S,D都已經pre-multiplied各自的alpha值。
假如Top Texture(上層view)的alpha值為1,即不透明。那麼它會遮住下層的Texture。即,R = S。是合理的。 假如Top Texture(上層view)的alpha值為0.5,S 為 (1,0,0),乘以alpha後為(0.5,0,0)。D為(0,0,1)。 得到的R為(0.5,0,0.5)。
基本上每個像素點都需要這麼計算一次。
因此,view的層級很復雜,或者view都是半透明的(alpha值不為1)都會帶來GPU額外的計算工作。
這個問題,主要是處理image帶來的,假如內存裡有一張400x400的圖片,要放到100x100的imageview裡,如果不做任何處理,直接丟進去,問題就大了,這意味著,GPU需要對大圖進行縮放到小的區域顯示,需要做像素點的sampling,這種smapling的代價很高,又需要兼顧pixel alignment。計算量會飙升。
如果我們對layer做這樣的操作:
label.layer.cornerRadius = 5.0f; label.layer.masksToBounds = YES;
會產生offscreen rendering,它帶來的最大的問題是,當渲染這樣的layer的時候,需要額外開辟內存,繪制好radius,mask,然後再將繪制好的bitmap重新賦值給layer。
因此繼續性能的考慮,Quartz提供了優化的api:
label.layer.cornerRadius = 5.0f; label.layer.masksToBounds = YES; label.layer.shouldRasterize = YES; label.layer.rasterizationScale = label.layer.contentsScale;
簡單的說,這是一種cache機制。
同樣GPU的性能也可以通過instrument去衡量:
紅色代表GPU需要做額外的工作來渲染View,綠色代表GPU無需做額外的工作來處理bitmap。
未完待續。。。
最近項目上需要實現藍牙傳輸apk的一個功能,能夠搜索周圍的藍牙手機並分享文件。從需求上講android手機自帶的藍牙傳輸模塊就可以滿足需要了,實現也很簡單。不過讓人頭疼的
在幾個月前,我第一次玩全民k歌,下載完app,它彈出來的引導頁吸引了我,不像以前的引導頁一樣千篇一律,而是用了視頻的方式,用一種動態的方式來實現。在今天,我突然又想起了這
相信大家已經對下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅滿目,然而有很多在我看來略有缺陷,接下來我將說明一下存在的缺陷問題,然後提供一種思路來解決這一缺陷,廢話不多
前幾天閒來無事,變想做一些小工具玩玩。花了一天多的時間,弄出一個簡單日歷的View。分為月份模式和星期模式。滾動查看,先上圖看看: 上面的是顯示的是月份的模