歡迎光臨
每天分享高質量文章

iOS:保護App不閃退

作者:lijie250

鏈接:https://github.com/jezzmemo/JJException

Unrecognized Selector Sent to Instance

由於Objective-c是Message機制,而且物件在轉換的時候,會有拿到的物件和預期不一致,所以會有方法找不到的情況,在找不到方法時,查找方法將會進入方法Forward流程,系統給了三次補救的機會,所以我們要解決這個問題,在這三次均可以解決這個問題。



  • resolveInstanceMethod:(SEL)sel 這是實體化方法沒有找到方法,最先執行的函式,首先會流轉到這裡來,傳回值是BOOL,沒有找到就是NO,找到就傳回YES,如果要解決就需要再當前的實體中加入不存在的Selector,並系結IMP,示例如下:

static void xxxInstanceName(id self, SEL cmd, id value) {    NSLog(@"resolveInstanceMethod %@", value);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{    NSLog(@"resolveInstanceMethod");    
    NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        class_addMethod([self class], sel, (IMP)xxxInstanceName, "[email protected]:@");        return YES;
    }    return [super resolveInstanceMethod:sel];

  • forwardingTargetForSelector:(SEL)aSelector


如果resolveInstanceMethod沒有處理,將進行到forwardingTargetForSelector這步來,這時候你可以傳回nil,你也可以用一個Stub物件來接住,把訊息流程流轉到了你的Stub那邊了,然後在你的Stub里添加不存在的Selector,這樣就不會crash了,示例如下:


- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{    NSMethodSignature* sign = [self methodSignatureForSelector:selector];    if (!sign) {        id stub = [[UnrecognizedSelectorHandle new] autorelease];        class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "[email protected]:");        return stub;
    }    return [self forwardingTargetForSelectorSwizzled:selector];
}


  • methodSignatureForSelector:(SEL)aSelector

  • forwardInvocation:(NSInvocation *)anInvocation


這兩個方法一起說,因為他們之間有關聯,


1、當methodSignatureForSelector傳回nil時,會Crash


2、如果methodSignatureForSelector傳回一個定義好的NSMethodSignature,但是沒有實現forwardInvocation,也會閃退,如果實現了forwardInvocation,會先傳回到resolveInstanceMethod然後再才會到forwardInvocation


3、當流轉到forwardInvocation,通過以下方法:


[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];


還可以流轉到多個物件,[anInvocation invokeWithTarget:xxxtarget2]是為了讓不存在的方法有著陸點


  • doesNotRecognizeSelector:(SEL)aSelector 執行到這裡的時候,兩種情況:


1、當methodSignatureForSelector傳回一種任意的方法簽名的時候,也會進入doesNotRecognizeSelector,但是不會閃退


2、當methodSignatureForSelector傳回nil時,進入doesNotRecognizeSelector就會閃退


根據以上流程,最終還是選擇流程2,原因如下:


1、resolveInstanceMethod雖然可以解決問題,給不存在的方法增加到示例中去,會污染當前示例


2、forwardInvocation在三步中式最後一步,會導致流轉的周期變長,而且會產生NSInvocation,性能不是最好的選擇


如何監聽實體化物件什麼時候釋放


先說下這個知識點,因為在接下來的好幾個地方都會用到,會有一些異常的情況,所以需要一種知道當前創建者啥時候釋放,首先會想到dealloc,這樣會Hook的NSObject,在一定程度會影響性能,後面發現一種比較優雅的方法,原理來自於Runtime原始碼:


/***********************************************************************
objc_destructInstance
Destroys 
an instance without freeing memory. 
Calls C++ destructors.
Calls ARR ivar cleanup.
Removes associative references.
Returns `obj`. Does nothing if `obj` is nil.
Be warned that GC DOES NOT CALL THIS. If 
you edit this, also edit finalize.
CoreFoundation and other clients do call this under GC.
**********************************************************************/

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        if (dealloc) obj->clearDeallocating();
    }

    return obj;
}


_object_remove_assocations會釋放所有的用AssociatedObject資料。

objc_setAssociatedObject給當前物件添加一個中間物件,當前物件釋放時,會清理AssociatedObject資料,AssociatedObject的中間物件將被清理釋放,中間物件的dealloc方法將被執行。


