美文网首页iOS开发资料收集区iOS Developer
基于Aspects框架的iOS热修复方案

基于Aspects框架的iOS热修复方案

作者: BytePorter | 来源:发表于2019-01-09 00:32 被阅读439次

    背景

    1. JSPatch 无法审核,就算进行深度的代码混淆依然无法逃脱苹果审核机制
    2. App 审核加快,但是依然无法很好的控制线上 Bug 的影响范围
    3. 目前未发现有其他可替代方案,只能另寻他径

    目标

    JSPatch 可以任意替换和新增方法,甚至可以用来开发新模块。但是如果纯粹用来修复线上bug的话,我们并不需要如此强大的功能。热修复只需要具备以下5点功能足以:

    1. 方法替换为空实现
    2. 判断方法参数
    3. 替换方法参数
    4. 更改方法返回值
    5. 方法调用前后插入自定义代码

    原理

    Runtime 术语

    1. SEL
    2. IMP
    3. Method
    4. NSMethodSignature
    5. NSInvocation
    6. void _objc_msgForward(void /* id receiver, SEL sel, ... */ ) 
    7. id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    8. Objective-C type encodings
    
    

    Runtime 基本操作

    • Class 反射创建
    // 1
    NSClassFromString(@"NSObject");
    
    // 2 
    objc_getClass("NSObject");
    
    • SEL 反射创建
    // 1
    @selector(init);
    
    // 2
    sel_registerName("init");
    
    // 3
    NSSelectorFromString(@"init");
    
    • 方法替换
    static void cc_forwardInvocation(id slf, SEL sel, NSInvocation *invocation) 
    {
        // do what you want to do
    }
    
    class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)cc_forwardInvocation, "v@:@");
    
    • 方法新增
    Class tClass = NSClassFromString(@"UIViewController");
    SEL selector = NSSelectorFromString(@"viewDidLoad");
    
    Method targetMethod = class_getInstanceMethod(tClass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    
    SEL aliasSelector = NSSelectorFromString([@"cc" stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
    class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
    
    • 新类创建
    Class cls = objc_allocateClassPair([NSObject class], “CCObject”, 0);
    objc_registerClassPair(cls);
    
    • 消息转发
    // 1. 正常转发
    + (BOOL)resolveClassMethod:(SEL)sel 
    + (BOOL)resolveInstanceMethod:(SEL)sel 
    
    - (id)forwardingTargetForSelector:(SEL)aSelector
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    - (void)forwardInvocation:(NSInvocation *)anInvocation
    
    // 2. 自定义转发
    void _objc_msgForward(void /* id receiver, SEL sel, ... */ ) 
    

    Method Invoke 的几种方式

    @interface People : NSObject
    
    - (void)helloWord;
    
    @end
    
    1. 常规调用
    2. 反射调用
    3. objc_msgSend
    4. C 函数调用
    5. NSInvocation 调用
    // 常规调用
    People *people = [[People alloc] init];
    [people helloWord];
    
    // 反射调用    
    Class cls = NSClassFromString(@"People");
    id obj = [[cls alloc] init];
    [obj performSelector:NSSelectorFromString(@"helloWord")];
    
    // objc_msgSend
    ((void(*)(id, SEL))objc_msgSend)(people, sel_registerName("helloWord"));
    
    // C 函数调用
    Method initMethod = class_getInstanceMethod([People class], @selector(helloWord));
    IMP imp = method_getImplementation(initMethod);
    ((void (*) (id, SEL))imp)(people, @selector(helloWord));
    
    // NSInvocation 调用
    NSMethodSignature *sig = [[People class] instanceMethodSignatureForSelector:sel_registerName("helloWord")];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    invocation.target = people;
    invocation.selector = sel_registerName("helloWord");
    [invocation invoke];
    

    Aspects 原理分析:https://github.com/steipete/Aspects

    新版热修复是基于 Aspects 框架开发的,无审核风险,已上线。Aspects 和 JSPatch 的都是基于消息转发实现的。

    一、简介

    • AspectsContainer:Tracks all aspects for an object/class
    • AspectIdentifier:Tracks a single aspect

    二、Hook 流程

    1. 检查 selector 是否可以替换,里面涉及一些黑名单等判断
    2. 获取 AspectsContainer,如果为空则创建并绑定目标类
    3. 创建 AspectIdentifier,引用自定义实现(block)和 AspectOptions 等信息
    4. 将目标类 forwardInvocation: 方法替换为自定义方法
    5. 目标类新增一个带有aspects_前缀的方法,新方法(aliasSelector)实现跟目标方法相同
    6. 将目标方法实现替换为 _objc_msgForward
    // 将目标类 **forwardInvocation:** 方法替换为自定义方法
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    
    // 目标类新增一个带有` aspects_`前缀的方法,新方法(aliasSelector)实现跟目标方法相同
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    SEL aliasSelector = NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
    class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
    
    // 将目标方法实现替换为 `_objc_msgForward`
    class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    
    

    二、Invoke 流程

    1. 调用目标方法进入消息转发流程
    2. 调用自定义 __ASPECTS_ARE_BEING_CALLED__ 方法
    3. 获取对应 invocation,将 invocation.selector 设置为 aliasSelector
    4. 通过 aliasSelector 获取对应 AspectsContainer
    5. 根据 AspectOptions 调用用户自定实现(目标方法调用前/后/替换)

    开发中遇到的坑

    一、Illegal Instruction Crash

    -forwardInvocation: 里的 NSInvocation 对象取参数值时,若参数值是id类型,一般会这样取:

    id value = nil;
    [invocation getArgument:&value atIndex:2];
    

    但是这种写法存在 crash 风险。例如 Hook NSMutableArray 的 insertObject:atIndex: 方法.你会发现在有些系统调用会出现 EXC_BAD_INSTRUCTION 崩溃

    [NSClassFromString(@"__NSArrayM") aspect_hookSelector:@selector(insertObject:atIndex:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
            NSLog(@"insertObject:atIndex: hook");
            
            id value = nil;
            [info.originalInvocation getArgument:&value atIndex:2];
            if (value) {
                [info.originalInvocation invoke];
            }
        } error:NULL];
    

    开启 Zombie objects 下的异常打印

    -[UITraitCollection retain]: message sent to deallocated instance 0x6000007cde00    
    

    崩溃原因分析:

    1. NSInvocation 不会引用参数,详情可以看官方文档(This class does not retain the arguments for the contained invocation by default)
    2. ARC 在隐式赋值不会自动插入 retain 语句
    3. ARC 下 id value 相当于 __strong id vaule,所以在退出作用域时会自动插入 release 语句。
    4. 综上123可以得出:参数对象会多调用一次 release 方法,导致对象提前释放。如果此时再对该对象发送消息则会发生野指针崩溃

    解决办法:

    1、将 value 变成 __unsafe_unretained__weak,让 ARC 在它退出作用域时不插入 release 语句

    __unsafe_unretained id value = nil;
    

    2、通过 __bridge 转换让 value 持有返回对象,显示赋值

    id value = nil;
    void *result;
    [invocation getArgument:&result atIndex:2];
    value = (__bridge id)result;
    

    二、Memory leak

    背景:

    因为要支持参数替换,所以要从 -forwardInvocation: 里的 NSInvocation 对象取返回值,然后替换为自定义的参数。一般生成一个对象都会调用 alloc 方法,然后再初始化

    内存泄漏原因分析:

    1、根据内存管理规则可知,当调用 alloc / new / copy / mutableCopy 方法的返回对象的 retainCount = 1。

    2、如果方法有返回值的话,ARC会在 return 后自动插入 autorelease,所以一般的常规返回是没有问题的。

    3、ARC 对隐式赋值不会自动插入 autorelease,所以少了一次 release,从而导致内存泄漏。

    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = [NSObject class];
    invocation.selector = sel_registerName([@"alloc" UTF8String]);
    
    [invocation invoke];
    
    id returnValue = nil;
    [invocation getReturnValue:&returnValue];
    
    return returnValue;    
    
    

    解决办法: 把返回对象的内存管理权移交出来,让外部对象管理其内存。由于是显示赋值,ARC机制生效。

    id expectObj = nil,
    void *result;
    [invocation getReturnValue:&result];
        
    if ([selName isEqualToString:@"alloc"] ||
        [selName isEqualToString:@"new"] ||
        [selName isEqualToString:@"copy"] ||
        [selName isEqualToString:@"mutableCopy"]) {
        expectObj = (__bridge_transfer id)result;
    } else {
        expectObj = (__bridge id)result;
    }
    

    功能实现简介

    一、方法替换为空实现

    这个功能其实很容易实现,直接替换即可

    [NSClassFromString(@"UIViewController")  aspect_hookSelector:@selector(viewDidLoad:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
        // 空实现
    } error:NULL];
    

    二、判断方法参数

    核心点就是通过 Aspect 获取目标方法 Invocation ,然后对 Invocation 的参数进行对比,如果符合期望值则继续之前原方法,例如插入的对象是否为 nil,如果为 nil 则放弃调用原方法,相当于执行了一个空方法。这个可以扩展为基础变量判断,例如数组越界判断。

    [NSClassFromString(@"__NSArrayM") aspect_hookSelector:@selector(insertObject:atIndex:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
            
        // 当 value = nil,结束当前方法调用
        __unsafe_unretained id value = nil;
        [info.originalInvocation getArgument:&value atIndex:2];
        if (value) {
            [info.originalInvocation invoke];
        }
    } error:NULL];
    

    三、替换方法参数

    这个也是通过 Invocation 去修改方法里面的参数,然后再调用,具体实现如下

    [NSClassFromString(@"__NSArrayM") aspect_hookSelector:@selector(insertObject:atIndex:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
            
        // 不管外面怎么调用,每次 atIndex = 0
        NSUInteger value = 0;
        [info.originalInvocation setArgument:& value atIndex:3];
        [info.originalInvocation invoke];
    } error:NULL];
    

    四、更改方法返回值

    [NSClassFromString(@"__NSArrayM") aspect_hookSelector:@selector(objectAtIndex:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info){
            
        // 不管外面怎么调用,每次都返回 nil
        [info.originalInvocation invoke];
        id expectValue = nil;
        [info.originalInvocation setReturnValue:&expectValue];
    } error:NULL];
    

    五、方法调用前后插入自定义代码

    这个实现的起来稍微复杂一点,因为要实现方法前后插入方法,所以必须要构建消息发送对象和方法参数。例如在 UIViewControllerviewDidLoad 方法前设置其背景颜色为红色。首先需要获取 viewDidLoad 方法的 Invocation,然后通过 Invocation.target 获取到控制器对象 self,获取到 self 之后调用 objc_msgSend 方法获取 view,到这里我们已经获取到消息发送对象,然后我们用 sel_registerName 获取 setBackgroundColor: 方法的 SEL。通过 SEL 获取到函数签名 NSMethodSignature,同过函数签名去获取 setBackgroundColor: 的 Invocation,最后通过设置 Invocation 的参数为红色,然后调用 Invocation 的 invoke方法就将背景色改为 redColor。到此相信已经了解其核心原理了,我们只需要在此基础上再扩展,那么足以应付线上的 90% 以上的问题了。下面是具体实现代码。

    // viewDidLoad 执行前插入 self.view.backgroundColor = [UIColor redColor];
    [NSClassFromString(@"UIViewController") aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo){       
        // 获取控制器 view 对象
        target = ((id (*)(id, SEL))objc_msgSend)(aspectInfo.originalInvocation.target, NSClassFromString(@"view"));
        // 获取 setBackgroundColor:SEL
        SEL selector = sel_registerName([@"setBackgroundColor:" UTF8String]);
        // 获取函数签名
        NSMethodSignature *signature = [[target class] instanceMethodSignatureForSelector:selector];
        // 通过函数签名获取 invocation
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = target;
        invocation.selector = selector;
        // 构造  redColor 对象
        id value = ((id(*)(id, SEL))objc_msgSend)([UIColor class], sel_registerName("redColor"));
        // 设置 invocation 参数为 redColor
        [invocation setArgument:&value atIndex:2];
        // 调用 invoke 方法,设置背景颜色
        [invocation invoke];
        // 调用控制器原方法 viewDidLoad
        [info.originalInvocation invoke];
     } error:NULL];
    

    参考文献

    1. Objective-C Runtime Programming Guide
    2. NSInvocation returns value but makes app crash with EXC_BAD_ACCESS
    3. JSPatch 实现原理详解
    4. objc_msgSend_stret
    5. objc_msgSend() Tour Part 1: The Road Map
    6. -rac_signalForSelector: may fail for struct returns

    相关文章

      网友评论

        本文标题:基于Aspects框架的iOS热修复方案

        本文链接:https://www.haomeiwen.com/subject/vnwirqtx.html