iOS界的癌细胞-MethodSwizzling

原稿地址

缘何有这篇博文

不理解哪天最先iOS面试起先流行起来询问如何是 Runtime,于是 iOSer 一听
Runtime 总是就提起
MethodSwizzling,开口闭口就是黑科技。但实际只要读者注意过C语言的 Hook
原理其实会意识所谓的钩子都是框架或者语言的设计者预留给大家的工具,而不是何许黑科技,MethodSwizzling
其实只是一个大概而有趣的机制罢了。可是就是这样的体制,在普通中却总能成为万能药一般的被肆无忌惮的利用。

很多 iOS 项近期期架构设计的不够健全,中期可增加性差。于是 iOSer 想起了
MethodSwizzling 那一个武器,将品种中一个好端端的情势 hook
的满天飞,导致项目标质料变得难以�控制。曾经自己也爱在品种中滥用
MethodSwizzling,但在踩到坑从前接连无法觉察到这种不佳的做法会让项目陷入怎么样的险境。于是自己才精通学习某个机制要去浓厚的领悟机制的设计,而不是跟风滥用,带来不佳的后果。最终就有了那篇作品。

Hook的对象

在 iOS 平台大规模的 hook 的靶子一般有二种:

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

�对于 C/C+ +的 hook 常见的办法得以利用 facebook 的 fishhook
框架,具体原理可以参照深远精通Mac OS X & iOS 操作系统 这本书。
对于 Objective-C Methods 可能我们更熟知一点,本文也只谈谈这多少个。

最广泛的hook代码

深信不疑广大人拔取过
JRSwizzle
这么些库,或者是看过
http://nshipster.cn/method-swizzling/
的博文。
上述的代码简化如下。

+ (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情状颇为常见的气象下上述代码不会师世问题,不过场景复杂过后上边的代码会有为数不少安全隐患。

MethodSwizzling泛滥下的隐患

Github有一个�很硬朗的库
RSSwizzle(这也是本文推荐Swizzling的末段情势)
指出了上边代码带来的风险点。

  1. 只在 +load 中推行 swizzling 才是高枕无忧的。

  2. 被 hook 的不二法门必须是时下类自身的不二法门,假诺把后续来的 IMP copy
    到自家下面会存在问题。父类的方法应该在调用的时候利用,而不是
    swizzling 的时候 copy 到子类。

  3. 被 Swizzled 的法门假使借助与 cmd ,hook 之后 cmd
    发送了转移,就会有问题(一般你 hook 的是系统类,也不知情系统用没用
    cmd 这些参数)。

  4. 命名假使争辩导致前边 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
之后可能你一定找不到,等你找到之后心里一定会致敬那位 Swizzling
你的措施的开发者祖宗十八代安好的,再者假设您 Swizzling
的是系统的艺术恰好系统的不二法门内部用到了 cmd
\_(此处后背惊起一阵冷汗)。

Copy父类的办法带来的题材

下边的第二条才是大家最容易碰到的意况,并且是99%的开发者都不会小心到的题目。下面我们来做个试验

@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

下边的代码中有一个 Person 类实现了 sayHello 方法,有一个 Student
继承自 Person, 有一个Student 分类 Swizzling 了本来的� sayHello,
还有一个 Person 的分类也 Swizzling 了原先的 sayhello 方法。

当大家转变一个 Student 类的实例并且调用 sayHello
方法,我们愿意的出口如下:

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

只是出口有可能是这样的:

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

出现如此的气象是由于在 build Phasescompile Source
顺序子类分类在父类分类在此以前。

大家都知道在 Objective-C 的世界里父类的 +load
早于子类,不过并没有�限制父类的归类加载�会早于子类的归类的加载,实际上这有赖于编译的顺序。最后会按部就班编译的相继合并进
Mach-O �的固定 section 内。

下边会分析下怎么代码会现出如此的光景。

最开头的时候父类拥有和谐的 sayHello 方法,子类拥有分类添加的
s_sayHello 方法并且在 s_sayHello 方法内部调用了 sel 为 s_sayHello
方法。

不过子类的归类在拔取方面提到的 MethodSwizzling 的方法会导致�如下图的变型

出于调用了 class_addMethod 方法会导致重新生成一份新的Method添加到
Student 类下面 可是 sel 并没有爆发变化,IMP 依然指向父类唯一的不行
IMP。
此后互换了子类六个措施的 IMP 指针。于是方法引用变成了如下结构。
其间虚线指出的是情势的调用路径。

单单在 Swizzling
一回的时候并不曾什么问题,可是大家并不可以担保同事由于某种幕后的目标的又去
Swizzling 了父类,或者是我们引入的第三库做了这般的操作。

于是我们在 Person 的分类里面 Swizzling
的时候会造成方法社团爆发如下变化。

俺们的代码调用路径就会是下图这样,相信你已经领悟了前头的代码执行结果中缘何父类在子类之后
Swizzling 其实并没有对子类 hook 到。

这只是里面一种很宽泛的光景,造成的影响也只是 Hook
不到父类的派生类而已,�也不会招致局部严重的 Crash
等彰着现象,所以大部分开发者对此种行为是毫不知情的。

对于那种 Swizzling
格局的不确定性有一篇博文分析的更是完善玉令天下的博客Objective-C Method
Swizzling

换个姿态来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
使用的是宏作为代码包装的进口,并且由开发者自行保管情势的参数个数和参数类型的不错,所以使用起来也相比隐晦。
可能这也是他干吗如此可以不过 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核心代码其实唯有一个函数

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);
}

上述代码已经去除无关的加锁,防御逻辑,简化了解。

