iOS界的毒瘤-MethodSwizzling

原文地址

為什么有這篇博文

不知道何時(shí)開始iOS面試開始流行起來(lái)詢問什么是 Runtime,于是 iOSer 一聽 Runtime 總是就提起 MethodSwizzling,開口閉口就是黑科技。但其實(shí)如果讀者留意過C語(yǔ)言的 Hook 原理其實(shí)會(huì)發(fā)現(xiàn)所謂的鉤子都是框架或者語(yǔ)言的設(shè)計(jì)者預(yù)留給我們的工具,而不是什么黑科技,MethodSwizzling 其實(shí)只是一個(gè)簡(jiǎn)單而有趣的機(jī)制罷了。然而就是這樣的機(jī)制,在日常中卻總能成為萬(wàn)能藥一般的被肆無(wú)忌憚的使用。

很多 iOS 項(xiàng)目初期架構(gòu)設(shè)計(jì)的不夠健壯,后期可擴(kuò)展性差。于是 iOSer 想起了 MethodSwizzling 這個(gè)武器,將項(xiàng)目中一個(gè)正常的方法 hook 的滿天飛,導(dǎo)致項(xiàng)目的質(zhì)量變得難以?控制。曾經(jīng)我也愛在項(xiàng)目中濫用 MethodSwizzling,但在踩到坑之前總是不能意識(shí)到這種糟糕的做法會(huì)讓項(xiàng)目陷入怎樣的險(xiǎn)境。于是我才明白學(xué)習(xí)某個(gè)機(jī)制要去深入的理解機(jī)制的設(shè)計(jì),而不是跟風(fēng)濫用,帶來(lái)糟糕的后果。最后就有了這篇文章。

Hook的對(duì)象

在 iOS 平臺(tái)常見的 hook 的對(duì)象一般有兩種:

  1. C/C++ functions
  2. Objective-C method

?對(duì)于 C/C+ +的 hook 常見的方式可以使用 facebook 的 fishhook 框架,具體原理可以參考深入理解Mac OS X & iOS 操作系統(tǒng) 這本書。
對(duì)于 Objective-C Methods 可能大家更熟悉一點(diǎn),本文也只討論這個(gè)。

最常見的hook代碼

相信很多人使用過 JRSwizzle 這個(gè)庫(kù),或者是看過 http://nshipster.cn/method-swizzling/ 的博文。
上述的代碼簡(jiǎn)化如下。


+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_ {

    Method origMethod = class_getInstanceMethod(self, origSel_);
    if (!origMethod) {
        SetNSError(error_, @"original method %@ not found for class %@", NSStringFromSelector(origSel_), [self class]);
        return NO;
    }

    Method altMethod = class_getInstanceMethod(self, altSel_);
    if (!altMethod) {
        SetNSError(error_, @"alternate method %@ not found for class %@", NSStringFromSelector(altSel_), [self class]);
        return NO;
    }

    class_addMethod(self,
                    origSel_,
                    class_getMethodImplementation(self, origSel_),
                    method_getTypeEncoding(origMethod));

    class_addMethod(self,
                    altSel_,
                    class_getMethodImplementation(self, altSel_),
                    method_getTypeEncoding(altMethod));

    method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));
    return YES;

在?Swizzling情況極為普通的情況下上述代碼不會(huì)出現(xiàn)問題,但是場(chǎng)景復(fù)雜之后上面的代碼會(huì)有很多安全隱患。

MethodSwizzling泛濫下的隱患

Github有一個(gè)?很健壯的庫(kù) RSSwizzle(這也是本文推薦Swizzling的最終方式) 指出了上面代碼帶來(lái)的風(fēng)險(xiǎn)點(diǎn)。

  1. 只在 +load 中執(zhí)行 swizzling 才是安全的。

  2. 被 hook 的方法必須是當(dāng)前類自身的方法,如果把繼承來(lái)的 IMP copy 到自身上面會(huì)存在問題。父類的方法應(yīng)該在調(diào)用的時(shí)候使用,而不是 swizzling 的時(shí)候 copy 到子類。

  3. 被 Swizzled 的方法如果依賴與 cmd ,hook 之后 cmd 發(fā)送了變化,就會(huì)有問題(一般你 hook 的是系統(tǒng)類,也不知道系統(tǒng)用沒用 cmd 這個(gè)參數(shù))。

  4. 命名如果沖突導(dǎo)致之前 hook 的失效 或者是循環(huán)調(diào)用。

