【技術整理】Swizzle應用性研究

Swizzle的常見錯誤及基本原理

示例1


@implementation UIImageView(TestContentMode_Origin)

+ (void)load {

    Method originMethod = class_getInstanceMethod([UIImageView class], @selector(setContentMode:));

    Method swizzledMethod = class_getInstanceMethod([UIImageView class], @selector(nty_setContentMode:));

    method_exchangeImplementations(originMethod, swizzledMethod);

}

- (void)nty_setContentMode:(UIViewContentMode)contentMode {

    NSLog(@"swizzle contentmode %@", self);

    [self nty_setContentMode:contentMode];

}

@end

效果:程序崩潰

崩潰原因分析

method_exchangeImplementations是將兩個SEL指向的IMP互相替換。

originMethod想指向UIImageView的方法setContentMode,然而該方法是UIImageView的父類UIView實現的,所以UIImageView分類中的方法實際上是與UIView的setContentMode做了替換。在UIView的實例調用setContentMode時,會調用nty_setContentMode的SEL,UIView中沒有實現此方法,導致崩潰.

見圖1,2

圖1
圖2

引申:Method, SEL, IMP


// Method 在頭文件 objc_class.h中定義如下:

typedef struct objc_method *Method;

typedef struct objc_method {

    SEL method_name;

    char *method_types;

    IMP method_imp;

};

// SEL的定義為:

typedef struct objc_selector  *SEL; 

// IMP 的含義:

typedef id (*IMP)(id, SEL, ...);

SEL的定義為:是一個指向 objc_selector 指針,表示方法的名字/簽名。

IMP 的含義:是一個函數指針,這個被指向的函數包含一個接收消息的對象id(self 指針), 調用方法的選標 SEL (方法名),以及不定個數的方法參數,并返回一個id。也就是說 IMP 是消息最終調用的執行代碼,是方法真正的實現代碼 。

引申:class


struct objc_class {

    struct objc_class super_class;  /*父類*/

    const char *name;                /*類名字*/

    long version;                  /*版本信息*/

    long info;                        /*類信息*/

    long instance_size;              /*實例大小*/

    struct objc_ivar_list *ivars;    /*實例參數鏈表*/

    struct objc_method_list **methodLists;  /*方法鏈表*/

    struct objc_cache *cache;              /*方法緩存*/

    struct objc_protocol_list *protocols;  /*協議鏈表*/

};

methodLists方法鏈表里面存儲的是Method 類型。selector 就是指 Method的 SEL, address就是指Method的 IMP。

示例1優化

示例1證明,直接使用method_exchangeImplementations進行swizzle,有可能出現崩潰問題。使用第三方庫JRSwizzle的方法jr_swizzleMethod:withMethod:error:對該問題進行了優化。


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

#if OBJC_API_VERSION >= 2

Method origMethod = class_getInstanceMethod(self, origSel_);

if (!origMethod) {

...(容錯處理,節約篇幅,省略)

return NO;

}

Method altMethod = class_getInstanceMethod(self, altSel_);

if (!altMethod) {

...(容錯處理,節約篇幅,省略)

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;

#else

...(低版本API的配置方式,節約篇幅,省略)

#endif

}

該方法通過class_addMethod保證在父類實現原生方法或被swizzle方法而子類沒有實現的情況下,重新生成一個新的Method,SEL不變,IMP指向父類方法的IMP,保存在子類的method_list中(即將子類中實現同樣的方法)。

class_addMethod:如果發現方法已經存在,會失敗返回,也可以用來做檢查用,我們這里是為了避免源方法沒有實現的情況;如果方法沒有存在,我們則先嘗試添加被替換的方法的實現

示例2

通過jr_swizzleMethod:withMethod:error:進行setContentMode的swizzle


@implementation UIImageView (TestContentMode_JR)

+ (void)load {   

    [[UIImageView class] jr_swizzleMethod:@selector(setContentMode:) withMethod:@selector(nty_setContentMode:) error:nil];

}

- (void)nty_setContentMode:(UIViewContentMode)contentMode {

    NSLog(@"swizzle contentmode(JR) %@", self);

    [self nty_setContentMode:contentMode];

}

