iOS中的卡頓優化
iOS中的屏幕成像原理
在講解卡頓優化之前,我們先來思考一下,在iOS中,屏幕是怎么成像的呢
CPU和GPU
在屏幕成像的過程中,CPU和GPU起着至關重要的作用
CPU(Central Processing Unit,中央處理器)CPU的主要任務是進行對象的創建和銷毀、對象屬性的調整、布局計算、文本的計算和排版、圖片的格式轉換和解碼、圖像的繪制(Core Graphics)
GPU(Graphics Processing Unit,圖形處理器)GPU的主要任務是對紋理的渲染##### CPU和GPU的關系
我們所看到的成像,都是通過CPU和GPU共同協作才能完成的
一般是經過CPU的計算和處理好的數據,交給GPU進行渲染,然后放到幀的緩存區,再被視頻控制器讀取,才能顯示到我們的屏幕上

在iOS中的幀緩存屬於雙緩沖機制,有前幀緩存和后幀緩存;GPU會分情況進行選取用哪塊緩存,這樣執行效率會更高一些
屏幕成像原理
在iOS中的屏幕成像是由許多幀共同組成的。每一幀都會由屏幕先發出一個垂直同步信號,然后再發出很多行水平同步信號,每一行水平同步信號表示處理完一行的數據,直到屏幕發完所有的水平同步信號,表示這一幀的數據全部處理完成了,再會進行下一輪的垂直同步信號的發出,表示即將處理下一幀的數據