上述問題中第一條和第四條說(shuō)的是通常的 MethodSwizzling 是在分類里面實(shí)現(xiàn)的, 而分類的 Method 是被Runtime 加載的時(shí)候追加到類的 MethodList ,如果不是在 +load 是執(zhí)行的 Swizzling 一旦出現(xiàn)重名,那么 SEL 和 IMP 不匹配致 hook 的結(jié)果是循環(huán)調(diào)用。

第三條是一個(gè)不容易被發(fā)現(xiàn)的問題。
我們都知道 Objective-C Method 都會(huì)有兩個(gè)隱含的參數(shù) ?self, cmd,有的時(shí)候開發(fā)者在使用關(guān)聯(lián)屬性的適合可能懶得聲明 (void *) 的 key,直接使用 cmd 變量 objc_setAssociatedObject(self, _cmd, xx, 0); 這會(huì)導(dǎo)致對(duì)當(dāng)前IMP對(duì) cmd 的依賴。

一旦此方法被 Swizzling,那么方法的 cmd 勢(shì)必會(huì)發(fā)生變化,出現(xiàn)了 bug 之后想必你一定找不到,等你找到之后心里一定會(huì)問候那位 Swizzling 你的方法的開發(fā)者祖宗十八代安好的,再者如果你 Swizzling 的是系統(tǒng)的方法恰好系統(tǒng)的方法內(nèi)部用到了 cmd ..._(此處后背驚起一陣?yán)浜梗?/p>

Copy父類的方法帶來(lái)的問題

上面的第二條才是我們最容易遇見的場(chǎng)景,并且是99%的開發(fā)者都不會(huì)注意到的問題。下面我們來(lái)做個(gè)試驗(yàn)


@implementation Person

- (void)sayHello {
    NSLog(@"person say hello");
}

@end

@interface Student : Person

@end

@implementation Student (swizzle)

+ (void)load {
    [self jr_swizzleMethod:@selector(s_sayHello) withMethod:@selector(sayHello) error:nil];
}

- (void)s_sayHello {
    [self s_sayHello];

    NSLog(@"Student + swizzle say hello");
}

@end

@implementation Person (swizzle)

+ (void)load {
    [self jr_swizzleMethod:@selector(p_sayHello) withMethod:@selector(sayHello) error:nil];
}

- (void)p_sayHello {
    [self p_sayHello];
    
    NSLog(@"Person + swizzle say hello");
}

@end

上面的代碼中有一個(gè) Person 類實(shí)現(xiàn)了 sayHello 方法,有一個(gè) Student 繼承自 Person, 有一個(gè)Student 分類 Swizzling 了原來(lái)的? sayHello, 還有一個(gè) Person 的分類也 Swizzling 了原來(lái)的 sayhello 方法。

當(dāng)我們生成一個(gè) Student 類的實(shí)例并且調(diào)用 sayHello 方法,我們期望的輸出如下:

"person say hello"
"Person + swizzle say hello"
"Student + swizzle say hello"

但是輸出有可能是這樣的:

"person say hello"
"Student + swizzle say hello"

出現(xiàn)這樣的場(chǎng)景是由于在 build Phasescompile Source 順序子類分類在父類分類之前。

我們都知道在 Objective-C 的世界里父類的 +load 早于子類,但是并沒有?限制父類的分類加載?會(huì)早于子類的分類的加載,實(shí)際上這取決于編譯的順序。最終會(huì)按照編譯的順序合并進(jìn) Mach-O ?的固定 section 內(nèi)。

下面會(huì)分析下為什么代碼會(huì)出現(xiàn)這樣的場(chǎng)景。

最開始的時(shí)候父類擁有自己的 sayHello 方法,子類擁有分類添加的 s_sayHello 方法并且在 s_sayHello 方法內(nèi)部調(diào)用了 sel 為 s_sayHello 方法。

但是子類的分類在使用上面提到的 MethodSwizzling 的方法會(huì)導(dǎo)致?如下圖的變化

由于調(diào)用了 class_addMethod 方法會(huì)導(dǎo)致重新生成一份新的Method添加到 Student 類上面 但是 sel 并沒有發(fā)生變化,IMP 還是指向父類唯一的那個(gè) IMP。
之后交換了子類兩個(gè)方法的 IMP 指針。于是方法引用變成了如下結(jié)構(gòu)。
其中虛線指出的是方法的調(diào)用路徑。

單純?cè)?Swizzling 一次的時(shí)候并沒有什么問題,但是我們并不能保證同事出于某種不可告人的目的的又去 Swizzling 了父類,或者是我們引入的第三庫(kù)做了這樣的操作。

