【OC梳理】效能檢測及優化彙總

gzhongcheng發表於2018-10-22

啟動時間

啟動時間可謂是使用者對你的APP的第一印象,啟動時間過長很可能會讓使用者直接把APP打入冷宮。蘋果的watch dog機制(Xcode在debug模式下是沒有開啟watch dog的)也會kill掉啟動時間過長的APP,這種情況下給使用者的感覺就是這APP怎麼一啟動就卡死然後崩潰了。 首先大概瞭解一下APP的啟動過程:

app啟動過程
我們計算的啟動時間就是從main()applicationDidBecomeActive:的時間。 在Xcode的Edit scheme中增加DYLD_PRINT_STATISTICS這個環境變數,設定值為YES,如下圖所示:
【OC梳理】效能檢測及優化彙總
執行專案後在控制檯會列印出每個階段都耗時多少:
【OC梳理】效能檢測及優化彙總
main()呼叫之後的載入時間:
1.準備階段,主要是圖片的解碼
2.佈局階段,-(void)layoutSubViews()
3.繪製階段,-(void)drawRect:(CGRect)rect
4.啟動階段必要服務的啟動、必要資料的建立和讀取。

優化啟動時間:

1.內嵌的dylib儘可能少,或者合併起來。

2.Rebase/Binding減少__DATA中需要修正的指標。 對於oc來說減少 class, selector, category 這些後設資料的數量,對與c++來說,減少虛擬函式數量。swift結構體需要修正的比較少。

3.將不必須在+load中做的事延遲到+ initialize中。

4.不使用xib,直接用程式碼載入首頁檢視。

5.release版不要用NSLog輸出。

6.啟動時的網路請求儘可能非同步。


記憶體佔用量,記憶體告警次數

我們知道,一個程式佔用的記憶體空間,包含5種不同的資料區:

BSS段:通常是存放未初始化的全域性變數;

資料段:通常是存放已初始化的全域性變數。

程式碼段:通常是存放程式執行程式碼。

堆:通常是用於存放程式執行中被動態分配的記憶體段,OC物件(所有繼承自NSObject的物件)就存放在堆裡。

棧:由編譯器自動分配釋放,存放函式的引數值,區域性變數等值。 棧記憶體是系統來管理的,因此我們常說的記憶體管理,指的是堆記憶體的管理,也就是所有OC物件的建立和銷燬的管理。 在iOS應用中的記憶體洩露,原因一般有迴圈引用、錯用Strong/copy等。

檢測方法

Analyze — 靜態分析

使用command + shift + B編譯專案,或者點選Xcode - Product - Analyze即可使用。 Analyze通常用於檢測常見的三種洩露情形:

  • 建立了一個物件,但是並沒有使用。Xcode提示資訊:Value Stored to 'XXX' is never read。
  • 建立了一個(指標可變的)物件,且初始化了,但是初始化的值一直沒讀取過。Xcode提示資訊:Value Stored to 'str' during its initialization is never read
  • 呼叫了讓某個物件引用計數加1的函式,但沒有呼叫相應讓其引用計數減1的函式。Xcode提示資訊:Potential leak of an object stored into 'subImageRef'

Leaks — 記憶體洩露

Leaks是動態的記憶體洩露檢查工具,需要一邊執行程式,一邊檢測。 使用command + control + I調出Instruments,然後選擇Leaks即可調出Leaks介面:

【OC梳理】效能檢測及優化彙總
【OC梳理】效能檢測及優化彙總

Allocations — 記憶體分配

Allocations是檢測程式執行過程中的記憶體分配情況的,也在Instruments工具列表中,介面如下:

