MethodSwizzling

MethodSwizzling的实现

首先oc中的方法,是一个 objc_method 结构体,由三部分组成

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name        // 方法sel                         
    char * _Nullable method_types     // 方法参数
    IMP _Nonnull method_imp            // 方法实现
}  

IMP 指向了方法的实现

我们常使用objc/runtime.h中提供的api:method_exchangeImplementations(Method m1, Method m2)来交换IMP,重新绑定sel和 IMP对应关系

Method oriMethod = class_getInstanceMethod(self, @selector(method1));
Method swiMethod = class_getInstanceMethod(self, @selector(method2));
method_exchangeImplementations(oriMethod, swiMethod);

class_getInstanceMethod函数,在当前类没有实现方法时,会返回父类的方法

method_exchangeImplementations函数仅仅交换了两个 Method 结构体的 imp,上面的代码,当子类没有实现oriMethod时会将父类和子类方法的实现互换.

因此可以使用下面的方式

    Method oriMethod = class_getInstanceMethod(self, @selector(method1));
    Method swiMethod = class_getInstanceMethod(self, @selector(method2));
    method_exchangeImplementations(oriMethod, swiMethod);
    
    // 添加方法
    if(class_addMethod(self, @selector(method1), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))){
        // 如果添加成功了,当前类没有该方法,需要把swiMethod的实现替换成oriMethod的.
        class_replaceMethod(self, @selector(method2), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }
    else{
        // 如果添加失败了,当前类有这个方法
        method_exchangeImplementations(oriMethod, swiMethod);
    }

或者使用下面这种方式

//  确保当前类,两个方法都实现了,再去交换
    Method oriMethod = class_getInstanceMethod(self, @selector(method1));
       
    Method swiMethod = class_getInstanceMethod(self, @selector(method2));
       
    class_addMethod(self,
                    @selector(method1),
                       class_getMethodImplementation(self, @selector(method1)),
                       method_getTypeEncoding(oriMethod));

    class_addMethod(self,
                    @selector(method2),
                       class_getMethodImplementation(self, @selector(method2)),
                       method_getTypeEncoding(swiMethod));
   
    method_exchangeImplementations(class_getInstanceMethod(self, @selector(method1)), class_getInstanceMethod(self, @selector(method1)));

隐患

  • 只在 +load 中执行 swizzling 才是安全的。

  • 被 hook 的方法必须是当前类自身的方法,如果把继承来的 IMP copy 到自身上面会存在问题。父类的方法应该在调用的时候使用,而不是 swizzling 的时候 copy 到子类。

  • 被 Swizzled 的方法如果依赖 cmd ,hook 之后 cmd 发送了变化,就会有问题(一般你 hook 的是系统类,也不知道系统用没用 cmd 这个参数)。

  • 命名如果冲突导致之前 hook 的失效 或者是循环调用。

第一条和第四条说的是通常 MethodSwizzling 是在分类里面实现的, 而分类的 Method 是被Runtime 加载的时候追加到类的 MethodList ,如果不在 +load 中执行, Swizzling 一旦出现重名,那么 SEL 和 IMP 不匹配致 hook 的结果是循环调用。

第三条是一个不容易被发现的问题。
我们都知道 Objective-C Method 都会有两个隐含的参数 self,cmd,有的时候开发者在使用关联属性的适合可能懒得声明 (void *) 的 key,直接使用 cmd 变量 objc_setAssociatedObject(self, _cmd, xx, 0); 这会导致对当前IMP对 cmd 的依赖。

一旦此方法被 Swizzling,那么方法的 cmd 势必会发生变化,出现了 bug 之后想必一定找不到,

Copy父类的方法带来的隐患

上面的两种 Swizzling 方式,存在copy父类方法带来的隐患

当父类中存在一个 方法.

子类的分类 hook 父类方法时,会把父类实现 copy到子类.

这时父类的分类再去 hook 父类的 方法,会导致子类 Hook 不到父类的分类的实现

这时可以使用RSSwizzle

RSSwizzleInstanceMethod([Student class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // 调用之前的实现
                                                RSSWCallOriginal();
                                                // 自身的实现
                                                NSLog(@"Student + swizzle say hello sencod time");
                                            }), 0, NULL);

    RSSwizzleInstanceMethod([Person class],
                            @selector(sayHello),
                            RSSWReturnType(void),
                            RSSWArguments(),
                            RSSWReplacement(
                                            {
                                                // 调用之前的实现
                                                RSSWCallOriginal();
                                                // 自身的实现
                                                NSLog(@"Person + swizzle say hello");
                                            }), 0, NULL);

