背景
之前對iOS性能優(yōu)化總是碎片化了解,而且看了之后很快就忘記了。最近正好需要做一下技術(shù)調(diào)研,想要對于這個課題進(jìn)行深入整理,并且通過實際的應(yīng)用、結(jié)合項目,進(jìn)行理論與實踐相結(jié)合。
iOS性能優(yōu)化相對復(fù)雜、內(nèi)容相關(guān)性廣、涉及面也很廣,接下來通過對其的了解,逐步進(jìn)行分析和總結(jié)。

這是我總結(jié)的性能優(yōu)化內(nèi)容,我會從以上幾個方面進(jìn)行整理。
- 首先我會對卡頓優(yōu)化進(jìn)行整理,卡頓的現(xiàn)象是什么?為什么會發(fā)生卡頓?我們要怎么解決卡頓?有哪些解決思路?然后通過實際應(yīng)用,看看如果在項目開發(fā)的時候避免卡頓。
- 接下來也是比較重要的:內(nèi)存優(yōu)化。我們?yōu)楹我M(jìn)行內(nèi)存優(yōu)化?內(nèi)存是怎么被消耗的?然后說一下內(nèi)存的管理模型,通過內(nèi)存管理模型我們進(jìn)行內(nèi)存的分析,最后結(jié)合實際,說說我們具體在項目中如何避免內(nèi)存泄漏。
- 然后說一下啟動優(yōu)化。什么是冷啟動?什么是熱啟動?他們的區(qū)別是什么?APP啟動的具體流程是什么?最后通過實際整理,說一下啟動優(yōu)化的具體思路。
- 接著說一下電量優(yōu)化。
- 最后說一下安裝包瘦身。
CPU 占用率、 內(nèi)存使用情況、啟動時間、卡頓、FPS、使用時崩潰率、耗電量監(jiān)控、流量監(jiān)控、網(wǎng)絡(luò)狀況監(jiān)控、等等。
一、卡頓優(yōu)化
APP卡頓指的是在APP運(yùn)行過程中出現(xiàn)的掉幀現(xiàn)象。
什么是掉幀現(xiàn)象?為什么會出現(xiàn)掉幀,那我們就要先從UI圖像的顯示原理說起。
1、UI圖像顯示原理
我在之前講解視圖相關(guān)的知識的時候已經(jīng)有過講解。(PS:具體查閱
http://www.itdecent.cn/p/08a19fc1068f)

大致總結(jié)就是:CPU和GPU通過渲染總線,把需要顯示的圖像放在幀緩沖區(qū),然后視頻控制器在V-Sycn信號到來之后把圖像放在顯示器上進(jìn)行顯示,這樣就形成了一幀圖像。如果在V-Sycn信號到來的時候,CPU和GPU沒有把需要顯示的圖像放在幀緩沖區(qū),那這幀就無法在顯示器上顯示,從而發(fā)生丟幀現(xiàn)象。
我們應(yīng)該知道保證操作流暢不卡頓,應(yīng)該保證屏幕圖像的刷新率在60HZ/s,也就是說一秒要生成對應(yīng)60幀對應(yīng)的圖像(目前iPadpro已經(jīng)實裝了120HZ/S的高刷,體驗相當(dāng)絲滑),如果發(fā)生丟幀現(xiàn)象,那么刷新率就會降到60HZ/s以下,于是就出現(xiàn)了卡頓,APP的體驗也就瞬間下滑。
2、如何解決卡頓問題
一個優(yōu)秀的APP使用起來肯定是絲般順滑,如果想讓你的APP體驗上一個臺階,那么卡頓的問題是肯定要避免的,接下來我們要說一說如何解決卡頓的問題。
解決思路
通過上面對于UI圖像顯示原理的說明,我們可以看出圖像的生成是主要依賴CPU和GPU共同協(xié)作完成。是否在V-Sycn信號來之前成功把生成的圖像放在幀緩沖區(qū)是非常重要的,所以我們要合理安排CPU和GPU的工作,讓幀圖像的生成有條不紊的進(jìn)行。通過對CPU和GPU的工作優(yōu)化,從而優(yōu)化卡頓問題就是我們解決卡頓的思路。
首先我們分別看一下CPU和GPU在 “圖像” 方面的工作內(nèi)容。
CPU

從上圖可以了解到CPU的工作內(nèi)容大致分為:對象創(chuàng)建、對象調(diào)整、對象銷毀、布局計算。那么我們就從以上四個方面逐步分析,看看如何在這四方面進(jìn)行工作的優(yōu)化。
(1)對象創(chuàng)建
對象的創(chuàng)建會分配內(nèi)存、調(diào)整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優(yōu)化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應(yīng)觸摸事件的控件,用 CALayer 顯示會更加合適。如果對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建,但可惜的是包含有 CALayer 的控件,都只能在主線程創(chuàng)建和操作。通過 Storyboard 創(chuàng)建視圖對象時,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術(shù)選擇。
盡量推遲對象創(chuàng)建的時間,并把對象的創(chuàng)建分散到多個任務(wù)中去。盡管這實現(xiàn)起來比較麻煩,并且?guī)淼膬?yōu)勢并不多,但如果有能力做,還是要盡量嘗試一下。如果對象可以復(fù)用,并且復(fù)用的代價比釋放、創(chuàng)建新對象要小,那么這類對象應(yīng)當(dāng)盡量放到一個緩存池里復(fù)用。
(2)對象調(diào)整
對象的調(diào)整也經(jīng)常是消耗 CPU 資源的地方。這里特別說一下 CALayer:CALayer 內(nèi)部并沒有屬性,當(dāng)調(diào)用屬性方法時,它內(nèi)部是通過運(yùn)行時 resolveInstanceMethod 為對象臨時添加一個方法,并把對應(yīng)屬性值保存到內(nèi)部的一個 Dictionary 里,同時還會通知 delegate、創(chuàng)建動畫等等,非常消耗資源。UIView 的關(guān)于顯示相關(guān)的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進(jìn)行調(diào)整時,消耗的資源要遠(yuǎn)大于一般的屬性。對此你在應(yīng)用中,應(yīng)該盡量減少不必要的屬性修改。
當(dāng)視圖層次調(diào)整時,UIView、CALayer 之間會出現(xiàn)很多方法調(diào)用與通知,所以在優(yōu)化性能時,應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
(3)對象銷毀
對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當(dāng)容器類持有大量對象時,其銷毀時的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。
(4)布局計算與繪制
視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進(jìn)行緩存,那么這個地方基本就不會產(chǎn)生性能問題了。
不論通過何種技術(shù)對視圖進(jìn)行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調(diào)整上。上面也說過,對這些屬性的調(diào)整非常消耗資源,所以盡量提前計算好布局,在需要時一次性調(diào)整好對應(yīng)屬性,而不要多次、頻繁的計算和調(diào)整這些屬性.
Autolayout 是蘋果本身提倡的技術(shù),在大部分情況下也能很好的提升開發(fā)效率,但是 Autolayout 對于復(fù)雜視圖來說常常會產(chǎn)生嚴(yán)重的性能問題。隨著視圖數(shù)量的增長,Autolayout 帶來的 CPU 消耗會呈指數(shù)級上升。 如果你不想手動調(diào)整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。
除了文本計算,CPU還負(fù)責(zé)了繪制(Display)的工作(具體我其他文章也介紹UIView的繪制原理,可以參考一下),之后做一些準(zhǔn)備工作(PrePare),然后把對應(yīng)的位圖提交到GPU上面(Commit)
- layout: UI布局和文本計算包括控件Frame的設(shè)置,對于控件的文字或者size的計算。
- Display: 就是繪制過程,drawRect方法發(fā)生在這一步驟
- PrePare: 準(zhǔn)備階段,假如有UIImageView,那么設(shè)置它的image的時候,圖片是不能直接顯示到屏幕上去的,需要對圖片進(jìn)行解碼,解碼的動作就發(fā)生在這一過程當(dāng)中
- Commit: 對CPU最終的輸出結(jié)果位圖進(jìn)行提交。
GPU