【OC梳理】效能檢測及優化彙總
【OC梳理】效能檢測及優化彙總
右鍵就可以開啟Xcode自動定位到相關佔用記憶體方法的程式碼上,根據這些資訊,可以對程式裡不同程式碼的記憶體佔用情況有一些認識,並進行鍼對性的優化。
【OC梳理】效能檢測及優化彙總
重複的執行一系列的操作時候記憶體不會繼續增加,比如開啟和關閉一個視窗,這樣的操作,每一次操作的前後,記憶體應該是相同的,通過多次迴圈操作,記憶體不會遞增下去,通過這種分析結果,觀察記憶體分配趨勢,當發現不正確的結果或者矛盾的結果,就可以研究是不是Abandoned Momory的問題,並可以修正這個問題了:
【OC梳理】效能檢測及優化彙總

Zombies — 殭屍物件

Zombies是檢測殭屍物件的工具,也在Instruments,用於定位殭屍物件導致的崩潰:

【OC梳理】效能檢測及優化彙總
開啟Zombies並執行,直至崩潰,如果是殭屍物件導致,則可以通過Zombies定位到崩潰的位置:
【OC梳理】效能檢測及優化彙總


CPU使用率

影響CPU使用情況的主要是計算密集型的操作,比如動畫、佈局計算和Autolayout、文字的計算和渲染、圖片的解碼和繪製。比較常見的一種優化方式就是快取tableview的cell高度,避免每次計算。想要降低CPU的使用率就要儘量避免大量的計算,能快取的快取,不得不計算的,看看是否可以使用一些演算法進行優化,降低時間複雜度。 CPU方面的優化還可以參見iOS 保持介面流暢的技巧

檢測方式

  • 使用Xcode除錯程式時可以在Debug Session介面中看到CPU的使用率
  • 使用Instruments中的Activity Monitor(必須使用真機)監控程式級別的CPU,記憶體,磁碟,網路使用情況

頁面渲染時間

對於靜態頁面來講,頁面的渲染時間就是從viewDidLoad第一行到viewDidAppear最後一行程式碼的時間。但是大多數頁面是需要網路請求回資料才能正常展示,因此優化的方向主要有兩個:

  • 對網路請求進行優化,加快獲取資料的速度,如使用多執行緒等。
  • 未獲取到資料時,先展示載入動畫,可以是載入框,更好的做法是使用一些色塊填充資料展示的位置並作出載入動畫(實現起來比較費事)。

重新整理幀率

重新整理幀率可以通過Instrument裡的Core Animation檢視,也可以使用CADisplayLink(已經有許多現成的封裝控制元件,程式碼也很簡單),它是一個以和螢幕重新整理率相同的頻率將內容畫到螢幕上的定時器,最快能每秒呼叫60次,在正常情況下會在每次重新整理結束都被呼叫,精確度相當高。如果是CPU或是GPU某個步驟耗時導致渲染錯過了一次垂直訊號,那這個方法就不會被呼叫了,之後統計的幀數也就隨之降低了。

理想的FPS值為60左右,過低的話就用該進性優化了,根據WWDC的說法,當FPS 低於45的時候,使用者就會察覺到到滑動有卡頓。 ####Core Animation(必須使用真機) 執行Instrument中的Core Animation,可以錄製執行過程中的幀率:

【OC梳理】效能檢測及優化彙總
左側紅框中的數字,代表著FPS值,理論上60最佳。 以下是右側框中,Debug Options中選項的作用:

  • Color Blended Layers(混合過度繪製):這個選項基於渲染程度對螢幕中的混合區域進行綠到紅的高亮(也就是多個半透明圖層的疊加),由於重繪的原因,混合對GPU效能會有影響,同時也是滑動或者動畫掉幀的罪魁禍首之一
  • Color Hits Green and Misses Red(光柵化快取圖層的命中情況):這個選項主要是檢測我們有無濫用或正確使用layer的shouldRasterize屬性.成功被快取的layer會標註為綠色,沒有成功快取的會標註為紅色。當測試的應用頻繁閃現出紅色標註圖層時,表明對圖層做的Rasterization作用不大。
  • Color Copied Image (拷貝的圖片):這個選項主要檢查我們有無使用不正確圖片格式,如果圖片的格式不正確,則系統將圖片轉化為畫素的時間就有可能變長。
  • Color Immediately (顏色立即更新):通常 Core Animation 以每秒10次的頻率更新圖層的除錯顏色,對於某些效果來說,這可能太慢了,這個選項可以用來設定每一幀都更新(可能會影響到渲染效能,所以不要一直都設定它)
  • Color Misaligned Image (圖片對齊方式):這裡會高亮那些被縮放或者拉伸以及沒有正確對齊到畫素邊界的圖片,即圖片Size和imageView中的Size不匹配,會使圖過程片縮放,而縮放會佔用CPU。
  • Color Offscreen- Rendered Yellow (離屏渲染):這裡會把那些需要離屏渲染的到圖層高亮成黃色。

