編寫穩定流暢的iOSApp

不忘初心

在過去幾年間,移動應用以雷霆之勢席卷全球。我們在工作和休閑時間中使用互聯網的方式,已經隨著移動應用的前進腳步發生了變革。在開發應用的時候,人們也開始考慮“移動優先”的做法。我們正在面對全新一代的移動設備,諸如可穿戴設備或眾多移動配件——正是它們構成了“萬物互聯”的世界。我們將面對全新的用戶界面,通過它們數據展示及指令接收處理。同時,我們還將看到,越來越多的公司將真正地踐行“移動優先”的思路。而在未來數年中,這一切都將影響我們設計、開發和測試軟件的方式。

把一個客戶端做得穩定、無奔潰、流暢,是寫客戶端朋友的夢想,但是,我們面臨的結果往往是不如人意的。天下武功,唯快不破。很多公司都信奉這個教條。恨不得把app開發周期壓縮到最低,這就導致了開發中隱藏了很多問題。有點經驗的工程師草率的優化一下,更糟的情況是那些沒有經驗的工程師甚至不會對app進行任何優化,這將會使情況變的更糟。

十年前,移動設備的硬件資源是非常有限的.甚至連浮點數都是被禁止的.因為浮點數能導致計算的速度變慢。科技發展如此迅速的今天,硬件很大程度上可以彌補軟件的短板。但是硬件的進步終究無法掩飾軟件的不足,這也是寫這篇文章的初心。

移動端關注要點

在程序開發中,測試是必不可少的。移動端測試按大的類型劃分可以分為白盒測試和黑盒測試。

白盒測試一般是由開發人員使用編碼的方式進行。測試者需要接觸程序的內部代碼;而黑盒測試可以在不知道程序內部結構和代碼的情況下進行。

下面是主要的測試流程了:

冒煙測試:在軟件測試中,冒煙測試是指快速驗證APP的主要功能(例如:微信的登陸、退出、發消息等功能) 。如果沒有發現問題,再進行更加深入的測試工作;如果發現有問題,就說明APP有重大缺陷。

功能測試:功能測試也叫行為測試,需要根據測試用例來驗證應用預期的功能有沒有實現。

自由探索式測試:嘗試邊界條件、輸入特殊符號、異常網絡環境、突然中斷程序等操作 。功能測試的目的是驗證正常的功能有沒有實現,而自由探索測試的目的就是為了試試應用在極端的操作下會不會出現問題。探索式測試就是要找到能讓應用出錯的操作。

回歸測試:對之前使用我們的服務測試過的應用,將案例復測一遍。

移動端關注的一些指標

運行多少小時不崩潰;

多次打開頁面,控制崩潰率;

界面優化,如何才能讓用戶不急躁、不煩躁;

服務器沒有返回數據,是否會導致奔潰;

網絡不好,數據來的太慢,界面是否不流暢;

從數據庫讀的數據太慢如何解決等。

移動端界面應該有自己的邏輯,需要網絡數據的地方,應該有默認值,這樣在網絡數據沒有返回的情況下,讓用戶有數據可以看到。收到的網絡數據應該是通過某種方式刷新到界面,而不是等到數據返回才刷新頁面。當沒有網絡數據的時候,界面應該可以自成一體,走的通流程,不強依賴網絡數據。

在弱網模式下調試是我們必備的功力,因為我們要考慮用戶的實施環境通常都不會很好。把經常使用的數據,存到緩存,提高APP的運行效率、界面流程度。同時,我們需要具備收集奔潰日志的功能,這樣才能更好的減少崩潰,提高用戶體驗。

界面卡頓產生的原因和解決方案

iOS界面處理是在主線程下進行的,系統圖形服務通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次刷新信號到來時顯示到屏幕上。顯示器通常以固定頻率進行刷新,如果在一個刷新時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。

CPU 資源消耗原因和解決方案

對象創建

對象的創建會分配內存、調整屬性、甚至還有讀取文件等操作,比較消耗 CPU 資源。盡量用輕量的對象代替重量的對象,可以對性能有所優化。比如 CALayer 比 UIView 要輕量許多,那么不需要響應觸摸事件的控件,用 CALayer 顯示會更加合適。如果對象不涉及 UI 操作,則盡量放到后臺線程去創建,但可惜的是包含有 CALayer 的控件,都只能在主線程創建和操作。通過 Storyboard 創建視圖對象時,其資源消耗會比直接通過代碼創建對象要大非常多,在性能敏感的界面里,Storyboard 并不是一個好的技術選擇。