卡頓產生的原因
在圖像處理過程中,CPU處理計算數據會消耗一定時間,然后再交由GPU,而GPU進行渲染也會花費時間,所以CPU和GPU都完成已經消耗了一定的時間;
而屏幕的垂直同步信號發出的時間如果正好是CPU、GPU處理完的時間,那么就會完好的先該幀圖像顯示出來;如果CPU、GPU處理的時間過長並且沒有完全處理完,而這時垂直同步信號已經發出了,那么就會讀取上一幀的數據進行展示,這種現象叫做掉幀;
而該幀沒有處理完的數據就只能等下一個垂直同步信號再進行讀取顯示了,這中間也會花費一定時間進行等待,這也是掉幀;
掉幀的現象就會造成卡頓,所以我們要想解決卡頓,就要盡量減少CPU和GPU的資源消耗
人眼感受不到卡頓的刷幀率平均是60FPS,表示每秒要刷60幀;通過計算相當於每隔16ms就會有一次VSync信號,也就是說我們要在16ms內完成CPU和GPU對數據的計算和渲染才行
優化卡頓的具體方案
關於CPU的卡頓優化
1.盡量用輕量級的對象
比如用不到事件處理的地方,可以考慮使用CALayer取代UIView
還有能用基本數據類型就不用對象類型等等
2.不要頻繁地調用UIView的相關屬性
比如frame、bounds、transform等屬性,盡量減少不必要的修改
盡量提前計算好布局,在有需要時一次性調整對應的屬性,不要多次修改屬性盡量減少使用Autolayout,Autolayout會比直接設置frame消耗更多的CPU資源
其他需要設置的屬性最后是能確定時再賦值,不要多次更改##### 3.圖片的size最好剛好跟UIImageView的size保持一致
如果圖片本身的大小和我們給予的大小有出入,CPU會去進行伸縮的處理,也是會消耗資源
4.控制一下線程的最大並發數量
不要過多的創建線程,線程的創建和消耗也是會消耗資源的
盡量保持較少數量的線程,設置好最大並發數
如果需要長期開啟線程來執行任務,可以考慮讓線程常駐,並再不需要后再進行統一銷毀
5.盡量把耗時的操作放到子線程
比如對文本的處理(尺寸計算、繪制),都可以放到異步去做處理,例如下面代碼
// 文字計算
[@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
// 文字繪制
[@"text" drawWithRect:CGRectMake(0, 0, 100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
還有對圖片的處理,對圖片的解碼和繪制都是會消耗性能的
我們經常使用的給UIImage賦值的方法,其本質是會去進行圖片的解碼和繪制的,所以我們可以將解碼繪制的過程放在子線程來處理,詳細代碼如下
// imageNamed:底層會進行對圖片的解碼和繪制
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [UIImage imageNamed:@"timg"];
// 換成如下方法
UIImageView *imageView = [[UIImageView alloc] init];
self.imageView = imageView;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 獲取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"timg"].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;
});
});
關於GPU的卡頓優化
1.盡量減少視圖數量和層次
比如一個UIView視圖我們需要創建三個圖層,減少到兩個或者一個更利於GPU的渲染性能
2.避免短時間內大量圖片的顯示
我們在處理多張圖片時,盡量避免短時間內大量圖片的顯示,盡可能將多張圖片合成一張進行顯示
3.GPU紋理尺寸的控制
GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸,就會占用CPU資源進行處理,所以紋理盡量不要超過這個尺寸
4.減少透明度
減少透明的視圖(alpha<1),不透明的就設置opaque為YES
像多個透明的視圖,如果有重疊部分,那么重疊部分需要重新計算展示的顏色是什么的,會消耗GPU資源
5.注意離屏渲染
在OpenGL中,GPU有2種渲染方式
- On-Screen Rendering:當前屏幕渲染,在當前用於顯示的屏幕緩沖區進行渲染操作
- Off-Screen Rendering:離屏渲染,在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作
離屏渲染消耗性能的原因- 本身GPU渲染就會消耗性能
- 需要創建新的緩沖區,又會消耗性能
- 離屏渲染的整個過程,需要多次切換上下文環境,先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕哪些操作會觸發離屏渲染?
- 光柵化
layer.shouldRasterize = YES - 遮罩
layer.mask - 圓角,同時設置
layer.masksToBounds = YES、layer.cornerRadius大於0,只滿足其中之一不會觸發離屏渲染- 考慮通過
CoreGraphics繪制裁剪圓角,或者叫美工提供圓角圖片- 陰影layer.shadowXXX - 如果設置了
layer.shadowPath就不會產生離屏渲染#### 卡頓的檢測
平時所說的“卡頓”主要是因為在主線程執行了比較耗時的操作##### 1.FPS監控
FPS的監控,參照YYKit中的YYFPSLabel,主要是通過CADisplayLink實現。借助link的時間差,來計算一次刷新刷新所需的時間,然后通過刷新次數 / 時間差得到刷新頻次,並判斷是否其范圍,通過顯示不同的文字顏色來表示卡頓嚴重程度。代碼實現如下
- 考慮通過
class LLFPSLabel: UILabel {
fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()
fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0
override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}
super.init(frame: f)
self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通過虛擬類
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
link.invalidate()
}
@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
//時間差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}
lastTime = link.timestamp
//刷新次數 / 時間差 = 刷新頻次
fps = Double(count) / detla
let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
count = 0
let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流暢
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡頓
fpsColor = UIColor.red
}
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
DispatchQueue.main.async {
self.attributedText = attrMStr
}
}
}
2.主線程卡頓監控我們可以添加Observer到主線程RunLoop中,通過監聽RunLoop狀態切換的耗時,以達到監控卡頓的目的
實現思路:
在整個運行循環中,需要監聽的主要就是RunLoop結束休眠到處理Source0的這段時間,如果時間過長,就證明有耗時操作
檢測主線程每次執行消息循環的時間,當這個時間大於規定的閾值時,就記為發生了一次卡頓。這個也是微信卡頓三方matrix的原理
以下是一個簡易版RunLoop監控的實現
class LLBlockMonitor: NSObject {
static let share = LLBlockMonitor.init()
fileprivate var semaphore: DispatchSemaphore!
fileprivate var timeoutCount: Int!
fileprivate var activity: CFRunLoopActivity!
private override init() {
super.init()
}
public func start(){
//監控兩個狀態
registerObserver()
//啟動監控
startMonitor()
}
}
fileprivate extension LLBlockMonitor{
func registerObserver(){
let controllerPointer = Unmanaged<LLBlockMonitor>.passUnretained(self).toOpaque()
var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: controllerPointer, retain: nil, release: nil, copyDescription: nil)
let observer: CFRunLoopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.allActivities.rawValue, true, 0, { (observer, activity, info) in
guard info != nil else{
return
}
let monitor: LLBlockMonitor = Unmanaged<LLBlockMonitor>.fromOpaque(info!).takeUnretainedValue()
monitor.activity = activity
let sem: DispatchSemaphore = monitor.semaphore
sem.signal()
}, &context)
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)
}
func startMonitor(){
//創建信號
semaphore = DispatchSemaphore(value: 0)
//在子線程監控時長
DispatchQueue.global().async {
while(true){
// 超時時間是 1 秒,沒有等到信號量,st 就不等於 0, RunLoop 所有的任務
let st = self.semaphore.wait(timeout: DispatchTime.now()+1.0)
if st != DispatchTimeoutResult.success {
//監聽兩種狀態kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting,
if self.activity == CFRunLoopActivity.beforeSources || self.activity == CFRunLoopActivity.afterWaiting {
self.timeoutCount += 1
if self.timeoutCount < 2 {
print("timeOutCount = \(self.timeoutCount)")
continue
}
// 一秒左右的衡量尺度 很大可能性連續來 避免大規模打印!
print("檢測到超過兩次連續卡頓")
}
}
self.timeoutCount = 0
}
}
}
}
使用時,直接調用即可
LLBlockMonitor.share.start()
也可以直接使用三方庫
Swift的卡頓檢測第三方ANREye,其主要思路是:創建子線程進行循環監測,每次檢測時設置標記置為true,然后派發任務到主線程,標記置為false,接着子線程睡眠超過閾值時,判斷標記是否為false,如果沒有,說明主線程發生了卡頓
OC可以使用微信matrix、滴滴DoraemonKit
iOS中的耗電優化
我們平時造成電量消耗的主要來源有哪些呢?
一般造成耗電來源有以下這些
- CPU的處理
- 網絡的連接
- 定位
- 圖像的展示和處理
耗電優化的一些具體方案
1.盡可能降低CPU、GPU功耗
詳情參照上面關於卡頓優化的相關處理
2.少用定時器
定時器的使用也會造成一定的電量消耗,因為要一直在程序中監聽執行
3.優化I/O操作(文件的讀寫)
盡量不要頻繁寫入小數據,最好批量一次性寫入
讀寫大量重要數據時,考慮用dispatch_io,其提供了基於GCD的異步操作文件I/O的API。用dispatch_io系統會優化磁盤訪問
數據量比較大的,建議使用數據庫(比如SQLite、CoreData),數據庫內部對讀寫已經做了相應的優化處理了#### 4.網絡優化
減少、壓縮網絡數據,可以采用JSON和protobuf這樣格式相對較小的傳輸格式
如果多次請求的結果是相同的,盡量使用緩存,可以利用NSCache來進行緩存使用斷點續傳,否則網絡不穩定時可能多次傳輸相同的內容
做好網絡狀態的監控,網絡不可用時,不要嘗試執行網絡請求
讓用戶可以取消長時間運行或者速度很慢的網絡操作,設置合適的超時時間
批量傳輸,比如,下載視頻流時,不要傳輸很小的數據包,直接下載整個文件或者一大塊一大塊地下載。如果下載廣告,一次性多下載一些,然后再慢慢展示。如果下載電子郵件,一次下載多封,不要一封一封地下載#### 5.定位優化如果只是需要快速確定用戶位置,最好用CLLocationManager的requestLocation方法。定位完成后,會自動讓定位硬件斷電
如果不是導航應用,盡量不要實時更新位置,定位完畢就關掉定位服務
盡量降低定位精度,比如盡量不要使用精度最高的kCLLocationAccuracyBest
需要后台定位時,盡量設置pausesLocationUpdatesAutomatically為YES,如果用戶不太可能移動的時候系統會自動暫停位置更新
盡量不要使用startMonitoringSignificantLocationChanges,優先考慮startMonitoringForRegion:#### 6.硬件檢測優化
用戶移動、搖晃、傾斜設備時,會產生動作(motion)事件,這些事件由加速度計、陀螺儀、磁力計等硬件檢測。在不需要檢測的場合,應該及時關閉這些硬件## APP的啟動優化
APP的啟動
我們先來了解一下APP的啟動有哪幾種
APP的啟動可以分為2種- 冷啟動(Cold Launch):從零開始啟動APP- 熱啟動(Warm Launch):APP已經在內存中,在后台存活着,再次點擊圖標啟動APP
我們對APP啟動時間的優化,主要是針對冷啟動進行優化
通過Xcode打印分析啟動過程
我們可以通過Xcode添加環境變量可以打印出APP的啟動時間分析
1.找到路徑Edit scheme -> Run -> Arguments -> Environment Variables
2.添加DYLD_PRINT_STATISTICS,設置為1

