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. 演示代码

友谊感谢

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