從上圖可以了解到CPU的工作內(nèi)容大致分為:頂點著色器、形狀裝配
、幾何著色器、光柵化、片段著色器、測試與混合。
(1)頂點著色器(Vertex Shader)
該階段輸入的是頂點數(shù)據(jù)(Vertex Data),頂點數(shù)據(jù)是一系列頂點的集合。頂點著色器主要的目的是把 3D 坐標(biāo)轉(zhuǎn)為 “2D” 坐標(biāo),同時頂點著色器可以對頂點屬性進(jìn)行一些基本處理。
( 一句話簡單說,確定形狀的點。)
(2)形狀(圖元)裝配(Shape Assembly)
該階段將頂點著色器輸出的所有頂點作為輸入,并將所有的點裝配成指定圖元的形狀。
圖元(Primitive) 用于表示如何渲染頂點數(shù)據(jù),如:點、線、三角形。
這個階段也叫圖元裝配。
( 一句話簡單說,確定形狀的線。)
(3)幾何著色器(Geometry Shader)
該階段把圖元形式的一系列定點的集合作為輸入,通過生產(chǎn)新的頂點,構(gòu)造出全新的(或者其他的)圖元,來生成幾何形狀。
( 一句話簡單說,確定三角形的個數(shù),使之變成幾何圖形。)
(4)光柵化(Rasterization)
該階段會把圖元映射為最終屏幕上相應(yīng)的像素,生成片段。片段(Fragment) 是渲染一個像素所需要的所有數(shù)據(jù)。
( 一句話簡單說,將圖轉(zhuǎn)化為一個個實際屏幕像素。)
(5)片段著色器(Fragment Shader)
該階段首先會對輸入的片段進(jìn)行裁切(Clipping)。裁切會丟棄超出視圖以外的所有像素,用來提升執(zhí)行效率。并對片段(Fragment)進(jìn)行著色。
( 一句話簡單說,對屏幕像素點著色。)
(6)測試與混合(Tests and Blending)
該階段會檢測片段的對應(yīng)的深度值(z 坐標(biāo)),來判斷這個像素位于其它圖層像素的前面還是后面,決定是否應(yīng)該丟棄。此外,該階段還會檢查 alpha 值( alpha 值定義了一個像素的透明度),從而對圖層進(jìn)行混合。
( 一句話簡單說,檢查圖層深度和透明度,并進(jìn)行圖層混合。)
通過這六個步驟逐步生成一個對應(yīng)的圖像,最終把圖像提交到幀緩沖區(qū)當(dāng)中去。
其中前五步都是固定的流水線作業(yè),我們無法干預(yù),但是通過第六點我們可以對GPU的工作進(jìn)行優(yōu)化。
在測試與混合中由于有不同的圖層混合,而且會有不同的alpha指定,所以最終生成的像素顏色不一樣,從而會出現(xiàn)不同的圖像內(nèi)容。
關(guān)于混合,GPU采用如下公式進(jìn)行計算,并得出最后的實際像素顏色。
R = S + D * (1 - Sa)
含義:
R:Result,最終像素顏色。
S:Source,來源像素(上面的圖層像素)。
D:Destination,目標(biāo)像素(下面的圖層像素)。
a:alpha,透明度。
結(jié)果 = S(上)的顏色 + D(下)的顏色 * (1 - S(上)的透明度)
可以看出幾個比較關(guān)鍵的點S、D、A. 我們可以圍繞這幾個點進(jìn)行進(jìn)行優(yōu)化。
1、盡量減少透視圖的數(shù)量和層次;
2、減少透明的視圖(alpha < 1),不透明的就設(shè)置 opaque 為 YES;
3、盡量避免離屏渲染(離屏渲染在之前的文章講解過)
離屏渲染的整個過程,需要多次切換上下文環(huán)境,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),渲染結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上,上下文環(huán)境從離屏切換到當(dāng)前屏幕,這個過程會造成性能的消耗。
最終也會增加圖像的層級,最終混合的時候會產(chǎn)生性能消耗。
3、解決辦法總結(jié)
通過上面的講解,對于卡頓優(yōu)化的方法大概總結(jié)如下:
CPU層面
1、盡量用輕量的對象代替重量的對象。
CALayer * topLayer = [CALayer layer];
topLayer.frame = CGRectMake(100, 100, 100, 100);
topLayer.cornerRadius = 50;
topLayer.masksToBounds = NO;
topLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.view.layer addSublayer:topLayer];
2、對象不涉及 UI 操作,則盡量放到后臺線程去創(chuàng)建。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 處理耗時操作的代碼塊...
//通知主線程刷新
dispatch_async(dispatch_get_main_queue(), ^{
//回調(diào)或者說是通知主線程刷新UI,
});
});
3、通過 Storyboard 創(chuàng)建視圖對象時,其資源消耗會比直接通過代碼創(chuàng)建對象要大非常多,在性能敏感的界面里,盡量使用純代碼進(jìn)行開發(fā)。
4、如果對象可以復(fù)用,并且復(fù)用的代價比釋放、創(chuàng)建新對象要小,那么這類對象應(yīng)當(dāng)盡量放到一個緩存池里復(fù)用(比如TableViewCell的復(fù)用規(guī)則)。
5、應(yīng)該盡量減少不必要的屬性修改。
6、應(yīng)該盡量避免調(diào)整視圖層次、添加和移除視圖。
7、如果對象可以放到子線程去釋放,那就挪到子線程去。
8、在子線程中預(yù)排版(布局計算,文本計算),讓主線程有更多的時間去響應(yīng)用戶的交互。
9、預(yù)渲染(文本等異步繪制,圖片編解碼等)。
GPU層面
1、UIView盡量設(shè)置為不透明。
2、盡量設(shè)置UIView的背景色。
3、經(jīng)我測試,設(shè)置shadowOpacity的透明度會發(fā)生離屏渲染,cornerRadius+clipsToBounds未發(fā)生離屏渲染,但是如果加了子視圖再設(shè)置clipsToBounds的時候就會發(fā)生離屏渲染。
4、UIImageView中只設(shè)置圖片和maskToBounds / clipsToBounds不會觸發(fā)離屏渲染,除非再設(shè)置背景色就會離屏渲染
UIImageView * aview = [[UIImageView alloc] init];
aview.frame = CGRectMake(100,100, 100, 100);
aview.layer.cornerRadius = 50;
aview.backgroundColor = [UIColor redColor];//如果設(shè)置背景色就會產(chǎn)生離屏渲染
aview.image = [UIImage imageNamed:@"ailitype_icon"];
aview.layer.masksToBounds = YES;
aview.clipsToBounds = YES;
5、善用離屏渲染(合理利用shouldRasterize屬性)。
如果一定會發(fā)生麗萍渲染就一定要設(shè)置shouldRasterize的屬性,shouldRasterize的主旨在于降低性能損失,但總是至少會觸發(fā)一次離屏渲染。如果你的layer本來并不復(fù)雜,打開這個開關(guān)反而會增加一次不必要的離屏渲染。
PS:
①如果layer 不能被復(fù)用,沒有必要開啟光柵化。
②如果layer不是靜態(tài),頻繁被修改,比如在動畫中,開始反而影響效率
③緩存內(nèi)容時間限制,100ms內(nèi)沒被使用,自動丟棄
④緩存空間有限,最大不超過屏幕的2.5倍
//以下代碼大家可以自行測試
UIView * aview = [[UIView alloc] init];
aview.frame = CGRectMake(100,100, 100, 100);
aview.layer.cornerRadius = 50;
aview.layer.masksToBounds = NO;
aview.layer.shadowColor = [UIColor greenColor].CGColor;
aview.layer.shadowRadius = 4.0;
//aview.layer.shadowOpacity = 1; //設(shè)置陰影透明度會發(fā)生離屏渲染
aview.layer.shadowOffset = CGSizeMake(5, 5);
aview.backgroundColor = [UIColor redColor];
//aview.layer.shouldRasterize = YES;//使用了光柵化就一定會發(fā)生離屏渲染
[self.view addSubview:aview];
5、設(shè)置layer的mask的時候會發(fā)生離屏渲染
UIImageView * aview = [[UIImageView alloc] init];
aview.frame = CGRectMake(100,100, 100, 100);
aview.backgroundColor = [UIColor clearColor];
[self.view addSubview:aview];
CALayer * topLayer = [CALayer layer];
topLayer.frame = CGRectMake(10, 10, 80, 80);
topLayer.cornerRadius = 50;
topLayer.masksToBounds = NO;
topLayer.contents = (id)[UIImage imageNamed:@"ailitype_icon"].CGImage;
topLayer.contentsGravity = kCAGravityResizeAspect;
topLayer.backgroundColor = [UIColor greenColor].CGColor;
aview.layer.mask = topLayer; //設(shè)置成mask的時候會發(fā)生離屏渲染
PS:如何監(jiān)測離屏渲染
1、模擬器打開

