一個iOS App的穩定性,主要決定於整體的系統架構設計,同時也不可忽略編程的細節,正所謂“千裡之堤,潰於蟻穴”,一旦考慮不周,看似無關緊要的代碼片段可能會帶來整體軟件系統的崩潰。尤其因為蘋果限制了熱更新機制,App本身的穩定性及容錯性就顯的更加重要,之前可以通過發布熱補丁的方式解決線上代碼問題,現在就需要在提交之前對App開發周期內的各個指標進行實時監測,盡量讓問題暴漏在開發階段,然後及時修復,減少線上出問題的幾率。針對一個App的開發周期,它的穩定性指標主要有以下幾個環節構成,用一個腦圖表示如下:
穩定性指標
1 開發過程
開發過程中,主要是通過監控內存使用及洩露,CPU使用率,FPS,啟動時間等指標,以及常見的UI的主線程監測,NSAssert斷言等,最好能在Debug模式下,實時顯示在界面上,針對出現的問題及早解決。
內存問題
內存問題主要包括兩個部分,一個是iOS中常見循環引用導致的內存洩露 ,另外就是大量數據加載及使用導致的內存警告。
mmap
雖然蘋果並沒有明確每個App在運行期間可以使用的內存最大值,但是有開發者進行了實驗和統計,一般在占用系統內存超過20%的時候會有內存警告,而超過50%的時候,就很容易Crash了,所以內存使用率還是盡量要少,對於數據量比較大的應用,可以采用分步加載數據的方式,或者采用mmap方式。mmap 是使用邏輯內存對磁盤文件進行映射,中間只是進行映射沒有任何拷貝操作,避免了寫文件的數據拷貝。 操作內存就相當於在操作文件,避免了內核空間和用戶空間的頻繁切換。之前在開發輸入法的時候 ,詞庫的加載也是使用mmap方式,可以有效降低App的內存占用率,具體使用可以參考鏈接第一篇文章。
循環引用
循環引用是iOS開發中經常遇到的問題,尤其對於新手來說是個頭疼的問題。循環引用對App有潛在的危害,會使內存消耗過高,性能變差和Crash等,iOS常見的內存主要以下三種情況:
Delegate
代理協議是一個最典型的場景,需要你使用弱引用來避免循環引用。ARC時代,需要將代理聲明為weak是一個即好又安全的做法:
@property (nonatomic, weak) iddelegate;
NSTimer
NSTimer我們開發中會用到很多,比如下面一段代碼
- (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES]; } - (void)doSomeThing { } - (void)dealloc { [self.timer invalidate]; self.timer = nil; }
這是典型的循環引用,因為timer會強引用self,而self又持有了timer,所有就造成了循環引用。那有人可能會說,我使用一個weak指針,比如
__weak typeof(self) weakSelf = self; self.mytimer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(doSomeThing) userInfo:nil repeats:YES];
但是其實並沒有用,因為不管是weakSelf還是strongSelf,最終在NSTimer內部都會重新生成一個新的指針指向self,這是一個強引用的指針,結果就會導致循環引用。那怎麼解決呢?主要有如下三種方式:
使用類方法
使用weakProxy
使用GCD timer
具體如何使用,我就不做具體的介紹,網上有很多可以參考。
Block
Block的循環引用,主要是發生在ViewController中持有了block,比如:
@property (nonatomic, copy) LFCallbackBlock callbackBlock;
同時在對callbackBlock進行賦值的時候又調用了ViewController的方法,比如:
self.callbackBlock = ^{ [self doSomething]; }];
就會發生循環引用,因為:ViewController->強引用了callback->強引用了ViewController,解決方法也很簡單:
__weak __typeof(self) weakSelf = self; self.callbackBlock = ^{ [weakSelf doSomething]; }];
原因是使用MRC管理內存時,Block的內存管理需要區分是Global(全局)、Stack(棧)還是Heap(堆),而在使用了ARC之後,蘋果自動會將所有原本應該放在棧中的Block全部放到堆中。全局的Block比較簡單,凡是沒有引用到Block作用域外面的參數的Block都會放到全局內存塊中,在全局內存塊的Block不用考慮內存管理問題。(放在全局內存塊是為了在之後再次調用該Block時能快速反應,當然沒有調用外部參數的Block根本不會出現內存管理問題)。
所以Block的內存管理出現問題的,絕大部分都是在堆內存中的Block出現了問題。默認情況下,Block初始化都是在棧上的,但可能隨時被收回,通過將Block類型聲明為copy類型,這樣對Block賦值的時候,會進行copy操作,copy到堆上,如果裡面有對self的引用,則會有一個強引用的指針指向self,就會發生循環引用,如果采用weakSelf,內部不會有強類型的指針,所以可以解決循環引用問題。
那是不是所有的block都會發生循環引用呢?其實不然,比如UIView的類方法Block動畫,NSArray等的類的遍歷方法,也都不會發生循環引用,因為當前控制器一般不會強引用一個類。
其他內存問題
1 NSNotification addObserver之後,記得在dealloc裡面添加remove;
2 動畫的repeat count無限大,而且也不主動停止動畫,基本就等於無限循環了;
3 forwardingTargetForSelector返回了self。
內存解決思路:
1 通過Instruments來查看leaks
2 集成Facebook開源的FBRetainCycleDetector
3 集成MLeaksFinder
具體原理及使用,可以參考鏈接。
CPU使用率
CPU的使用也可以通過兩種方式來查看,一種是在調試的時候Xcode會有展示,具體詳細信息可以進入Instruments內查看,通過查看Instruments的time profile來定位並解決問題。另一種常見的方法是通過代碼讀取CPU使用率,然後顯示在App的調試面板上,可以在Debug環境下顯示信息,具體代碼如下:
int result; mib[0] = CTL_HW; mib[1] = HW_CPU_FREQ; length = sizeof(result); if (sysctl(mib, 2, &result, &length, NULL, 0) < 0) { perror("getting cpu frequency"); } printf("CPU Frequency = %u hz\n", result);
FPS監控
目前主要使用CADisplayLink來監控FPS,CADisplayLink是一個能讓我們以和屏幕刷新率相同的頻率將內容畫到屏幕上的定時器。我們在應用中創建一個新的 CADisplayLink 對象,把它添加到一個runloop中,並給它提供一個 target 和selector 在屏幕刷新的時候調用,需要注意的是添加到runloop的common mode裡面,代碼如下:
- (void)setupDisplayLink { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTicks:)]; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void)linkTicks:(CADisplayLink *)link { //執行次數 _scheduleTimes ++; //當前時間戳 if(_timestamp == 0){ _timestamp = link.timestamp; } CFTimeInterval timePassed = link.timestamp - _timestamp; if(timePassed >= 1.f) //fps CGFloat fps = _scheduleTimes/timePassed; printf("fps:%.1f, timePassed:%f\n", fps, timePassed); } }
啟動時間
點評App裡面本身就包含了很多復雜的業務,比如外賣、團購、到綜和酒店等,同時還引入了很多第三方SDK比如微信、QQ、微博等,在App初始化的時候,很多SDK及業務也開始初始化,這就會拖慢應用的啟動時間。
App的啟動時間t(App總啟動時間) = t1(main()之前的加載時間) + t2(main()之後的加載時間)。 t1 = 系統dylib(動態鏈接庫)和自身App可執行文件的加載; t2 = main方法執行之後到AppDelegate類中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法執行結束前這段時間,主要是構建第一個界面,並完成渲染展示。
針對t1的優化,優化主要有如下:
減少不必要的framework,因為動態鏈接比較耗時;
檢查framework應當設為optional和required,如果該framework在當前App支持的所有iOS系統版本都存在,那麼就設為required,否則就設為optional,因為optional會有些額外的檢查;
合並或者刪減一些OC類,這些我會在後續的靜態檢查中進行詳解;
針對t2的時間優化,可以采用:
異步初始化部分操作,比如網絡,數據讀取;
采用延遲加載或者懶加載某些視圖,圖片等的初始化操作;
對與圖片展示類的App,可以將解碼的圖片保存到本地,下次啟動時直接加載解碼後的圖片;
對實現了+load()方法的類進行分析,盡量將load裡的代碼延後調用。
UI的主線程監測
我們都知道iOS的UI的操作一定是在主線程進行,該監測可以通過hook UIView的如下三個方法
-setNeedsLayout, -setNeedsDisplay, -setNeedsDisplayInRect
確保它們都是在主線程執行。子線程操作UI可能會引起什麼問題,蘋果說得並不清楚,但是在實際開發中,我們經常會遇到整個App的動畫丟失,很大原因就是UI操作不是在主線程導致。
2 靜態分析過程
靜態分析在這裡,我主要介紹兩方面,一個是正常的code review機制,另外一個就是代碼靜態檢查工具
code review
組內的code review機制,可以參考團隊之前的OpenDoc - 前端團隊CodeReview制度,iOS客戶端開發,會在此基礎上進行一些常見手誤及Crash情況的重點標記,比如:
1.我們開發中首先都是在測試環境開發,開發時可以將測試環境的url寫死到代碼中,但是在提交代碼的時候一定要將他改為線上環境的url,這個就可以通過gitlab中的重點比較部分字符串,給提交者一個強力的提示;
2.其他常見Crash的重點檢查,比如NSMutableString/NSMutableArray/NSMutableDictionary/NSMutableSet 等類下標越界判斷保護,或者 append/insert/add nil對象的保護;
3.ARC下的release操作,UITableViewCell返回nil,以及前面介紹的常見的循環引用等。
code review機制,一方面是依賴寫代碼者的代碼習慣及質量,另一名依賴審查者的經驗和細心程度,即使讓多人revew,也可能會漏過一些錯誤,所以我們又添加了代碼的靜態檢查。
代碼靜態檢查
代碼靜態分析(Static Program Analysis)是指在不運行程序的條件下,由代碼靜態分析工具自動對程序進行分析的方法. iOS常見的靜態掃描工具有Clang Static Analyzer、OCLint、Infer,這些主要是用來檢查可能存在的問題,還有Deploymate用來檢查api的兼容性。
Clang Static Analyzer
Clang Static Analyzer是一款靜態代碼掃描工具,專門用於針對C,C++和Objective-C的程序進行分析。已經被Xcode集成,可以直接使用Xcode進行靜態代碼掃描分析,Clang默認的配置主要是空指針檢測,類型轉換檢測,空判斷檢測,內存洩漏檢測這種等問題。如果需要更多的配置,可以使用開源的Clang項目,然後集成到自己的CI上。
OCLint
OCLint是一個強大的靜態代碼分析工具,可以用來提高代碼質量,查找潛在的bug,主要針對 C、C++和Objective-C的靜態分析。功能非常強大,而且是出自國人之手。OCLint基於 Clang 輸出的抽象語法樹對代碼進行靜態分析,支持與現有的CI集成,部署之後基本不需要維護,簡單方便。
OCLint可以發現這些問題
可能的bug - 空的 if / else / try / catch / finally 語句
未使用的代碼 - 未使用的局部變量和參數
復雜的代碼 - 高圈復雜度, NPath復雜, 高NCSS
冗余代碼 - 多余的if語句和無用的括號
壞味道的代碼 - 過長的方法和過長的參數列表
不好的使用 - 倒邏輯和入參重新賦值
對於OCLint的與原理和部署方法,可以參考團隊成員之前的文章:靜態代碼分析之OCLint的那些事兒,每次提交代碼後,可以在打包的過程中進行代碼檢查,及早發現有問題的代碼。當然也可以在合並代碼之前執行對應的檢查,如果檢查不通過,不能合並代碼,這樣檢查的力度更大。
Infer
Infer facebook開源的靜態分析工具,Infer可以分析 Objective-C, Java 或者 C 代碼,報告潛在的問題。Infer效率高,規模大,幾分鐘能掃描數千行代碼;
C/OC中捕捉的bug類型主要有:
1:Resource leak 2:Memory leak 3:Null dereference 4:Premature nil termination argument
只在 OC中捕捉的bug類型
1:Retain cycle 2:Parameter not null checked 3:Ivar not null checked
結論
Clang Static Analyzer和Xcode集成度更高、更好用,支持命令行形式,並且能夠用於持續集成。OCLint有更多的檢查規則和定制,和很多工具集成,也同樣可用於持續集成。Infer效率高,規模大,幾分鐘能掃描數千行代碼;支持增量及非增量分析;分解分析,整合輸出結果。infer能將代碼分解,小范圍分析後再將結果整合在一起,兼顧分析的深度和速度,所以根據自己的項目特點,選擇合適的檢查工具對代碼進行檢查,減少人力review成本,保證代碼質量,最大限度的避免運行錯誤。
3 測試過程
前面介紹了很多指標的監測,代碼靜態檢查,這些都是性能相關的,真正決定一個App功能穩定是否的是測試環節。測試是發布之前的最後一道卡,如果bug不能在測試中發現,那麼最終就會觸達用戶,所以一個App的穩定性,很大程度決定它的測試過程。iOS App的測試包括以下幾個層次:單元測試,UI測試,功能測試,異常測試。
單元測試
XCTest是蘋果官方提供的單元測試框架,與Xcode集成在一起,由此蘋果提供了很詳細的文檔XCTest。
Xcode單元測試包含在一個XCTestCase的子類中。依據約束,每一個 XCTestCase 子類封裝一個特殊的有關聯的集合,例如一個功能、用例或者一個程序流。同時還提供了XCTestExpectation來處理異步任務的測試,以及性能測試measureBlock(),還包括很多第三方測試框架比如:KiWi,Quick,Specta等,以及常用的mock框架OCMock。
單元測試的目的是將程序中所有的源代碼,隔離成最小的可測試單元,以確保每個單元的正確性,如果每個單元都能保證正確,就能保證應用程序整體相當程度的正確性。但是在實際的操作過程中,很多公司都很難徹底執行單元測試,主要就是單元測試代碼量甚至大於功能開發,比較難於維護。
對於測試用例覆蓋度多少合適這個話題,也是仁者見仁智者見智,其實一個軟件覆蓋度在50%以上就可以稱為一個健壯的軟件了,要達到70,80這些已經是非常難了,不過我們常見的一些第三方開源框架的測試用例覆蓋率還是非常高的,讓人咋舌。例如,AFNNetWorking的覆蓋率高達87%,SDWebImage的覆蓋率高達77%。
UI測試
Xcode7中新增了UI Test測試,UI測試是模擬用戶操作,進而從業務處層面測試,常用第三方庫有KIF,appium。關於XCTest的UI測試,建議看看WWDC 2015的視頻UI Testing in Xcode。 UI測試還有一個核心功能是UI Recording。選中一個UI測試用例,然後點擊圖中的小紅點既可以開始UI Recoding。你會發現:隨著點擊模擬器,自動合成了測試代碼。(通常自動合成代碼後,還需要手動的去調整)
UI測試
功能測試
功能測試跟上述的UT和UI測試有一些相通的地方,首先針對各個模塊設計的功能,測試是否達到產品的目的,通常功能測試主要是測試及產品人員,然後還需要進行專項測試,比如我們公司的雲測平台,會對整個App的性能,穩定性,UI等都進行整體評測,看是否達到標准,對於大規模的活動,還需要進行服務端的壓力測試,確保整個功能無異常。測試通過後,可以進行estFlight測試,到最後正式發布。
功能測試還包括如下場景:系統兼容性測試,屏幕分辨率兼容性測試,覆蓋安裝測試,UI是否符合設計,消息推送等,以及前面開發過程中需要監控的內存、cpu、電量、網絡流量、冷啟動時間、熱啟動時間、存儲、安裝包的大小等測試。
異常測試
異常測試主要是針對一些不常規的操作
使用過程中的來電時及結束後,界面顯示是否正常;
狀態欄為兩倍高度時,界面是否顯示正常;
意外斷電後,數據是否保存,數據是否有損害等;
設備充電時,不同電量時的App響應速度及操作流暢度等;
其他App的相互切換,前後台轉換時,是否正常;
網絡變化時的提示,弱網環境下的網絡請求成功率等;
各種monkey的隨機點擊,多點觸摸測試等是否正常;
更改系統時間,字體大小,語言等顯示是否正常;
設備存儲不夠時,是否能正常操作;
...
異常測試有很多,App針對自身的特點,可以選擇性的進行邊界和異常測試,也是保證App穩定行的一個重要方面。
4 發布及監控
因為移動App的特點,即使我們通過了各種測試,產品最終發布後,還是會遇到很多問題,比如Crash,網絡失敗,數據損壞,賬號異常等等。針對已經發布的App,主要有一下方式保證穩定性:
熱修復
目前比較流行的熱修復方案都是基於JSPatch、React Native、Weex、lua+wax。
JSPatch能做到通過js調用和改寫OC方法。最根本的原因是 Objective-C 是動態語言,OC上所有方法的調用/類的生成都通過 objective-c Runtime 在運行時進行,我們可以通過類名和方法名反射得到相應的類和方法,也可以替換某個類的方法為新的實現,還可以新注冊一個類,為類添加方法。JSPatch 的原理就是:JS傳遞字符串給OC,OC通過 Runtime 接口調用和替換OC方法。
React Native 是從 Web 前端開發框架 React 延伸出來的解決方案,主要解決的問題是 Web 頁面在移動端性能低的問題,React Native 讓開發者可以像開發 Web 頁面那樣用 React 的方式開發功能,同時框架會通過 JavaScript 與 Objective-C 的通信讓界面使用原生組件渲染,讓開發出來的功能擁有原生App的性能和體驗。
Weex阿裡開源的,基於Vue+Native的開發模式,跟RN的主要區別就在React和Vue的區別,同時在RN的基礎上進行了部分性能優化,總體開發思路跟RN是比較像的。
但是在今年上半年,蘋果以安全為理由,開始拒絕有熱修復功能的應用,但其實蘋果拒的不是熱更新,拒的是從網絡下載代碼並修改應用行為,蘋果禁止的是“基於反射的熱更新“,而不是 “基於沙盒接口的熱更新”。而大部分框架(如 React Native、weex)和游戲引擎(比如 Unity、Cocos2d-x等)都屬於後者,所以不在被警告范圍內。而JSPatch因為在國內大部分應用來做熱更新修復bug的行為,所以才回被蘋果禁止。
降級
用戶使用App一段時間後,可能會遇到這樣的情況:每次打開App時閃退,或者正常操作到某個界面時閃退,無法正常使用App。這樣的用戶體驗十分糟糕,如果沒有一個好的解決方案,很容易被用戶刪除App,導致用戶量的流失。因為熱更新基本不能使用,那就只能是App自身修復能力。目前常用的修復能力有:
啟動Crash的監控及修復
1 在應用起來的時候,記錄flag並保存本地,啟動一個定時器,比如5秒鐘內,如果沒有發生Crash,則認為用戶操作正常,清空本地flag。
2 下次啟動,發現有flag,則表明上次啟動Crash,如果flag數組越大,則說明Crash的次數越多,這樣就需要對整個App進行降級處理,比如登出賬號,清空Documents/Library/Caches目錄下的文件。
具體業務下的Crash及修復
針對某些具體業務Crash場景,如果是上線的前端頁面引起的,可以先對前端功能進行回滾,或者隱藏入口,等修復完畢後再上線,如果是客戶端的某些異常,比如數據庫升遷問題,主要是進行業務數據庫修復,緩存文件的刪除,賬號退出等操作,盡量只修復此業務的相關的數據。
網絡降級
比如點評App,本身有CIP(公司內部自己研發的)長連接,接入騰訊雲的WNS長連接,UDP連接,HTTP短連接,如果CIP服務器發生問題,可以及時切換到WNS連接,或者降級到Http連接,保證網絡連接的成功率。
線上監控
Crash監控
Crash是對用戶來說是最糟糕的體驗,Crash日志能夠記錄用戶閃退的崩潰日志及堆棧,進程線程信息,版本號,系統版本號,系統機型等有用信息,收集的信息越詳細,越能夠幫助解決崩潰,所以各大App都有自己崩潰日志收集系統,或者也可以使用開源或者付費的第三方Crash收集平台。
端到端成功率監控
端到端監控是從客戶端App發出請求時計時,到App收到數據數據的成功率,統計對象是:網絡接口請求(包括H5頁面加載)的成敗和端到端延時情況。端到端監控SDK提供了監控上傳接口,調用SDK提供的監控API可以將數據上報到監控服務器中。
整個端到端監控的可以在多個維度上做查詢端到端成功率、響應時間、訪問量的查詢,維度包括:返回碼、網絡、版本、平台、地區、運營商等。
用戶行為日志
用戶行為日志,主要記錄用戶在使用App過程中,點擊元素的時間點,浏覽時長,跳轉流程等,然後基於此進行用戶行為分析,大部分應用的推薦算法都是基於用戶行為日志來統計的。某些情況下,Crash分析需要查詢用戶的行為日志,獲取用戶使用App的流程,幫助解決Crash等其他問題。
代碼級日志
代碼級別的日志,主要用來記錄一個App的性能相關的數據,比如頁面打開速度,內存使用率,CPU占用率,頁面的幀率,網絡流量,請求錯誤統計等,通過收集相關的上下文信息,優化App性能。
總結
雖然現在市面上第三方平台已經很成熟,但是各大互聯公司都會自己開發線上監控系統,這樣保證數據安全,同時更加靈活。因為移動用戶的特點,在開發測試過程中,很難完全覆蓋所有用戶的全部場景,有些問題也只會在特定環境下才發生,所以通過線上監控平台,通過日志回撈等機制,及時獲取特定場景的上下文環境,結合數據分析,能夠及時發現問題,並後續修復,提高App的穩定性。
全文總結
本文主要從開發測試發布等流程來介紹了一個App穩定性指標及監測方法,開發階段主要針對一些比較具體的指標,靜態檢查主要是掃描代碼潛在問題,然後通過測試保證App功能的穩定性,線上降級主要是在盡量不發版的情況下,進行自修復,配合線上監控,信息收集,用戶行為記錄,方便後續問題修復及優化。本文觀點是作者從事iOS開發的一些經驗,希望能對你有所幫助,觀點不同歡迎討論。