發生離屏渲染的可能有:

/* 圓角處理 */ 
view.layer.maskToBounds = truesomeView.clipsToBounds = true 
/* 設定陰影 */ >view.shadow.. 
/* 柵格化 */ 
view.layer.shouldRastarize = true 
複製程式碼

針對柵格化處理,我們需要指定螢幕的解析度

//離屏渲染 - 非同步繪製 耗電 
self.layer.drawsAsynchronously = true 
/**柵格化 - 非同步繪製之後 ,會生成一張獨立的圖片 cell 在螢幕上滾動的時候,本質上滾動的
是這張圖片 
cell 優化,要儘量減少圖層的數量,想當於只有一層 
停止滾動之後,可以接受監聽**/ 
self.layer.shouldRasterize = true 
//使用 “柵格化” 必須指定解析度 
self.layer.rasterizationScale = UIScreen.main.scale 
複製程式碼

指定陰影的路徑,可以防止離屏渲染

// 指定陰影曲線,防止陰影效果帶來的離屏渲染 
imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath
複製程式碼

這行程式碼制定了陰影路徑,如果沒有手動指定,Core Animation會去自動計算,這就會觸發離屏渲染。如果人為指定了陰影路徑,就可以免去計算,從而避免產生離屏渲染。

設定cornerRadius本身並不會導致離屏渲染,但很多時候它還需要配合layer.masksToBounds = true使用。根據之前的總結,設定masksToBounds會導致離屏渲染。解決方案是儘可能在滑動時避免設定圓角,如果必須設定圓角,可以使用光柵化技術將圓角快取起來:

// 設定圓角 
label.layer.masksToBounds = true 
label.layer.cornerRadius = 8 
label.layer.shouldRasterize = true 
label.layer.rasterizationScale = layer.contentsScale 
複製程式碼

如果介面中有很多控制元件需要設定圓角,比如tableView中,當tableView有超過25個圓角,使用如下方法

view.layer.cornerRadius = 10 
view.maskToBounds = Yes 
複製程式碼

那麼fps將會下降很多,特別是對某些控制元件還設定了陰影效果,更會加劇介面的卡頓、掉幀現>象,對於不同的控制元件將採用不同的方法進行處理: 1). 對於label類,可以通過CoreGraphics來畫出一個圓角的label 2). 對於imageView,通過CoreGraphics對繪畫出來的image進行裁邊處理,形成一個圓角的imageView

  • Color Compositing Fast-Path Blue:這個選項會對任何直接使用OpenGL繪製的圖層進行高亮,如果僅僅使用UIKit或者Core Animation的API,不會有任何效果。
  • Flash Updated Regions (Core Graphics 繪製的圖層):此選項會對重繪的內容進行高亮成黃色,也就是軟體層面使用Core Graphics 繪製的圖層。 模擬器中可以直接設定上面的某些選項:
    【OC梳理】效能檢測及優化彙總

網路請求時間,流量消耗

大部分的網路請求都是通過HTTP完成,使用成熟的第三方庫諸如AFNetworking很容易搭建一個功能簡易的網路模組。 對於網路模組,通用的優化方面大致如下:

  • DNS對映

