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. 以身作则代码

雅感谢

最终谢谢 骑神
大佬修改我那次的文字描述。