iOS優(yōu)化實戰(zhàn)

這是一個很宏大的課題,有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)有點繞迷糊了,下面我們看一張圖,一切自會明了。

object_model.png

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ā)流程如下圖:

message.png

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率也會如期而降。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,755評論 0 9
  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會被問到的, ...
    SOI閱讀 21,840評論 3 63
  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會被問到的, ...
    made_China閱讀 1,217評論 0 7
  • 428里除了我當然還有慶慶是正常人以外 其余四個都是長期關在污人院的一群傻子 說她們是傻子已經(jīng)很客氣...
    1123gx閱讀 249評論 0 0
  • 近來中年危機成了炙手可熱的電影題材,如《港囧》《夏洛特煩惱》這類電影,很不幸的是我竟然都看了,好在懸崖勒馬表...
    鐘白庸閱讀 1,166評論 0 9