将宏展开

// 创建block
RSSwizzleImpFactoryBlock newImp = ^id(RSSwizzleInfo *swizzleInfo) {
        void (*originalImplementation_)(__unsafe_unretained id,SEL));
        SEL selector_ = @selector(sayHello);
        return ^void (__unsafe_unretained id self) {
            //  调用父类的实现
            ((__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)];;

其中RSSwizzleInfo的声明和 swizzleInstanceMethod:inClass:newImpFactory:newImp:mode:key:方法的实现

/*
其中
typedef id (^RSSwizzleImpFactoryBlock)(RSSwizzleInfo *swizzleInfo);
*/

@interface RSSwizzleInfo : NSObject
/**
返回swizzled方法的原始实现。
如果swizzled类实现了方法本身,那么它实际上是一个原始实现;如果没有实现方法,则是从父类获取的实现。
@注意:调用时,必须始终将返回的实现强制转换为相应的函数指针。
@返回指向swizzled方法的原始实现的函数指针。
 */
-(RSSwizzleOriginalIMP)getOriginalImplementation;

/// The selector of the swizzled method.
@property (nonatomic, readonly) SEL selector;
@end
*/

typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
    ///RSSwizzle总是做swizzing。
    RSSwizzleModeAlways = 0,
    /// 如果以前用同一个键 RSSwizzle 过同一个类,就不会RSSwizzle。
    RSSwizzleModeOncePerClass = 1,
    /// 如果同一个类或父类之前已经用同一个键进行了swizzle,就不会RSSwizzle。
 
    RSSwizzleModeOncePerClassAndSuperclasses = 2
};
+(BOOL)swizzleInstanceMethod:(SEL)selector
                     inClass:(Class)classToSwizzle
               newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock
                        mode:(RSSwizzleMode)mode
                         key:(const void *)key
{
        if (key){
            // 获得key对应的set
            NSSet *swizzledClasses = swizzledClassesForKey(key);
            if (mode == RSSwizzleModeOncePerClass) {
                // 是否用同一个key  RSSwizzle 过同一个类
                if ([swizzledClasses containsObject:classToSwizzle]){
                    return NO;
                }
            }else if (mode == RSSwizzleModeOncePerClassAndSuperclasses){
                for (Class currentClass = classToSwizzle;
                     nil != currentClass;
                     currentClass = class_getSuperclass(currentClass))
                {
                    if ([swizzledClasses containsObject:currentClass]) {
                        return NO;
                    }
                }
            }
        }
        
        swizzle(classToSwizzle, selector, factoryBlock);
        
        if (key){
            //  保存key
            [swizzledClassesForKey(key) addObject:classToSwizzle];
        }
    }
    
    return YES;
}

RSSwizzle核心代码其实只有一个函数,

// 删除无关的加锁,防御逻辑,简化理解。
static void swizzle(Class classToSwizzle,
                    SEL selector,
                    RSSwizzleImpFactoryBlock factoryBlock)
{
    Method method = class_getInstanceMethod(classToSwizzle, selector);
    
    __block IMP originalIMP = NULL;

    RSSWizzleImpProvider originalImpProvider = ^IMP{
        // 保存 class_replaceMethod 获得之前的实现,如果之前没有实现,
        IMP imp = originalIMP;
        // 如果imp不存在,则获取父类的方法的imp.
        if (NULL == imp){
            imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
        }
        return imp;
    };
    
    RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
    swizzleInfo.selector = selector;
    swizzleInfo.impProviderBlock = originalImpProvider;
    
    // 执行传进来的block, 获得 替换后的实现
    id newIMPBlock = factoryBlock(swizzleInfo);
    // 获得返回值类型
    const char *methodType = method_getTypeEncoding(method);
      
    // 创建实现  
    IMP newIMP = imp_implementationWithBlock(newIMPBlock);
    
    // 替换方法,并获得之前的实现
    originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
}

通过阅读源码可知, RSSwizzle 并没有去交换方法的imp,而是,保存旧的imp, 然后新的方法替换了旧的方法.

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

推荐阅读更多精彩内容