俺们可以看来 RS 的代码其实是布局了一个 Block
里面装着我们需要的实践的代码。

接下来再把我们的名字叫 originalImpProviderBloc
当做参数传递到大家的block里面,这之中富含了对即将被 Swizzling 的原始 IMP
的调用。

亟需专注的是使用 class_replaceMethod
的时候如若一个艺术来自父类,那么就给子类 add 一个形式, 并且把这个NewIMP 设置给她,然后再次回到的结果是NULL。

originalImpProviderBloc 里面我们注意到即便 imp
NULL的时候,是动态的得到父类的 Method 然后去执行。

俺们还用图来分析代码。

最起首 Swizzling 第一次的时候,由于子类不存在 sayHello
方法,再添加方法的时候由于重回的原始 IMP 是
NULL,所以对父类的调用是动态获取的,而不是经过以前的 sel 指针去调用。

假定大家再次对 Student Hook,由于 Student 已经有 sayHello 方法,本次replace 会重临原来 IMP 的指针, 然后新的 IMP 会执被填充到 Method
的指针指向。

有鉴于此我们的章程引用是一个链表形状的。

同理我们在 hook 父类的时候 父类的不二法门引用也是一个链表样式的。

深信不疑到了此地您早就清楚 RS 来 Swizzling 格局是:

假若是父类的主意那么就动态查找,如果是自身的法门就构造方法引用链。来担保多次
Swizzling 的安静,并且不会和外人的 Swizzling 争辨。

还要 RS 的落实由于不是分类的形式也不用约束开发者必须在 +load
方法调用才能确保安全,并且cmd 也不会发生变化。

其他Hook方式

骨子里出名的 Hook 库还有一个叫
Aspect
他选拔的措施是把装有的艺术调用指向 _objc_msgForward
然后自行实现音信转发的步骤,在里头自行处理参数列表和再次来到值,通过
NSInvocation 去动态调用。

境内出名的热修复库 JSPatch 就是借鉴这种格局来促成热修复的。

唯独地点的库要求必须是最后执行的担保 Hook 的功成名就。 而且他不包容其他 Hook
模式,所以技术选型的时候要三思。

�啥时候需要Swizzling

我记念第一次学习 AO P概念的时候是这时在学习 javaWeb 的时候 Serverlet
里面的
FilterChain,开发者可以兑现各样各类的过滤器然后在过滤器中插入log,
总计, 缓存等无关主业务逻辑的效果行性代码, 出名的框架 Struts2
就是这样实现的。

iOS 中由于 Swizzling 的 API
的简单易用性导致开发者肆意滥用,影响了品种的安居。
当我们想要 Swizzling
的时候应该考虑下大家能不可能采用美观的代码和架构设计来兑现,或者是浓密语言的特征来兑现。

一个应用言语特征的例证

我们都了解在iOS8下的�操作系统中通报主旨会所有一个 __unsafe_unretained
的观察者指针。倘若�观望者在 �dealloc
的时候忘记从公告中央中移除,之后若是接触相关的关照就会促成 Crash。

自我在规划防 Crash 工具
XXShield
的时候最初是 Hook NSObjec 的 dealloc
方法,在里头做相应的移除观察者操作。后来一位真大佬提议这是一个可怜不明智的操作,因为
dealloc
会影响全局的实例的刑释解教,开发者并不可以确保代码质地十分有保持,一旦出现问题将会挑起上上下下
APP 运行期间广大崩溃或特别行为。

下边我们先来看下 ObjCRuntime
源码关于一个对象释放时要做的事体,代码约在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;
}

上边的逻辑中有目共睹了写明了一个目的在假释的时候初了调用 dealloc
方法,还亟需断开实例上绑定的观看对象,
那么大家可以在充裕观望者的时候给观看者动态的绑定一个涉及对象,然后关联对象可以反向持有观望者,然后在提到对象释放的时候去移除观望者,由于无法促成循环引用所以只可以采纳
__weak 或者 __unsafe_unretained 的指针, 实验得知 __weak 的指针在
dealloc 往日就曾经被清空, 所以我们不得不利用 __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);

}

内需注意的是在加上关联者的时候势必要将代码包含在一个自定义的
AutoreleasePool 内。

俺们都晓得在 Objective-C 的世界里一个目的假诺是 Autorelease 的
那么那多少个目的在目前艺术栈为止后才会延时释放,在 ARC 环境下�,一般一个
Autorelease 的目的会被放在一个系统提供的 AutoreleasePool
里面,然后AutoReleasePool drain
的时候再去放活内部装有的靶子,平常境况下命令行程序是没有问题的,不过在iOS的条件中
AutoReleasePool是在 Runloop
控制下在清闲时间展开放飞的,这样可以荣升用户体验,防止造成卡顿,然则在我们这种光景中会有题目,我们严刻看重了观看者�调用
dealloc 的时候关系对象也会去 dealloc,假若系统的 AutoReleasePool
出现了延时自由,会造成当前目的被回收之后
过段时间关联对象才会放出,这时候前文使用的 __unsafe_unretained
访问的�就是非法地址。

咱俩在累加提到对象的时候增长一个自定义的 AutoreleasePool
保证了对涉嫌对象引用的单一性,保证了俺们依靠的放飞顺序是正确的。从而不利的移除观望者。

参考

  1. JRSwizzle
  2. RSSwizzle
  3. Aspect
  4. 玉令天下的博客Objective-C Method
    Swizzling
  5. 以身作则代码

友谊感谢

最后谢谢 骑神
大佬修改我这不行的文字描述。