3.然后運行程序,可以看到控制台的打印如下

如果需要更詳細的信息,那就添加DYLD_PRINT_STATISTICS_DETAILS設置為1
然后查看控制台的打印如下

上述操作也僅僅是作為一個參考,如果啟動時間小於400ms,那就屬於正常范圍,如果超出該值,就需要考慮一定的啟動優化了
APP的啟動過程
APP的冷啟動可以概括為3大階段
- dyld- Runtime- main

dylddyld(dynamic link editor),Apple的動態鏈接器,可以用來裝載Mach-O文件(可執行文件、動態庫等)
啟動APP時,dyld所做的事情如下
啟動APP時,dyld會先裝載APP的可執行文件,同時會遞歸加載所有依賴的動態庫
當dyld把可執行文件、動態庫都裝載完畢后,會通知Runtime進行下一步的處理
Runtime
啟動APP時,Runtime所做的事情如下
Runtime會調用map_images進行可執行文件內容的解析和處理
進行各種objc結構的初始化(注冊Objc類 、初始化類對象等等)
在load_images中調用call_load_methods,調用所有Class和Category的+load方法
調用C++靜態初始化器和__attribute__((constructor))修飾的函數
到此為止,可執行文件和動態庫中所有的符號(Class,Protocol,Selector,IMP,…)都已經按格式成功加載到內存中,被Runtime所管理
main
整個啟動過程可以概述為:
APP的啟動由dyld主導,將可執行文件加載到內存,順便加載所有依賴的動態庫
並由Runtime負責加載成objc定義的結構
然后所有初始化工作結束后,dyld就會調用main函數
接下來就是UIApplicationMain函數,AppDelegate的application:didFinishLaunchingWithOptions:方法
APP的啟動優化方案
dyld階段
減少動態庫、合並一些動態庫(定期清理不必要的動態庫)
減少Objc類、分類的數量、減少Selector數量(定期清理不必要的類、分類)
減少C++虛函數數量(C++一旦有虛函數,就會多維護一張虛表)
Swift盡量使用struct
runtime加載階段用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++靜態構造器、ObjC的+load#### 執行main函數階段在不影響用戶體驗的前提下,盡可能將一些操作延遲,不要全部都放在finishLaunching方法中
按需加載## 安裝包瘦身
我們開發的安裝包(IPA)主要由可執行文件、資源組成
在我們日常開發中,項目業務會越來越多,慢慢就會積攢下一些不必要的代碼和資源,我們可以對其進行一定的瘦身優化
資源(圖片、音頻、視頻等)
我們在使用項目里的資源時,盡量采取無損壓縮的,會適當減少包的大小
當項目里的資源太多了,我們可以通過一些工具來清除無用的或者重復的資源
可執行文件瘦身
我們可以對編譯器做一定的優化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default這幾個配置都改為YES



去掉異常支持,Enable C++ Exceptions、Enable Objective-C Exceptions設置為NO, Other C Flags添加-fno-exceptions


我們可以利用一些工具對沒有使用的代碼進行清除
1.例如AppCode,檢測未使用的代碼:菜單欄 -> Code -> Inspect Code
2.編寫LLVM插件檢測出重復代碼、未被調用的代碼
3.通過生成Link Map文件,可以查看可執行文件的具體組成和大小分析
將Link Map File的路徑改成我們桌面路徑,然后將Write Link Map File改成Yes

然后會生成這么一個文件

由於其文件內容過於龐大,不利於我們分析,可借助第三方工具解析LinkMap文件:https://github.com/huanxsd/LinkMap