盡量推遲對象創建的時間,并把對象的創建分散到多個任務中去。盡管這實現起來比較麻煩,并且帶來的優勢并不多,但如果有能力做,還是要盡量嘗試一下。如果對象可以復用,并且復用的代價比釋放、創建新對象要小,那么這類對象應當盡量放到一個緩存池里復用。

對象調整

對象的調整也經常是消耗 CPU 資源的地方。這里特別說一下 CALayer:CALayer 內部并沒有屬性,當調用屬性方法時,它內部是通過運行時 resolveInstanceMethod 為對象臨時添加一個方法,并把對應屬性值保存到內部的一個 Dictionary 里,同時還會通知 delegate、創建動畫等等,非常消耗資源。UIView 的關于顯示相關的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調整時,消耗的資源要遠大于一般的屬性。對此你在應用中,應該盡量減少不必要的屬性修改。當視圖層次調整時,UIView、CALayer 之間會出現很多方法調用與通知,所以在優化性能時,應該盡量避免調整視圖層次、添加和移除視圖。

對象銷毀

對象的銷毀雖然消耗資源不多,但累積起來也是不容忽視的。通常當容器類持有大量對象時,其銷毀時的資源消耗就非常明顯。同樣的,如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中,然后扔到后臺隊列去隨便發送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了。

布局計算

視圖布局的計算是 App 中最為常見的消耗 CPU 資源的地方。如果能在后臺線程提前計算好視圖布局、并且對視圖布局進行緩存,那么這個地方基本就不會產生性能問題了。

不論通過何種技術對視圖進行布局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上。上面也說過,對這些屬性的調整非常消耗資源,所以盡量提前計算好布局,在需要時一次性調整好對應屬性,而不要多次、頻繁的計算和調整這些屬性。

Autolayout

Autolayout 是蘋果本身提倡的技術,在大部分情況下也能很好的提升開發效率,但是 Autolayout 對于復雜視圖來說常常會產生嚴重的性能問題。隨著視圖數量的增長,Autolayout 帶來的 CPU 消耗會呈指數級上升。如果你不想手動調整 frame 等屬性,你可以用一些工具方法替代(比如常見的 left/right/top/bottom/width/height 快捷屬性),或者使用 ComponentKit、AsyncDisplayKit 等框架。

文本計算

如果一個界面中包含大量文本(比如微博微信朋友圈等),文本的寬高計算會占用很大一部分資源,并且不可避免。如果你對文本顯示沒有特殊要求,可以參考下 UILabel 內部的實現方式:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文本寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪制文本。盡管這兩個方法性能不錯,但仍舊需要放到后臺線程進行以避免阻塞主線程。如果你用 CoreText 繪制文本,那就可以先生成 CoreText 排版對象,然后自己計算了,并且 CoreText 對象還能保留以供稍后繪制使用。

文本渲染

屏幕上能看到的所有文本內容控件,包括 UIWebView,在底層都是通過 CoreText 排版、繪制為 Bitmap 顯示的。常見的文本控件 (UILabel、UITextView 等),其排版和繪制都是在主線程進行的,當顯示大量文本時,CPU 的壓力會非常大。對此解決方案只有一個,那就是自定義文本控件,用 TextKit 或最底層的 CoreText 對文本異步繪制。盡管這實現起來非常麻煩,但其帶來的優勢也非常大,CoreText 對象創建好后,能直接獲取文本的寬高等信息,避免了多次計算(調整 UILabel 大小時算一遍、UILabel 繪制時內部再算一遍);CoreText 對象占用內存較少,可以緩存下來以備稍后多次渲染。

圖片的解碼

當你用 UIImage 或 CGImageSource 的那幾個方法創建圖片時,圖片數據并不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的數據才會得到解碼。這一步是發生在主線程的,并且不可避免。如果想要繞開這個機制,常見的做法是在后臺線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創建圖片。目前常見的網絡圖片庫都自帶這個功能。

圖像的繪制

圖像的繪制通常是指用那些以 CG 開頭的方法把圖像繪制到畫布中,然后從畫布創建圖片并顯示這樣一個過程。這個最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進行。一個簡單異步繪制的過程大致如下(實際情況會比這個復雜得多,但原理基本一致):

GPU 資源消耗原因和解決方案

相對于 CPU 來說,GPU 能干的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合并渲染,然后輸出到屏幕上。通常你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。

紋理的渲染