無論是HTTP還是Socket長連線,第一步都是DNS解析。域名根據層級「主機名.次級域名.頂級域名.根域名」去解析,每一級快取生命週期不同。在iOS裝置上幾乎每次斷網重連,重啟裝置都會使DNS快取失效,觸發重新查詢。這一步的優化對請求的延遲來說至關重要。

  • 請求壓縮

DNS查詢之後是TCP握手建立連線,併傳送請求資料。對於TCP來說,單個IP包大小受限於MSS值,大部分使用者所處網路環境下每個包的大小約在1.5KB,新建立的HTTP連線由於TCP的slow start特性,會導致本地的部分IP包本臨時快取,從而增加了整體request的延遲。所以我們應該儘可能嘗試去壓縮我們的網路請求業務資料,減少一個Request的IP包數量,或許可以讓使用者少經歷一個RTT,降低請求延遲的使用者感知。

  • 請求合併

對於非關鍵性的業務資料,或者對實時性要求不高的請求來說,通過合併請求的方式可以減少和伺服器互動的次數,一則降低伺服器壓力,二則合併之後再壓縮能節約客戶端的流量。這類請求一般見於打點SDK,crash日誌收集等非業務型請求。

  • 請求的安全性
  1. 僅僅用HTTPS + POST請求提交使用者的隱私資料,不能完全解決安全問題,我們還需要對請求的引數進行一定的加密,例如登入: 事先生成一對公私鑰(RSA等等),使用者在登入時用公鑰將使用者的密碼加密,將密文傳輸到伺服器,伺服器使用私鑰將密碼解密,然後加鹽(在密碼學中是指通過在密碼任意固定位置插入特定的字串,讓雜湊後的結果和使用原始密碼的雜湊結果不相符)之後再進行MD5等操作,之後再和伺服器原來儲存的用同樣方法處理過的密碼匹配,如果一致則登陸成功.
  2. 可以選擇類似Protobuf之類的二進位制通訊協議或者自己實現通訊協議,對於傳輸的內容進行一定程度的加密,以增加黑客破解的難度.
  3. 應用內購買的安全問題,客戶端在拿到憑證後,還需將憑證上傳到自己的伺服器進行二次驗證,以保證真實性(二次驗證的過程中也需要考慮返回結果被篡改的可能).
  • 合理的併發數

有些業務場景會出現多個Request集中產生的情況,此時我們需要設定一個合理的併發數。併發數如果太小,會導致“劣質”的請求block住“優質”的請求。如果併發數太大,頻寬有限的場景下,會增加請求的整體延遲。

  • 可靠性保障 可靠性保障也是個容易被忽視的方面,在深入探討之前,可以先將Request按業務屬性分類:

第一類:關鍵核心的業務資料,期望能100%送達伺服器。

第二類:重要內容請求,需要較高的請求成功率。

第三類:一般性內容請求,對成功率無要求。

之所以要將請求分為三類,是要在可靠性保障上做區分。理論上我們應該儘可能讓所有的請求成功率達到最高,但客戶端的流量,頻寬,手機電量,伺服器的壓力等都是有限的資源,所以我們採取的策略是隻對關鍵性的網路請求做高強度的可靠性保障。 第一類請求類似大家用微信時傳送的訊息,訊息資料一旦從輸入框發出,從使用者來的角度感知這個訊息資料是一定會到達對方的。如果網路環境差,網路模組會自動在後頭悄悄重試,一段時間後仍無法成功就通過產品互動的方式告知使用者傳送失敗了,即使失敗,請求的資料(訊息本身)一直存在客戶端。

對於這類請求的處理方式第一步不是通過網路傳送,而是持久化到Database當中。一旦入了DB,即使斷網,斷電,重啟,請求資料依然還在,只需在App重啟的時候還原請求資料,再次傳送即可。

如果判斷為第一類請求:

第一步我們先將請求持久化到DB當中。

第二步傳送請求,如果請求失敗則將請求加入重試佇列,成功則從重試佇列中移除。重試佇列背後也需要一套通用機制,比如多久重試一次,重試幾次之後放棄。

遇到最惡劣的場景,請求傳送失敗之後,App被kill。我們需要在App重啟之後從DB當中重新load所有失敗的請求再次重試。

第二類請求的例子可以是我們App啟動時使用者看到的首頁,首頁的內容從伺服器獲取,如果第一次請求就失敗體驗較差,這種場景下我們應該允許請求有機會多試幾次,增加一個retryCount即可。一般3次的重試基本可以排除網路抖動的情況。三次失敗之後即可認為請求失敗,通過產品互動告知使用者。

第三類請求的重要性最低,比如進入Controller的UV採集打點。這類請求只需要做一次,即使失敗也不會對產品體驗產生什麼負面影響。

  • 多通道

現在不少有技術條件的團隊都有自己的tcp長連線通道,技術再硬點的甚至配有UDP通道,UDP在丟包率高的網路環境下能極大的提高請求成功的概率。如果能同時具備HTTP,TCP,UDP三條網路通道,在某些場景下,如果不考慮流量(比如wifi),可以針對某個網路請求,兩通道或者三通道齊發,對請求成功的速度和可靠性有明顯的療效,不過客戶端和伺服器都需要針對業務場景做去重。

  • 網路環境監控

現在網路環境雖然越來越好,Wifi,4G,3G在一二線城市都有很好的普及,但還是有不少場景會導致網路狀態突然變差,比如進電梯,做火車、地鐵,人多的集會場所,從公司回家4G切Wifi等等,這些場景在生活當中並不少見,健壯的網路模組需要仔細的檢測網路的變化,針對性的做請求重試。

  • 請求成功率監控

網路模組應該能監控當前App的網路請求成功率,對於失敗率較高的請求,帶上業務資料,手機網路環境,系統引數等等,在使用者不活躍的時候能打包上報給server端,一則能找出更多需要優化的業務場景,二則能實時監控server端的健康狀態,三則能從資料層面精確判斷每一次網路優化是否有成效。


UI阻塞次數,不可操作時長,主執行緒阻塞超過400毫秒次數

主執行緒阻塞超過400毫秒就會讓使用者感知到卡頓,跟使用者互動的操作如渲染,管理觸控反應,迴應輸入等都是在主執行緒的,所以不要讓主執行緒承擔過多耗時操作,耗時操作放到子執行緒中進行。

Time Profiler

使用此工具,就可以揪出耗時的函式,除錯介面介紹:

【OC梳理】效能檢測及優化彙總
同樣,可以右鍵開啟定位到方法的程式碼:
【OC梳理】效能檢測及優化彙總


耗電功率

耗電功率是個比較綜合的指標,影響因素很多。跟效能相關的,密集的網路請求,長連結,密集的CPU操作(比如大量的複雜計算)都會使耗電功率增加。此外,耗電量還會被很多其他因素影響,比如使用者在不同光線下使用,iPhone會自動調整螢幕亮度,就會導致耗電量不同;網路狀況(流暢的Wi-Fi還是訊號不好的3G),由於耗電量的影響因素太多,統計出來並不能精準的反應你的APP的效能,一般的APP不必把耗電量當作一個優化指標,只要把可能影響耗電量的、可優化的部分儘量優化即可,比如網路請求和CPU操作。 測量耗電量的時候不能用模擬器,模擬器下得到的電量值是負數,也不能用真機連著電腦debug,因為這個過程本身就在給手機充電。正確的做法是在手機上設定:設定–>開發者–>logging–>energy點選Start Recording,一段時間以後再stop,再用手機連線到電腦的instrument上,使用import log記錄進行分析。


Tips

  • 做效能方面的檢測工作時,一定要在真機上測試,而不是模擬器。模擬器的效能是Mac的,跟iPhone不可同日而語,測出來的資料不準也就沒有了意義。
  • 效能測試要用釋出配置,也就是說要用release包,而不是除錯模式。
  • 最好在你支援的裝置中效能最差的裝置上測試。

參考文章

iOS APP 效能檢測

iOS App從點選到啟動

深度優化iOS網路模組

相關文章