這是一個很宏大的課題,有UI卡頓的優(yōu)化,網(wǎng)絡請求的優(yōu)化,降低Crash概率的優(yōu)化,技術方案的優(yōu)化等等。本文將會重點關注降低Crash概率的優(yōu)化。
一、知己知彼,百戰(zhàn)不殆
首先我們來了解一下Crash,Crash的原因有很多種,不同的技術所導致的Crash也會不同。
1、內存非法地址訪問,常說的野指針,段錯誤
ObjC不是強類型的,在強制類型轉換或者強制寫內存等操作時,很容易Crash。
2、訪問了不存在的方法
ObjC的消息傳遞機制會在無法解讀消息時拋出異常,并讓程序Crash。
3、訪問數(shù)組等對象越界或插入了空對象
一個固定數(shù)組有一塊連續(xù)內存,數(shù)組指針指向內存首地址,靠下標來計算元素地址,如果下標越界則指針偏移出這塊內存,會訪問到野數(shù)據(jù),ObjC 為了安全就直接讓程序 Crash 了。
4、循環(huán)引用導致內存泄漏
ObjC使用的內存管理機制ARC(自動引用計數(shù)),當對象的引用計數(shù)為0,執(zhí)行RunLoop時會自動回收其內存,但是如果出現(xiàn)對象循環(huán)引用,引用計數(shù)無法減為0,則出現(xiàn)了內存泄漏。
既然已經(jīng)知道了原因,該如何進行優(yōu)化呢?
二、工欲善其事必先利其器
我們再來了解一下ObjC的基礎知識。
1、ARC(Automatic Reference Counting)
其實在ObjC中內存的管理是依賴對象引用計數(shù)器來進行的:在ObjC中每個對象內部都有一個與之對應的整數(shù)(retainCount),叫“引用計數(shù)器”,當一個對象在創(chuàng)建之后它的引用計數(shù)器為1,當調用這個對象的alloc、retain、new、copy方法之后引用計數(shù)器自動在原來的基礎上加1(ObjC中調用一個對象的方法就是給這個對象發(fā)送一個消息),當調用這個對象的release方法之后它的引用計數(shù)器減1,如果一個對象的引用計數(shù)器為0,則系統(tǒng)會自動調用這個對象的dealloc方法來銷毀這個對象。遵循誰創(chuàng)建,誰釋放原則。
在ObjC中沒有GC機制,但提供了一種半自動化的機制(ARC),在程序編譯階段編譯器會自動為我們添加retain,release,處于autoreleaespool
中的對象都會自動release一次。當弱引用對象被釋放時,運行時自動將其置為nil
。
2、消息傳遞
眾所周知,ObjC是從C發(fā)展而來的一門面向對象開發(fā)語言,不同于C++的靜態(tài)性,ObjC是真正意義上的動態(tài)語言(雖然C++也能通過virtual來實現(xiàn)有限的動態(tài)性)。觀察objc_class
的定義,如下:
struct objc_class {
Class isa;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
這里的isa指向一個“類對象”(類的實例是一個對象,類本身也是對象,此時isa指向其元類,不單單表示一個數(shù)據(jù)類型)。
對象的類不僅描述了對象的數(shù)據(jù):對象占用的內存大小、成員變量的類型和布局等,而且也描述了對象的行為:對象能夠響應的消息、實現(xiàn)的實例方法等。因此,當我們調用實例方法
[receiver message]
給一個對象發(fā)送消息時,這個對象能否響應這個消息就需要通過 isa
找到它所屬的類,然后遍歷methodLists
查找字符串匹配的實例方法,再通過super_class
遍歷繼承樹繼續(xù)查找字符串匹配的實例方法,直至超級父類NSObject
,若還是無法找到匹配的方法,則最終會執(zhí)行
// 該方法會拋出異常,并abort程序
- (void)doesNotRecognizeSelector:(SEL)aSelector
當我們調用類方法,比如[NSObject new]
,給類對象發(fā)送消息。同樣的,類對象能否響應這個消息也要通過 isa 找到類對象所屬的類(元類)才能知道。也就是說,實例方法是保存在類中的,而類方法是保存在元類中的。
說了這么多,大家可能已經(jīng)有點繞迷糊了,下面我們看一張圖,一切自會明了。
3、消息轉發(fā)
ObjC的消息轉發(fā)機制分為兩大階段。第一階段先征詢接收對象所屬的類,看其能否動態(tài)添加方法。第二階段運行時系統(tǒng)會請求接收對象看看有沒有其他對象能處理這條消息,若有則會轉發(fā)給那個對象繼續(xù)消息傳遞;若沒有則會啟動完整的消息轉發(fā)機制。
1)、動態(tài)方法解析
對象在收到無法解讀的消息后,首先會調用其類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
動態(tài)的為對象添加新方法,前提是相關方法的實現(xiàn)代碼已經(jīng)寫好,只等著運行時調用即可。
2)、備援接收者
運行時會征詢當前接收對象,能不能轉給其他接收者來處理,與之相對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
若可以轉給其他對象,則執(zhí)行其他對象的消息傳遞機制
3)、完整的消息轉發(fā)
轉發(fā)算法來到這一步,首先會把消息有關的全部細節(jié)都封裝在NSInvocation
對象中,并調用下列方法來轉發(fā)消息:
- (void)forwardingInvocation:(NSInvocation *)invocation;
如果還不能處理消息,同消息傳遞一樣程序很自然地會拋異常,abort。整個消息轉發(fā)流程如下圖:
4、Runtime
基于ObjC的對象模型,消息傳遞、轉發(fā)機制,Apple提供了一系列底層可操作它們的API。比如methodLists
本質上是一個鏈表,使用下列API即可動態(tài)地控制消息的實現(xiàn)。
// 添加實例方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
// 替換實例方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
// 交換實例方法
void method_exchangeImplementations(Method m1, Method m2);
// 獲取類方法
Method class_getClassMethod(Class cls, SEL name);
// 獲取實例方法
IMP class_getMethodImplementation(Class cls, SEL name);
......
所有的ObjC代碼都會被轉化成runtime
的C代碼執(zhí)行,例如[receiver message];
會被轉化成objc_msgSend(target, @selector(doSomething));
我們可以把@selector(doSomething)
替換成任意指定的方法實現(xiàn),從而達到Hook(鉤子)的目的。這就是大名鼎鼎"Method Swizzling 黑魔法"的基本原理。
5、RunLoop
一般來講,一個線程一次只能執(zhí)行一個任務,執(zhí)行完成后線程就會退出。對于iOS之類的GUI(圖形用戶界面系統(tǒng))需要一個機制,讓線程能隨時處理各種事件但并不退出,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
我們常說的程序啟動了,某種意義上來說可理解成RunLoop
運行起來了。一旦該RunLoop
結束了(很多異常Crash都會導致RunLoop
停止運行),程序也就終止了。
三、分而治之,各個擊破
1、內存非法地址訪問,常說的野指針,段錯誤
解決方案:遵循良好的編碼規(guī)范,變量使用前要判斷非空,釋放后要置空。類型不確定時,盡量進行類型判斷,少用類型強制轉換。必要位置可使用@try @catch捕捉異常。
2、訪問了不存在的方法
解決方案:
(1)在消息傳遞,轉發(fā)關鍵位置指定相應的異常處理邏輯(或異常處理對象)。
(2)使用Method Swizzling去Hook- (void)doesNotRecognizeSelector:(SEL)aSelector
,添加異常處理邏輯。
3、訪問數(shù)組對象越界或插入了空對象
解決方案:
(1)訪問數(shù)組前判長度,插入數(shù)組前判空對象
(2)使用Method Swizzling去Hook- (id)objectAtIndex:(NSUInteger)index
等方法,添加異常處理邏輯。
4、循環(huán)引用導致內存泄漏
解決方案:
(1)搭配使用libextobjc
中的@weakify
和@strongify
宏來避免循環(huán)引用
@weakify(self);
[self.context performBlock:^{
@strongify(self);
[self doSomething];
}];
(2)使用Method Swizzling去Hook- (void)dealloc;
方法,添加檢測內存泄漏邏輯,代碼如下:
- (void)custom_dealloc {
[self custom_dealloc];
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
if ( self != nil ) { // 利用ARC下weak變量會自動置為nil的特性
NSLog(@"%@ leaked!", NSStringFromClass(self.class));
}
});
}
5、讓程序回光返照
當程序因Crash導致RunLoop終止,我們截獲相應的異常處理,同時再次重啟當前所有的RunLoop,讓程序回光返照繼續(xù)運行。
至此我們差不多已經(jīng)解決了絕大多數(shù)的Crash,當然Crash率也會如期而降。