于是我們?cè)?Person 的分類里面 Swizzling 的時(shí)候會(huì)導(dǎo)致方法結(jié)構(gòu)發(fā)生如下變化。

我們的代碼調(diào)用路徑就會(huì)是下圖這樣,相信你已經(jīng)明白了前面的代碼執(zhí)行結(jié)果中為什么父類在子類之后 Swizzling 其實(shí)并沒有對(duì)子類 hook 到。

這只是其中一種很常見的場(chǎng)景,造成的影響也只是 Hook 不到父類的派生類而已,?也不會(huì)造成一些嚴(yán)重的 Crash 等明顯現(xiàn)象,所以大部分開發(fā)者對(duì)此種行為是毫不知情的。

對(duì)于這種 Swizzling 方式的不確定性有一篇博文分析的更為全面玉令天下的博客Objective-C Method Swizzling

換個(gè)姿勢(shì)來(lái)Swizzling

前面提到 RSSwizzle 是另外一種更加健壯的Swizzling方式。

這里使用到了如下代碼

   RSSwizzleInstanceMethod([Student class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // Calling original implementation.
                                                RSSWCallOriginal();
                                                // Returning modified return value.
                                                NSLog(@"Student + swizzle say hello sencod time");
                                            }), 0, NULL);

    RSSwizzleInstanceMethod([Person class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // Calling original implementation.
                                                RSSWCallOriginal();
                                                // Returning modified return value.
                                                NSLog(@"Person + swizzle say hello");
                                            }), 0, NULL);

由于 RS 的方式需要提供一種 Swizzling 任何類型的簽名的 SEL,所以 RS 使用的是宏作為代碼包裝的入口,并且由開發(fā)者自行保證方法的參數(shù)個(gè)數(shù)和參數(shù)類型的正確性,所以使用起來(lái)也較為晦澀。 可能這也是他為什么這么優(yōu)秀但是 star 很少的原因吧 :(。

我們將宏展開


    RSSwizzleImpFactoryBlock newImp = ^id(RSSwizzleInfo *swizzleInfo) {
        void (*originalImplementation_)(__attribute__((objc_ownership(none))) id, SEL);
        SEL selector_ = @selector(sayHello);
        return ^void (__attribute__((objc_ownership(none))) id self) {
            IMP xx = method_getImplementation(class_getInstanceMethod([Student class], selector_));
            IMP xx1 = method_getImplementation(class_getInstanceMethod(class_getSuperclass([Student class]) , selector_));
            IMP oriiMP = (IMP)[swizzleInfo getOriginalImplementation];
                ((__typeof(originalImplementation_))[swizzleInfo getOriginalImplementation])(self, selector_);
            //只有這一行是我們的核心邏輯
            NSLog(@"Student + swizzle say hello");
            
        };
        
    };
    [RSSwizzle swizzleInstanceMethod:@selector(sayHello)
                             inClass:[[Student class] class]
                       newImpFactory:newImp
                                mode:0 key:((void*)0)];;

RSSwizzle核心代碼其實(shí)只有一個(gè)函數(shù)



static void swizzle(Class classToSwizzle,
                    SEL selector,
                    RSSwizzleImpFactoryBlock factoryBlock)
{
    Method method = class_getInstanceMethod(classToSwizzle, selector);

    __block IMP originalIMP = NULL;


    RSSWizzleImpProvider originalImpProvider = ^IMP{

        IMP imp = originalIMP;
        
        if (NULL == imp){

            Class superclass = class_getSuperclass(classToSwizzle);
            imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
        }
        return imp;
    };
    
    RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
    swizzleInfo.selector = selector;
    swizzleInfo.impProviderBlock = originalImpProvider;

    id newIMPBlock = factoryBlock(swizzleInfo);
    
    const char *methodType = method_getTypeEncoding(method);
    
    IMP newIMP = imp_implementationWithBlock(newIMPBlock);

    originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
}

上述代碼已經(jīng)刪除無(wú)關(guān)的加鎖,防御邏輯,簡(jiǎn)化理解。

我們可以看到 RS 的代碼其實(shí)是構(gòu)造了一個(gè) Block 里面裝著我們需要的執(zhí)行的代碼。

然后再把我們的名字叫 originalImpProviderBloc 當(dāng)做參數(shù)傳遞到我們的block里面,這里面包含了對(duì)將要被 Swizzling 的原始 IMP 的調(diào)用。

需要注意的是使用 class_replaceMethod 的時(shí)候如果一個(gè)方法來(lái)自父類,那么就給子類 add 一個(gè)方法, 并且把這個(gè) NewIMP 設(shè)置給他,然后返回的結(jié)果是NULL。

originalImpProviderBloc 里面我們注意到如果 imp 是 NULL的時(shí)候,是動(dòng)態(tài)的拿到父類的 Method 然后去執(zhí)行。

我們還用圖來(lái)分析代碼。

最開始 Swizzling 第一次的時(shí)候,由于子類不存在 sayHello 方法,再添加方法的時(shí)候由于返回的原始 IMP 是 NULL,所以對(duì)父類的調(diào)用是動(dòng)態(tài)獲取的,而不是通過之前的 sel 指針去調(diào)用。

如果我們?cè)俅螌?duì) Student Hook,由于 Student 已經(jīng)有 sayHello 方法,這次 replace 會(huì)返回原來(lái) IMP 的指針, 然后新的 IMP 會(huì)執(zhí)被填充到 Method 的指針指向。