@end

該方法中,在class_addMethod時,見圖3.

圖3

在method_exchangeImplementations后,見圖4.

圖4

當前,可以完美解決方問題

示例3

針對示例1, 如果不使用jr_swizzleMethod:withMethod:error:的方式,仍有辦法解決此問題。

示例1之所以崩潰是因為在UIView執行setContentMode時,會調用UIView不存在的方法nty_setContentMode。那么,將swizzle的方法從UIImageView的分類中改為寫在UIView的分類中,即可解決此問題。


@implementation UIView(TestContentMode_Origin)

+ (void)load {

    Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));

    Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));

    method_exchangeImplementations(originMethod, swizzledMethod);

}

- (void)nty_setContentMode:(UIViewContentMode)contentMode {

    if ([self isKindOfClass:[UIImageView class]]) {

      NSLog(@"swizzle contentmode %@", self);

    }

    [self nty_setContentMode:contentMode];

}

@end

示例4

若由于需求原因,既有針對UIView的setContentMode的swizzle方法,也有針對UIImageView的swizzle方法(即示例2與示例3共存)。將會發生邏輯錯誤。

兩個swizzle都是寫在分類的+load方法中,兩方法的調用順序與build phase中的文件編繹順序有關。此處,我們假設UIView (TestContentMode_Origin)的+load先被調用

見圖5

圖5

UIImageView(TestContentMode_Origin)的+load再被調用

見圖6?7

圖6
圖7

那么此時,若UIView調用setContentMode不會有問題,UIImageView調用時會出現無限調用循環的問題

拓展:RSSwizzle提供了另外一種更加健壯的Swizzle方式,如以下代碼所示。但此代碼在我們項目中沒有普及,我也沒有確認此方法是否會出現其他問題,此處列出僅供參考。


RSSwizzleInstanceMethod([UIView class],

                            @selector(setContentMode:),

                            RSSWReturnType(void),

                            RSSWArguments(UIViewContentMode contentMode),

                            RSSWReplacement({

    // Returning modified return value.

    NSLog(@"swizzle contentmode %@", @(contentMode));

    // 先執行原始方法

    RSSWCallOriginal();

                            }), 0, NULL);

示例5

針對示例4的需求,建議將UIImageView的swizzle方法寫到UIView的分類中。即示例3的代碼。那么代碼會變成以下的樣式。


@implementation UIView(ForUIViewSwizzle)

+ (void)load {

    Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));

    Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));

    method_exchangeImplementations(originMethod, swizzledMethod);

}

- (void)nty_setContentMode:(UIViewContentMode)contentMode {

    // 執行針對UIImageView的swizzle的邏輯

    [self nty_setContentMode:contentMode];

}

@end

@implementation UIView(ForUIImageViewSwizzle)

+ (void)load {

    Method originMethod = class_getInstanceMethod([UIView class], @selector(setContentMode:));

    Method swizzledMethod = class_getInstanceMethod([UIView class], @selector(nty_setContentMode:));

    method_exchangeImplementations(originMethod, swizzledMethod);

}

- (void)nty_setContentMode:(UIViewContentMode)contentMode {

    if ([self isKindOfClass:[UIImageView class]]) {

      // 執行針對UIImageView的swizzle的邏輯

    }

    [self nty_setContentMode:contentMode];

}

@end

見圖8

圖8

由于兩個分類的swizzle名字相同,通過class_getInstanceMethod獲得nty_setContentMode的Method將一直是同一個(該問題出現原因需要詳細了解class、category實現機制,此處不多做綴述),所以相當于兩個Method互相swizzle了兩次,最終SEL與IMP的連接仍為圖8的結果。

示例6

將示例5的代碼做一點點調整,將UIView(ForUIImageViewSwizzle)中替換nty_setContentMode方法名改為nty2_setContentMode

見圖9?10?11

圖9
圖10
圖11

最終成功完成需求

Swizzle在項目中應用出現的問題

iOS項目在很多方法中如果傳參不對,會直接導致crash。比如NSString的substringToIndex:方法在數組越界時、NSDictionary傳入nil值時、NSArray數組越界時。這些情況,我們可能用swizzle將這些系統方法進行swizzle,加入數據空值、數組越界情況的容錯處理,有效減少崩潰率。