2、真機(jī)Instrument-選中Core Animation-勾選Color Offscreen-Rendered Yellow
總結(jié)下來我們在開發(fā)的過程中,要注意對象的管理、耗時操作的處理、和UI相關(guān)的問題。
為了保持UI的流暢度我們也可以借助于三方庫。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 開源的一個用于保持 iOS 界面流暢的庫
這個三方庫我在之前的文章中提到過。http://www.itdecent.cn/p/08a19fc1068f
4、卡頓檢測
在實際的項目盡量要做一下卡頓檢測,為了卡頓能直接可視化檢測,我整理了一份代碼,可以直接在項目中使用,其原理就是利用RunLoop的機(jī)制,通過監(jiān)聽RunLoop狀態(tài)切換時間來檢測是否卡頓,如果沒有達(dá)到就是出現(xiàn)了卡頓。
具體地址如下:
一、內(nèi)存優(yōu)化
在開發(fā)過程中對于內(nèi)存的管理和優(yōu)化是非常重要的,合理的內(nèi)存資源分配和使用是一門高深的技術(shù)。其最主要的目的是如何高效,快速的分配,并且在適當(dāng)?shù)臅r候釋放和回收內(nèi)存資源。
內(nèi)存是什么?它是什么樣的?接下來我們就看看內(nèi)存的布局。
1、內(nèi)存布局
關(guān)于內(nèi)存的布局我整理了一份圖片

從圖中我們可以看到,在iOS中內(nèi)存是一塊相對復(fù)雜的區(qū)域,這塊區(qū)域從低地址到高地址,不同的地址段的職責(zé)也是不一樣的。
首先程序進(jìn)行加載,然后進(jìn)行內(nèi)存的分配:
從低到高我們說一下內(nèi)存分配了哪些內(nèi)容:
1、保留段:系統(tǒng)保留的一塊內(nèi)存區(qū)域。
2、代碼區(qū)(.text):存儲編譯后的代碼區(qū)域,而且還包括了操作碼和要操作的對象的地址引用。
3、常量區(qū):存儲已使用的字符串常量(比如const、extern修飾的字符串),程序結(jié)束后由系統(tǒng)釋放,相同字符串的地址是一致的。
4、全局(靜態(tài))區(qū)(.bbs/data):存儲全局變量或者靜態(tài)變量(static),程序結(jié)束后由系統(tǒng)進(jìn)行釋放。static int a:未初始化的全局靜態(tài)變量,存放在全局(.bbs)段。static int a = 10:已初始化的全局靜態(tài)變量,存放在全局data段當(dāng)中。
PS:可以看到當(dāng)程序加載到內(nèi)存中的時候,全局區(qū)、常量區(qū)、代碼區(qū)、是已經(jīng)分配好的。
5、堆區(qū):iOS存放的對象都是存在堆區(qū)的,由開發(fā)者進(jìn)行管理(ARC下是開發(fā)者不回收,由系統(tǒng)自行回收)。速度相對較慢,但是操作靈活(是由鏈表結(jié)構(gòu)進(jìn)行管理的),所以會造成內(nèi)存碎片話,分配的地址是由低到高的。
6、棧區(qū):用來存放參數(shù)值,局部變量值,對象的指針值,由系統(tǒng)自行進(jìn)行分配和釋放,不需要開發(fā)者進(jìn)行管理。快速高效,但是操作不是很靈活(在內(nèi)存中是連續(xù)的),分配的地址是從高到低的。
PS:堆區(qū)是從低到高進(jìn)行分配,棧區(qū)是從高到低分配,一旦堆區(qū)和棧區(qū)相遇,就會發(fā)生堆棧溢出。堆區(qū)和棧區(qū)是在程序運(yùn)行的過程中所產(chǎn)生的。
7、內(nèi)核區(qū):內(nèi)核所占用的內(nèi)存空間。
通過上面的圖片我們還可以了解到:獲取對象值的查找過程是通過棧中對應(yīng)的對象在堆中的地址進(jìn)行具體查找的,具體表現(xiàn)形式如下:
int a = 1; //局部變量 存儲在棧中
int b = 2; //局部變量 存儲在棧中
Palindrome * c = [[Palindrome alloc] init]; //創(chuàng)建對象 地址存儲在棧中,實際值是存儲在堆中的
