1 卡頓產生的原因及優化
產生卡頓是由于屏幕的成像顯示導致,而屏幕畫面的顯示離不開手機的CPU和GPU;
CPU:(Central Processing Unit 中央處理器)
對象的創建和銷毀,對象屬性的調整,布局的計算,文本的布局計算和排版,圖片格式的轉換和解碼,圖像的繪制(Core Graphics)
GPU: (Graphics Processing Unit 圖形處理器)
紋理的繪制
iOS是雙幀緩存機制,有前幀緩存,后幀緩存
1.1屏幕成像顯示的過程是:
- CPU先計算出圖像的布局,大小,位置等信息;(CPU計算出來的數據是不能直接顯示到屏幕上的)
- GPU將CPU計算的數據,渲染到幀緩存中;
- 要顯示圖像的時候,視頻控制器從幀緩存中讀取圖像,顯示到屏幕上;
1.2 屏幕成像顯示的原理:
iPhone的刷幀頻率是 60 FPS,也就是每秒顯示60幀數據;
每幀圖像顯示的時間間隔是: 1000ms / 60 fps = 16ms;
如圖:
屏幕在顯示一幀數據的時候:
- 會先發送一條垂直同步信號
- 然后會 從上至下 發送水平同步信號,填充整個屏幕,顯示這一幀的數據;
重點:每隔16ms就會顯示下一幀數據,接收到 垂直同步信號 代表開始顯示下一幀的內容
1.3 顯示和卡頓產生的根本原因:
之前介紹,屏幕成像在CPU計算和GPU渲染到幀緩存區之后,再由視頻控制器讀取并顯示到屏幕上。
如下圖所示:
1、2、3、4、5 代表5幀數據的顯示流程,
其中紅色箭頭代表CPU計算所用時間,藍色箭頭代表GPU渲染所用時間;
- 第一幀 1:CPU和GPU所花的時間 ==16ms,所以在 垂直同步信號到來的時候,幀緩存中有完整的數據,正常展示;
- 第二幀 2:CPU和GPU所花的時間 < 16ms, 超前將要顯示的內容繪制到幀緩存中,正常展示;
-
第三幀 3:CPU和GPU所花的時間 > 16ms, 16ms內這一幀的數據還沒渲染完成,垂直同步信號已經到來,幀緩存中的數據不全,這一幀會繼續顯示上一幀(第二幀)的內容;
所以這就是卡頓的原因; - 第四幀 4:在這一次的顯示中,第三幀的內容CPU和GPU渲染剛完成,當垂直信號到來時,去幀緩存中去讀取并直接顯示 第三幀 的內容;
- 第五幀 5:同第二幀,正常展示;
由上圖直接展示了卡頓產生的 根本原因:
在一幀顯示的頻率16ms中,如果CPU和GPU沒有將要顯示的內容渲染到幀緩存中,當前垂直同步信號到來的時候,就會顯示上一幀的內容;
這一幀的內容,會在下一個周期16ms后,垂直同步信號再次到來的時候,顯示到屏幕上。
1.4 解決卡頓的方式CPU和GPU:
CPU:
1、使用輕量級的對象:比如不用點擊的地方,使用CALayer代替UIView;
2、不要頻繁的修改屬性:frame,bounds,transfrom等,這些都需要CPU的計算;
3、盡量提前計算好布局:計算好frame,bounds等,一次性修改,不要多次修改;
4、使用AutoLayout比直接設置frame消耗更多的資源;
5、圖片的size最好和UIImageView的size保持一致,這樣就不用耗費CPU資源去進行縮放操作;
6、控制線程的最大并發數量:比如說3,不要無限制的開辟新的線程;
7、盡量耗時操作放到子線程:
- 文本的計算(高度),繪制(排版)等
// 文字計算
[@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
// 文字繪制
[@"text" drawWithRect:CGRectMake(0, 0, 100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
- 圖片的解碼、繪制
正常圖片的展示:imageView.image = [UIImage imageNamed:@"test.png"];
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 100, 56);
imageView.image = [UIImage imageNamed:@"test.png"];
[self.view addSubview:imageView];
其實正常圖片的顯示,不是直接展示到屏幕上的,需要解碼成能夠展示的二進制數據,而這個解碼的過程,可以異步的放到子線程中去做:
- (void)viewDidLoad {
[super viewDidLoad];
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 100, 56);
[self.view addSubview:imageView];
self.imageView = imageView;
[self image]; //異步解碼圖片,解碼成功后再回主線程展示
}
- (void)image{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 獲取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"test.png"].CGImage;
// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
// draw
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
// get CGImage
cgImage = CGBitmapContextCreateImage(context);
// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];
// release
CGContextRelease(context);
CGImageRelease(cgImage);
// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});
}
其實就是將image轉化成CGImage,然后將CGImage解碼,首先創建一個上下文,通過drawImage方法將image畫到上下文context完成解碼操作,然后從上下文獲取解碼后的圖片;
GPU:
1、盡量減少視圖的數量和層級:多層次的視圖繪制更占用GPU資源;
2、盡量避免短時間大量圖片的顯示:可以合成為一張圖片展示;
3、GPU能處理的圖片的最大尺寸是4096x4096,盡量不要超過這個尺寸;
4、減少透明視圖的使用 alpha < 1,
重疊部分:有透明度:需要混合計算;不透明:計算一次(最上層的顏色)
5、避免離屏渲染:
離屏渲染:
當前屏幕渲染:(On-Screen Rendering)在當前顯示的屏幕緩沖區進行操作;
離屏渲染: (Off-Screen Rendering)在當前屏幕緩沖區以外,開辟一個新的緩沖區;
離屏渲染消耗性能的原因:
需要開辟新的緩沖區;
需要多次切換上下文狀態:從當前屏幕(On-Screen)切換到離屏(Off-Screen),等離屏渲染結束以后,又要從離屏切換到當前屏幕;
哪些操作會觸發離屏渲染?
1、光柵化:layer.shouldRasterize = YES;
2、遮罩:layer.mask;
-
3、圓角,同時設置layer.masksToBounds = YES、layer.cornerRadius大于0;
解決辦法:考慮通過CoreGraphics繪制裁剪圓角,或者叫美工提供圓角圖片;
4、陰影,layer.shadowXXX;
如果設置了layer.shadowPath就不會產生離屏渲染(不設置路徑默認是圍繞這個view)
卡頓檢測:
平時所說的“卡頓”主要是因為在主線程執行了比較耗時的操作
可以添加Observer到主線程RunLoop中,通過監聽RunLoop狀態切換的耗時,以達到監控卡頓的目的
耗電優化
耗電的主要來源:
- CPU處理計算
- 網絡請求
- 定位
- 圖形的處理
耗電優化的處理:
1、 盡可能減少CPU和GPU的消耗;
-
2、 優化I/O操作:
- 盡量不要頻繁的讀寫小數據,可以批量一次性寫入
- 讀寫大量重要數據時,考慮用dispatch_io,其提供了基于GCD的異步操作文件I/O的API。用dispatch_io系統會優化磁盤訪問
- 數據量比較大的,建議使用數據庫(比如SQLite、CoreData)
-
3、網絡優化:
- 減少、壓縮網絡數據
- 如果多次請求的結果是相同的,盡量使用緩存
- 使用斷點續傳,否則網絡不穩定時可能多次傳輸相同的內容
- 網絡不可用時,不要嘗試執行網絡請求
- 讓用戶可以取消長時間運行或者速度很慢的網絡操作,設置合適的超時時間
- 批量傳輸,比如,下載視頻流時,不要傳輸很小的數據包,直接下載整個文件或者一大塊一大塊地下載。如果下載廣告,一次性多下載一些,然后再慢慢展示。如果下載電子郵件,一次下載多封,不要一封一封地下載
-
4、定位優化
- 如果只是需要快速確定用戶位置,最好用CLLocationManager的requestLocation方法。
- 定位完成后,會自動讓定位硬件斷電
- 如果不是導航應用,盡量不要實時更新位置,定位完畢就關掉定位服務
- 盡量降低定位精度,比如盡量不要使用精度最高的kCLLocationAccuracyBest
- 需要后臺定位時,盡量設置pausesLocationUpdatesAutomatically為YES,如果用戶不太可能移動的時候系統會自動暫停位置更新
- 盡量不要使用startMonitoringSignificantLocationChanges,優先考慮startMonitoringForRegion: