【技术整理】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];

        }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345