此處,以NSString的substringToIndex:方法為例。

示例1


@implementation NSString (AvoidCrash)

+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [[NSString class] jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

    };

}

- (NSString*)nty_substringToIndex:(NSUInteger)to {

    if (to <= self.length) {

        return [self nty_substringToIndex:to];

    }

    return self;

}

@end

在Demo中寫下測試代碼測試此功能


- (void)testCrash {

    NSString *testStr = @"asdf";

    [testStr substringToIndex:100];

}

然后,崩潰了,發現此swizzle方法完全沒有被調用。

類簇

類簇 是一群隱藏在通用接口下的與實現相關的類,使得我們編寫的代碼可以獨立于底層實現(因為接口是穩定的)。

示例2

將代碼改成如下形式


+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        Class clazz = nil;

        id obj;

        /* 普通方法 */

        obj = [[NSString alloc] init];

        clazz = [obj class];

        [obj release];

        ACSwizzle(clazz,substringToIndex:);

    });

}

然而,根據友盟上統計的crash結果,仍有substringToIndex導致的崩潰問題。

示例3

示例2的崩潰問題是由于,不同形式聲明的NSString產生的類簇有可能不同。為避免此問題,寫了一個Demo去讀取出不同NSString聲明方式會出現的所有類。

2017-12-26 15:19:39.378849+0800 TestClassType[3787:1570162] [NSString alloc] 's class is NSPlaceholderString

2017-12-26 15:19:39.378881+0800 TestClassType[3787:1570162] [[NSString alloc] init] 's class is __NSCFConstantString

2017-12-26 15:19:39.378896+0800 TestClassType[3787:1570162] @"as" 's class is __NSCFConstantString

2017-12-26 15:19:39.378908+0800 TestClassType[3787:1570162] @"" 's class is __NSCFConstantString

2017-12-26 15:19:39.378918+0800 TestClassType[3787:1570162] @"as".copy 's class is __NSCFConstantString

2017-12-26 15:19:39.378942+0800 TestClassType[3787:1570162] ([NSString stringWithFormat:@"aa%@", @"a"]) 's class is NSTaggedPointerString

2017-12-26 15:19:39.378998+0800 TestClassType[3787:1570162] [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] 's class is NSTaggedPointerString

2017-12-26 15:19:39.379032+0800 TestClassType[3787:1570162]

然后將所有的類簇都進行swizzle


+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        /* 普通方法 */

        NSArray *classNameList = @[

                                  @"__NSCFConstantString",

                                  @"NSTaggedPointerString"

                                  ];

        for (NSString *className in classNameList) {

            Class clazz = NSClassFromString(className);

            if (clazz) {

                [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

            }

        }

    });

}

經運行,發生了iOS 8設備100%崩潰無法使用的問題。

示例4

將自己查詢類簇的Demo在iOS 8設備上運行,導出如下結果

2017-12-26 15:16:37.673 TestClassType[389:48818] [NSString alloc] 's class is NSPlaceholderString

2017-12-26 15:16:37.673 TestClassType[389:48818] [[NSString alloc] init] 's class is __NSCFConstantString

2017-12-26 15:16:37.673 TestClassType[389:48818] @"as" 's class is __NSCFConstantString

2017-12-26 15:16:37.674 TestClassType[389:48818] @"" 's class is __NSCFConstantString

2017-12-26 15:16:37.674 TestClassType[389:48818] @"as".copy 's class is __NSCFConstantString

2017-12-26 15:16:37.674 TestClassType[389:48818] ([NSString stringWithFormat:@"aa%@", @"a"]) 's class is __NSCFString

2017-12-26 15:16:37.674 TestClassType[389:48818] [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] 's class is __NSCFString

2017-12-26 15:16:37.674 TestClassType[389:48818]

發現在iOS 8設備上,沒有NSTaggedPointerString這種類型,如果對NSTaggedPointerString進行swizzle,就會出現崩潰。

