科技美学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. 以身作则代码

友谊感谢

终极谢谢 骑神
大佬修改我那么不行的文字描述。