所有的 Bitmap,包括圖片、文本、柵格化的內容,最終都要由內存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時(比如 TableView 存在非常多的圖片并且快速滑動時),CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。

當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096x4096,所以,盡量不要讓圖片和視圖的大小超過這個值。

視圖的混合 (Composing)

當多個視圖(或者說 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結構過于復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當盡量減少視圖數量和層次,并在不透明的視圖里標明 opaque 屬性以避免無用的 Alpha 通道合成。當然,這也可以用上面的方法,把多個視圖預先渲染為一張圖片來顯示。

圖形的生成

CALayer 的 border、圓角、陰影、遮罩(mask),CASharpLayer 的矢量圖形顯示,通常會觸發離屏渲染(offscreen rendering),而離屏渲染通常發生在 GPU 中。當一個列表視圖中出現大量圓角的 CALayer,并且快速滑動時,可以觀察到 GPU 資源已經占滿,而 CPU 資源消耗很少。這時界面仍然能正?;瑒?,但平均幀數會降到很低。為了避免這種情況,可以嘗試開啟 CALayer.shouldRasterize 屬性,但這會把原本離屏渲染的操作轉嫁到 CPU 上去。對于只需要圓角的某些場合,也可以用一張已經繪制好的圓角圖片覆蓋到原本視圖上面來模擬相同的視覺效果。最徹底的解決辦法,就是把需要顯示的圖形在后臺線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。

用 Instruments 來檢驗你的app

時間事件查看器-Time Profiler

在xcode的菜單中選擇 product->Profile

我們會看到下面的界面:

點擊Time Profiler進入。

Time Profiler

下面我們來深究如下的控制面板:

控制面板

以下介紹下配置選項:

Separate by Thread: 每個線程應該分開考慮。只有這樣你才能揪出那些大量占用CPU的"重"線程。

Invert Call Tree: 從上倒下跟蹤堆棧,這意味著你看到的表中的方法,將已從第0幀開始取樣,這通常你是想要的,只有這樣你才能看到CPU中話費時間最深的方法.也就是說FuncA{FunB{FunC}} 勾選此項后堆棧以C->B-A 把調用層級最深的C顯示在最外面。

Hide System Libraries: 勾選此項你會顯示你app的代碼,這是非常有用的. 因為通常你只關心cpu花在自己代碼上的時間不是系統上的。

Flatten Recursion: 遞歸函數, 每個堆棧跟蹤一個條目。

Top Functions: 一個函數花費的時間直接在該函數中的總和,以及在函數調用該函數所花費的時間的總時間。因此,如果函數A調用B,那么A的時間報告在A花費的時間加上B.花費的時間,這非常真有用,因為它可以讓你每次下到調用堆棧時挑最大的時間數字,歸零在你最耗時的方法。

找到Detail面板里最耗時的進程,點擊進去可以看到代碼,觀察是否有異,如此便可逐步優化應用的運行效果了。

修改

修改好后,在儀器重新運行該應用程序Product—Profile(或?I-記住,這些快捷鍵真的會為您節省一些時間)。

分配工具

分配工具

點擊進入

這個時候你會發現兩個曲目。一個叫(分配)Allocations,以及一個被稱為VM Tracker(虛擬機跟蹤)。

內存泄漏有兩種泄漏。第一個是真正的內存泄漏,一個對象尚未被釋放,但是不再被引用的了。因此,存儲器不能被重新使用。第二類泄漏是比較麻煩一些。這就是所謂的“無界內存增長”。這發生在內存繼續分配,并永遠不會有機會被釋放。如果永遠這樣下去你的程序占用的內存會無限大,當超過一定內存的話 會被系統的看門狗給kill掉。

內存警告是ios處理app最好的方式,尤其是在內存越來越吃緊的時候,你需要清除一些內存。內存一直增長其實也不一定是你的代碼出了問題,也有可能是UIKit 系統框架本身導致的。

嘗試

自己動手觀察下,一切自然明了。

內存泄露

這一類泄漏是前面提到的 - 當一個對象不再被引用時出現的那種,檢測泄漏可以理解為一個很復雜的事情,但泄漏的工具記得已分配的所有對象,通過定期掃描每個對象以確定是否有任何不能從任何其他對象訪問的。

關閉儀器,回到Xcode和選擇Product->Profile

內存泄露

點擊進入,運行:

運行

自己動手嘗試下,找到右邊面板里,如果有黑色標識的方法,進入看看。學習就是多嘗試。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容