于是,想出一種復雜的判斷各因素的方法,它將會考慮NSString不同聲明形式的類簇的排重問題,NSString與NSMutableString的類的相同類簇的排重問題


@implementation NSMutableString (AvoidCrash)

+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        id obj = [NSMutableString alloc];

        Class clazz;

        NSData*data      = [@"testdata" dataUsingEncoding:NSUTF8StringEncoding];

        NSArray *varList = @[

            [[[NSString alloc] init] autorelease],

            @"as",

            @"",

            @"as".copy,

            [NSString stringWithFormat:@"aa%@", @"a"],

            [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]

        ];

        NSArray *mutaVarList = @[

            [[[NSMutableString alloc] init] autorelease],

            @"as".mutableCopy,

            @"".mutableCopy,

            [NSMutableString stringWithString:@"as"],

            [[[NSMutableString alloc] initWithString:@"as"] autorelease],

            [[[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]

        ];

        [self swizzleForVarList:varList

                    mutaVarList:mutaVarList

                      varBlock:^(Class clazz) {

                [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

        } mutaVarBlock:^(Class clazz) {

                [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

        }];

    });

}

- (void)swizzleForVarList:(NSArray*)varList

              mutaVarList:(NSArray*)mutaVarList

                varBlock:(void (^)(Class clazz))varSwizzleBlock

            mutaVarBlock:(void (^)(Class clazz))mutaVarSwizzleBlock {

    // 使用Set,保證數據去重

    NSMutableSet *mutaClassList = [NSMutableSet set];

    NSMutableSet *classList    = [NSMutableSet set];

    for (NSString *var in mutaVarList) {

        // 將MutableXXX的變量轉成類名存入mutaClassList

        [mutaClassList addObject:[var class]];

    }

    for (NSString *var in varList) {

        // 將XXX的變量轉成類名存入classList

        [classList addObject:[var class]];

    }

    for (Class clazz in mutaClassList) {

        // 遍歷MutableXXX類簇的各種隱藏子類,進行swizzle

        if (mutaVarSwizzleBlock) {

            mutaVarSwizzleBlock(clazz);

        }

    }

    for (Class clazz in classList) {

        // 有時MutableXXX與XXX類簇中的隱藏子類有相同的(比如NSString與NSMutableString都有__NSCFString)

        // 此處確保不會被swizzle兩處

        if (![mutaClassList containsObject:clazz]

            && varSwizzleBlock) {

            varSwizzleBlock(clazz);

        }

    }

}

@end

此時,無明顯的問題。但在編寫Unit Test遍歷各種錯誤情況時,發現@"sa"這種形式的NSString在執行數組越界時仍會崩潰。

經分析,@"sa"形式的類簇是__NSCFConstantString。而__NSCFConstantString的父類是__NSCFString。__NSCFConstantString的substringToIndex方法是實現在__NSCFString中的。此處就會發生父類、子類兩次swizzle引起的問題,導致__NSCFConstantString的substringToIndex方法仍指向系統方法的IMP。

Demo5

而我們很難去識別類簇之間是否有繼承關系,而繼承關系的類簇的方法是否是只在父類中實現。

所以最終,對避免crash想使用的高級辯別類簇的功能全線失敗。我們使用簡單的網絡上歸納好的類簇進行swizzle,并對這些方法進行了詳進的Unit Test編寫測試。最終發現, 此化繁為簡的方法,能夠完美的解決所有問題。


/* 普通方法 */

        // iOS 8是__NSCFConstantString,iOS 11上是__NSCFConstantString

        id obj = [[NSString alloc] init];

        Class clazz = [obj class];

        [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

        // iOS 8上是__NSCFString, iOS 11上是NSTaggedPointerString

        id obj2 = [NSString stringWithFormat:@"aa%@", @"a"];

        if (![obj2 isKindOfClass:clazz]

            && ![obj isKindOfClass:[obj2 class]]) {

            // 若obj2與obj的類簇不同且不是繼承關系,則進行swizzle

            // (__NSCFConstantString的父類是__NSCFString)

            clazz = [obj2 class];

            [clazz jr_swizzleMethod:@selector(substringToIndex:) withMethod:@selector(nty_substringToIndex:) error:nil];

        }

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。