一個App的穩定性,主要決定於總體的系統架構設計,同時也不可忽略編程的細節,正所謂「千里之堤,潰於蟻穴」,一旦考慮不周,看似可有可無的代碼片斷可能會帶來總體軟件系統的崩潰。尤爲由於蘋果限制了熱更新機制,App自己的穩定性及容錯性就顯的更加劇要,以前能夠經過發佈熱補丁的方式解決線上代碼問題,如今就須要在提交以前對App開發週期內的各個指標進行實時監測,儘可能讓問題暴漏在開發階段,而後及時修復,減小線上出問題的概率 。針對一個App的開發週期,它的穩定性指標主要有如下幾個環節構成,用一個腦圖表html
開發過程當中,主要是經過監控內存使用及泄露,CPU使用率,FPS,啓動時間等指標,以及常見的UI的主線程監測,NSAssert斷言等,最好能在Debug模式下,實時顯示在界面上,針對出現的問題及早解決。前端
內存問題主要包括兩個部分,一個是iOS中常見循環引用致使的內存泄露 ,另外就是大量數據加載及使用致使的內存警告。ios
雖然蘋果並無明確每一個App在運行期間可使用的內存最大值,可是有開發者進行了實驗和統計,通常在佔用系統內存超過20%的時候會有內存警告,而超過50%的時候,就很容易Crash了,因此內存使用率仍是儘可能要少,對於數據量比較大的應用,能夠採用分步加載數據的方式,或者採用mmap方式。mmap 是使用邏輯內存對磁盤文件進行映射,中間只是進行映射沒有任何拷貝操做,避免了寫文件的數據拷貝。 操做內存就至關於在操做文件,避免了內核空間和用戶空間的頻繁切換。以前在開發輸入法的時候 ,詞庫的加載也是使用mmap方式,能夠有效下降App的內存佔用率,具體使用能夠參考的文章。git
循環引用是iOS開發中常常遇到的問題,尤爲對於新手來講是個頭疼的問題。循環引用對App有潛在的危害,會使內存消耗太高,性能變差和Crash等,iOS常見的內存主要如下三種狀況:github
代理協議是一個最典型的場景,須要你使用弱引用來避免循環引用。ARC時代,須要將代理聲明爲weak是一個即好又安全的作法:objective-c
@property (nonatomic, weak) id <MyCustomDelegate> delegate;
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,這是一個強引用的指針,結果就會致使循環引用。那怎麼解決呢?主要有以下三種方式:編程
具體如何使用,參考《NSTimer循環引用的幾種解決方案》。api
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的使用也能夠經過兩種方式來查看,一種是在調試的時候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);
目前主要使用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類中的didFinishLaunchingWithOptions方法執行結束前這段時間,主要是構建第一個界面,並完成渲染展現。
針對t1的優化,優化主要有以下:
針對t2的時間優化,能夠採用:
咱們都知道iOS的UI的操做必定是在主線程進行,該監測能夠經過hook UIView的以下三個方法
-setNeedsLayout,
-setNeedsDisplay,
-setNeedsDisplayInRect
確保它們都是在主線程執行。子線程操做UI可能會引發什麼問題,蘋果說得並不清楚,可是在實際開發中,咱們常常會遇到整個App的動畫丟失,很大緣由就是UI操做不是在主線程致使。
靜態分析在這裏,我主要介紹兩方面,一個是正常的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是一款靜態代碼掃描工具,專門用於針對C,C++和Objective-C的程序進行分析。已經被Xcode集成,能夠直接使用Xcode進行靜態代碼掃描分析,Clang默認的配置主要是空指針檢測,類型轉換檢測,空判斷檢測,內存泄漏檢測這種等問題。若是須要更多的配置,可使用開源的Clang項目,而後集成到本身的CI上。
OCLint是一個強大的靜態代碼分析工具,能夠用來提升代碼質量,查找潛在的bug,主要針對 C、C++和Objective-C的靜態分析。功能很是強大,並且是出自國人之手。OCLint基於 Clang 輸出的抽象語法樹對代碼進行靜態分析,支持與現有的CI集成,部署以後基本不須要維護,簡單方便。
OCLint能夠發現這些問題
對於OCLint的與原理和部署方法,能夠參考團隊成員以前的文章:靜態代碼分析之OCLint的那些事兒,每次提交代碼後,能夠在打包的過程當中進行代碼檢查,及早發現有問題的代碼。固然也能夠在合併代碼以前執行對應的檢查,若是檢查不經過,不能合併代碼,這樣檢查的力度更大。
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成本,保證代碼質量,最大限度的避免運行錯誤。
前面介紹了不少指標的監測,代碼靜態檢查,這些都是性能相關的,真正決定一個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%。
Xcode7中新增了UI Test測試,UI測試是模擬用戶操做,進而從業務處層面測試,經常使用第三方庫有KIF,appium。關於XCTest的UI測試,建議看看WWDC 2015的視頻UI Testing in Xcode。 UI測試還有一個核心功能是UI Recording。選中一個UI測試用例,而後點擊圖中的小紅點既能夠開始UI Recoding。你會發現:隨着點擊模擬器,自動合成了測試代碼。(一般自動合成代碼後,還須要手動的去調整)
功能測試跟上述的UT和UI測試有一些相通的地方,首先針對各個模塊設計的功能,測試是否達到產品的目的,一般功能測試主要是測試及產品人員,而後還須要進行專項測試,好比咱們公司的雲測平臺,會對整個App的性能,穩定性,UI等都進行總體評測,看是否達到標準,對於大規模的活動,還須要進行服務端的壓力測試,確保整個功能無異常。測試經過後,能夠進行estFlight測試,到最後正式發佈。
功能測試還包括以下場景:系統兼容性測試,屏幕分辨率兼容性測試,覆蓋安裝測試,UI是否符合設計,消息推送等,以及前面開發過程當中須要監控的內存、cpu、電量、網絡流量、冷啓動時間、熱啓動時間、存儲、安裝包的大小等測試。
異常測試主要是針對一些不常規的操做
異常測試有不少,App針對自身的特色,能夠選擇性的進行邊界和異常測試,也是保證App穩定行的一個重要方面。
由於移動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自身修復能力。目前經常使用的修復能力有:
1 在應用起來的時候,記錄flag並保存本地,啓動一個定時器,好比5秒鐘內,若是沒有發生Crash,則認爲用戶操做正常,清空本地flag。
2 下次啓動,發現有flag,則代表上次啓動Crash,若是flag數組越大,則說明Crash的次數越多,這樣就須要對整個App進行降級處理,好比登出帳號,清空Documents/Library/Caches目錄下的文件。
針對某些具體業務Crash場景,若是是上線的前端頁面引發的,能夠先對前端功能進行回滾,或者隱藏入口,等修復完畢後再上線,若是是客戶端的某些異常,好比數據庫升遷問題,主要是進行業務數據庫修復,緩存文件的刪除,帳號退出等操做,儘可能只修復此業務的相關的數據。
好比點評App,自己有CIP(公司內部本身研發的)長鏈接,接入騰訊雲的WNS長鏈接,UDP鏈接,HTTP短鏈接,若是CIP服務器發生問題,能夠及時切換到WNS鏈接,或者降級到Http鏈接,保證網絡鏈接的成功率。
Crash是對用戶來講是最糟糕的體驗,Crash日誌可以記錄用戶閃退的崩潰日誌及堆棧,進程線程信息,版本號,系統版本號,系統機型等有用信息,收集的信息越詳細,越可以幫助解決崩潰,因此各大App都有本身崩潰日誌收集系統,或者也可使用開源或者付費的第三方Crash收集平臺。
端到端監控是從客戶端App發出請求時計時,到App收到數據數據的成功率,統計對象是:網絡接口請求(包括H5頁面加載)的成敗和端到端延時狀況。端到端監控SDK提供了監控上傳接口,調用SDK提供的監控API能夠將數據上報到監控服務器中。
整個端到端監控的能夠在多個維度上作查詢端到端成功率、響應時間、訪問量的查詢,維度包括:返回碼、網絡、版本、平臺、地區、運營商等。
用戶行爲日誌,主要記錄用戶在使用App過程當中,點擊元素的時間點,瀏覽時長,跳轉流程等,而後基於此進行用戶行爲分析,大部分應用的推薦算法都是基於用戶行爲日誌來統計的。某些狀況下,Crash分析須要查詢用戶的行爲日誌,獲取用戶使用App的流程,幫助解決Crash等其餘問題。
代碼級別的日誌,主要用來記錄一個App的性能相關的數據,好比頁面打開速度,內存使用率,CPU佔用率,頁面的幀率,網絡流量,請求錯誤統計等,經過收集相關的上下文信息,優化App性能。
雖然如今市面上第三方平臺已經很成熟,可是各大互聯公司都會本身開發線上監控系統,這樣保證數據安全,同時更加靈活。由於移動用戶的特色,在開發測試過程當中,很難徹底覆蓋全部用戶的所有場景,有些問題也只會在特定環境下才發生,因此經過線上監控平臺,經過日誌回撈等機制,及時獲取特定場景的上下文環境,結合數據分析,可以及時發現問題,並後續修復,提升App的穩定性。
本文主要從開發測試發佈等流程來介紹了一個App穩定性指標及監測方法,開發階段主要針對一些比較具體的指標,靜態檢查主要是掃描代碼潛在問題,而後經過測試保證App功能的穩定性,線上降級主要是在儘可能不發版的狀況下,進行自修復,配合線上監控,信息收集,用戶行爲記錄,方便後續問題修復及優化。本文觀點是做者從事iOS開發的一些經驗,但願能對你有所幫助,觀點不一樣歡迎討論。
微信mars 的高性能日誌模塊 xlog
基於 CADisplayLink 的 FPS 指示器詳解
今日頭條iOS客戶端啓動速度優化
微信讀書 iOS 性能優化總結
移動端監控體系之技術原理剖析
美團點評移動網絡優化實踐
iOS 啓動連續閃退保護方案
微信 SQLite 數據庫修復實踐
轉自 https://zhuanlan.zhihu.com/p/28108686