最終清理被遺漏的監聽者,會用在KVO和NSNotification清理沒用的監聽者,不過這種方式有以下問題需要註意:


  • 清理的時候執行緒安全問題

  • 清理時機偏晚,是否適合你當前的情況


NSArray,NSMutableArray,NSDictonary,NSMutableDictionary


  • 類族(Class Cluster)


 NSDictonary,NSArray,NSString等,都使用了類族,這種樣式最大的好處就是,可以隱藏抽象基類背後的複雜細節,使用者只需呼叫基類簡單的方法就可以傳回不同的子類實體


  • Swizzle Hook


這裡就不贅述Swizzle概念了,Google到處都是講解的,這裡給一個典型的例子:


swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));

- (id) hookObjectAtIndex:(NSUInteger)index {
    if (index self.count) {
        return [self hookObjectAtIndex:index];
    }
    handleCrashException(@"HookObjectAtIndex invalid index");
    return nil;
}


Zombie Pointer


讓野指標不閃退是模仿了XCode debug的Zombie Object,也參考了網易和美團的做法,主要是以下步驟:


1、Hook住dealloc方法


2、如果當前示例在黑名單里,就把當年前示例加入集合,並把當前物件objc_destructInstance清理取用關係,並未真正釋放記憶體,並將object_setClass設置成自己的中間物件


3、Hook中間物件的方法,收到的訊息都由中間物件來處理


4、維護的野指標集合,要麼根據個數來維護,要麼根據總大小來維護,當滿了,就需要真正釋放物件記憶體free(obj)


存在的問題:


1、需要單獨的記憶體那些問題物件

2、最後釋放記憶體後,再訪問時會閃退,這個方法只是一定程度延遲了閃退時間

3、需要後臺維護黑名單機制,來指定那些問題物件


KVO


KVO在以下情況會導致閃退:

  • 添加監聽後沒有清除會導致閃退

  • 清除不存在的key也會閃退

  • 添加重覆的key導致閃退


需要Hook以下方法:

  • addObserver:forKeyPath:options:context:

  • removeObserver:forKeyPath:


主要解決以下問題:

  • 在註冊監聽後,中間物件需要維護註冊的資料集合,當宿主釋放時,清除還在集合中的監聽者

  • 保護key不存在的情況

  • 保護重覆添加的情況


NSTimer


NSTimer存在以下問題:

  • Target是強取用,記憶體泄漏

  • 在宿主不存在的時候,清理NSTimer


Hook以下方法:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats


解決方法:1.當repeats為NO時,走原始方法 2.當repeats為YES時,新建一個物件,宣告一個target屬性為weak型別,指向引數的target,當中間物件的target為空時,清理NSTimer


NSNotification


NSNotification的主要問題是:

  • 添加通知後,沒有移除導致Crash的問題(不過在iOS9以後沒有這個問題,我在真機8.3測試也沒有這個問題,不知道iOS8是否有這個問題)


Hook以下方法:

  • addObserver:selector:name:object:


原因和解決辦法:問題就在在於和assign和weak問題,野指標問題,要麼置空指標或者刪除空指標的集合


MRC


這裡單獨說下,為什麼工程選擇了MRC,因為在Hook集合型別的時候,啟動的時候就閃退了,Crash的地方在系統類里,Stack里顯示在CF這層,這裡只能猜測系統底層對ARC的支持不好導致的,後續改成MRC就沒有問題,所以這個需要繼續研究和追蹤,如果有知道的同學記得告知我下.


性能


本來是沒有打算註意性能這個問題的,因為從Hook原理的角度來說,只是交換IMP的指向,時間複雜度來說,只是在系統級別上增加了幾條邏輯判斷指令,所以這個影響是極小的,基本可以忽略,我經過測試,迴圈1000000次,沒有HOOK和HOOK相差0.0x秒的,所以減少Crash,來增加這麼點時間複雜度來說,是值得的。


不過最後說一點,就是dealloc確實需要註意,因為這裡存在集合的操作,所以要註意時間複雜度,dealloc執行的很頻繁的,而且主執行緒和子執行緒都會涉及到,尤其是主執行緒一定註意,否則會影響到UI的體驗。


參考資料


https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.m

大白健康系統

最後給出開原始碼:https://github.com/jezzmemo/JJException



編號299,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