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