由此可見我們的方法引用是一個(gè)鏈表形狀的。

同理我們?cè)?hook 父類的時(shí)候 父類的方法引用也是一個(gè)鏈表樣式的。

相信到了這里你已經(jīng)理解 RS 來(lái) Swizzling 方式是:

如果是父類的方法那么就動(dòng)態(tài)查找,如果是自身的方法就構(gòu)造方法引用鏈。來(lái)保證多次 Swizzling 的穩(wěn)定性,并且不會(huì)和別人的 Swizzling 沖突。

而且 RS 的實(shí)現(xiàn)由于不是分類的方法也不用約束開發(fā)者必須在 +load 方法調(diào)用才能保證安全,并且cmd 也不會(huì)發(fā)生變化。

其他Hook方式

其實(shí)著名的 Hook 庫(kù)還有一個(gè)叫 Aspect 他利用的方法是把所有的方法調(diào)用指向 _objc_msgForward 然后自行實(shí)現(xiàn)消息轉(zhuǎn)發(fā)的步驟,在里面自行處理參數(shù)列表和返回值,通過 NSInvocation 去動(dòng)態(tài)調(diào)用。

國(guó)內(nèi)知名的熱修復(fù)庫(kù) JSPatch 就是借鑒這種方式來(lái)實(shí)現(xiàn)熱修復(fù)的。

但是上面的庫(kù)要求必須是最后執(zhí)行的確保 Hook 的成功。 而且他不兼容其他 Hook 方式,所以技術(shù)選型的時(shí)候要深思熟慮。

?什么時(shí)候需要Swizzling

我記得第一次學(xué)習(xí) AO P概念的時(shí)候是當(dāng)初在學(xué)習(xí) javaWeb 的時(shí)候 Serverlet 里面的 FilterChain,開發(fā)者可以實(shí)現(xiàn)各種各種的過濾器然后在過濾器中插入log, 統(tǒng)計(jì), 緩存等無(wú)關(guān)主業(yè)務(wù)邏輯的功能行性代碼, 著名的框架 Struts2 就是這樣實(shí)現(xiàn)的。

iOS 中由于 Swizzling 的 API 的簡(jiǎn)單易用性導(dǎo)致開發(fā)者肆意濫用,影響了項(xiàng)目的穩(wěn)定性。
當(dāng)我們想要 Swizzling 的時(shí)候應(yīng)該思考下我們能不能利用良好的代碼和架構(gòu)設(shè)計(jì)來(lái)實(shí)現(xiàn),或者是深入語(yǔ)言的特性來(lái)實(shí)現(xiàn)。

一個(gè)利用語(yǔ)言特性的例子

我們都知道在iOS8下的?操作系統(tǒng)中通知中心會(huì)持有一個(gè) __unsafe_unretained 的觀察者指針。如果?觀察者在 ?dealloc 的時(shí)候忘記從通知中心中移除,之后如果觸發(fā)相關(guān)的通知就會(huì)造成 Crash。

我在設(shè)計(jì)防 Crash 工具 XXShield 的時(shí)候最初是 Hook NSObjec 的 dealloc 方法,在里面做相應(yīng)的移除觀察者操作。后來(lái)一位真大佬提出這是一個(gè)非常不明智的操作,因?yàn)?dealloc 會(huì)影響全局的實(shí)例的釋放,開發(fā)者并不能保證代碼質(zhì)量非常有保障,一旦出現(xiàn)問題將會(huì)引起整個(gè) APP 運(yùn)行期間大面積崩潰或異常行為。

下面我們先來(lái)看下 ObjCRuntime 源碼關(guān)于一個(gè)對(duì)象釋放時(shí)要做的事情,代碼約在objc-runtime-new.mm第6240行。


/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

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

    return obj;
}


/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

上面的邏輯中明確了寫明了一個(gè)對(duì)象在釋放的時(shí)候初了調(diào)用 dealloc 方法,還需要斷開實(shí)例上綁定的觀察對(duì)象, 那么我們可以在添加觀察者的時(shí)候給觀察者動(dòng)態(tài)的綁定一個(gè)關(guān)聯(lián)對(duì)象,然后關(guān)聯(lián)對(duì)象可以反向持有觀察者,然后在關(guān)聯(lián)對(duì)象釋放的時(shí)候去移除觀察者,由于不能造成循環(huán)引用所以只能選擇 __weak 或者 __unsafe_unretained 的指針, 實(shí)驗(yàn)得知 __weak 的指針在 dealloc 之前就已經(jīng)被清空, 所以我們只能使用 __unsafe_unretained 指針。


@interface XXObserverRemover : NSObject {
    __strong NSMutableArray *_centers;
    __unsafe_unretained id _obs;
}
@end
@implementation XXObserverRemover

- (instancetype)initWithObserver:(id)obs {
    if (self = [super init]) {
        _obs = obs;
        _centers = @[].mutableCopy;
    }
    return self;
}

- (void)addCenter:(NSNotificationCenter*)center {
    if (center) {
        [_centers addObject:center];
    }
}

- (void)dealloc {
    @autoreleasepool {
        for (NSNotificationCenter *center in _centers) {
            [center removeObserver:_obs];
        }
    }
}

@end

void addCenterForObserver(NSNotificationCenter *center ,id obs) {
    XXObserverRemover *remover = nil;
    static char removerKey;
    @autoreleasepool {
        remover = objc_getAssociatedObject(obs, &removerKey);
        if (!remover) {
            remover = [[XXObserverRemover alloc] initWithObserver:obs];
            objc_setAssociatedObject(obs, &removerKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        [remover addCenter:center];
    }
    
}
void autoHook() {
    RSSwizzleInstanceMethod([NSNotificationCenter class], @selector(addObserver:selector:name:object:),
                            RSSWReturnType(void), RSSWArguments(id obs,SEL cmd,NSString *name,id obj),
                            RSSWReplacement({
        RSSWCallOriginal(obs,cmd,name,obj);
        addCenterForObserver(self, obs);
    }), 0, NULL);
    
}

需要注意的是在添加關(guān)聯(lián)者的時(shí)候一定要將代碼包含在一個(gè)自定義的 AutoreleasePool 內(nèi)。

我們都知道在 Objective-C 的世界里一個(gè)對(duì)象如果是 Autorelease 的 那么這個(gè)對(duì)象在當(dāng)前方法棧結(jié)束后才會(huì)延時(shí)釋放,在 ARC 環(huán)境下?,一般一個(gè) Autorelease 的對(duì)象會(huì)被放在一個(gè)系統(tǒng)提供的 AutoreleasePool 里面,然后AutoReleasePool drain 的時(shí)候再去釋放內(nèi)部持有的對(duì)象,通常情況下命令行程序是沒有問題的,但是在iOS的環(huán)境中 AutoReleasePool是在 Runloop 控制下在空閑時(shí)間進(jìn)行釋放的,這樣可以提升用戶體驗(yàn),避免造成卡頓,但是在我們這種場(chǎng)景中會(huì)有問題,我們嚴(yán)格依賴了觀察者?調(diào)用 dealloc 的時(shí)候關(guān)聯(lián)對(duì)象也會(huì)去 dealloc,如果系統(tǒng)的 AutoReleasePool 出現(xiàn)了延時(shí)釋放,會(huì)導(dǎo)致當(dāng)前對(duì)象被回收之后 過段時(shí)間關(guān)聯(lián)對(duì)象才會(huì)釋放,這時(shí)候前文使用的 __unsafe_unretained 訪問的?就是非法地址。

我們?cè)谔砑雨P(guān)聯(lián)對(duì)象的時(shí)候添加一個(gè)自定義的 AutoreleasePool 保證了對(duì)關(guān)聯(lián)對(duì)象引用的單一性,保證了我們依賴的釋放順序是正確的。從而正確的移除觀察者。

參考

  1. JRSwizzle
  2. RSSwizzle
  3. Aspect
  4. 玉令天下的博客Objective-C Method Swizzling
  5. 示例代碼

友情感謝

最后感謝 騎神 大佬修改我那蹩腳的文字描述。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容