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

原文地址

背景

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

目标

JSPatch 能够任意替换和新增方法,甚至能够用来开发新模块。可是若是纯粹用来修复线上bug的话,咱们并不须要如此强大的功能。热修复只须要具有如下几点功能足以:html

  1. 方法替换为空实现
  2. 方法参数修改
  3. 方法返回值修改
  4. 方法调用先后插入自定义代码
    • 支持任意 OC 方法调用
    • 支持赋值语句
    • 支持 if 语句:==、!=、>、>=、<、<=、||、&&
    • 支持 super 调用
    • 支持自定义局部变量
    • 支持 return 语句

JPAspect

JPAspect 一款轻量级、无侵入和无审核风险的 iOS 热修复框架。JPAspect 经过下发指定规则的 json 便可轻松实现线上 Bug 修复。JPAspect 已实现上述全部功能,具体实现请参考代码。git

原理

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

@end
复制代码
  1. 常规调用
  2. 反射调用
  3. objc_msgSend
  4. C 函数调用
  5. NSInvocation 调用
// 常规调用
People *people = [[People alloc] init];
[people helloWorld];

// 反射调用 
Class cls = NSClassFromString(@"People");
id obj = [[cls alloc] init];
[obj performSelector:NSSelectorFromString(@"helloWorld")];

// objc_msgSend
((void(*)(id, SEL))objc_msgSend)(people, sel_registerName("helloWorld"));

// C 函数调用
Method initMethod = class_getInstanceMethod([People class], @selector(helloWorld));
IMP imp = method_getImplementation(initMethod);
((void (*) (id, SEL))imp)(people, @selector(helloWorld));

// NSInvocation 调用
NSMethodSignature *sig = [[People class] instanceMethodSignatureForSelector:sel_registerName("helloWorld")];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = people;
invocation.selector = sel_registerName("helloWorld");
[invocation invoke];
复制代码

Aspects 原理分析

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

1、简介

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

2、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);

复制代码

3、Invoke 流程

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

4、Aspects 使用遇到的问题

  • 使用了自旋锁,存在优先级反转问题,使用 pthread_mutex_lock 代替便可
  • 特殊 struct 判断逻辑不够全面,例如:NSRange, NSPoint等 在 32 位架构下有问题,须要自行兼容
#if defined(__LP64__) && __LP64__
    if (valueSize == 16) {
        methodReturnsStructValue = NO;
    }
#endif
复制代码
  • 类方法没法直接 hook, 不过能够 hook 其 Meta class 元类方式进行解决
object_getClass(targetCls)
复制代码
  • 没法同时 hook 一个类的实例方法和类方法,缘由是使用了相同的 swizzledClasse key, 解决以下:
static Class aspect_swizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = [NSString stringWithFormat:@"%@_%p", NSStringFromClass(klass), klass];

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        if (![swizzledClasses containsObject:className]) {
            aspect_swizzleForwardInvocation(klass);
            [swizzledClasses addObject:className];
        }
    });
    return klass;
}

static void aspect_undoSwizzleClassInPlace(Class klass) {
    NSCParameterAssert(klass);
    NSString *className = [NSString stringWithFormat:@"%@_%p", NSStringFromClass(klass), klass];

    _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
        if ([swizzledClasses containsObject:className]) {
            aspect_undoSwizzleForwardInvocation(klass);
            [swizzledClasses removeObject:className];
        }
    });
}
复制代码

开发中遇到的坑

1、Illegal Instruction Crash

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

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    
复制代码

崩溃缘由分析:app

  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 方法,致使对象提早释放。若是此时再对该对象发送消息则会发生野指针崩溃

解决办法:框架

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

__unsafe_unretained id value = nil;
复制代码

二、经过 __bridge 转换让 value 持有返回对象,显示赋值函数

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

2、Memory leak

背景:

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

内存泄漏缘由分析:

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

二、若是方法有返回值的话,ARC会在 return 后自动插入 autorelease,因此通常的常规返回是没有问题的。

三、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;    

复制代码

解决办法:

  1. 把返回对象的内存管理权移交出来,让外部对象管理其内存。因为是显示赋值,ARC机制生效。
  2. 采用常规方法调用代替 NSInvocation
id target = [NSObject class];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = target;
invocation.selector = sel_registerName([@"alloc" UTF8String]);
[invocation invoke];

id resultObj = nil,
void *result;
[invocation getReturnValue:&result];

// 方法1 
if ([selName isEqualToString:@"alloc"] ||
    [selName isEqualToString:@"new"] ||
    [selName isEqualToString:@"copy"] ||
    [selName isEqualToString:@"mutableCopy"]) {
    resultObj = (__bridge_transfer id)result;
} else {
    resultObj = (__bridge id)result;
}

// 方法2
if ([selName isEqualToString:@"alloc"]) {
    resultObj = [[target class] alloc];
} else if ([selName isEqualToString:@"new"]) {
	resultObj = [[target class] new];
} else if ([selName isEqualToString:@"copy"]) {
	resultObj = [target copy];
} else if ([selName isEqualToString:@"mutableCopy"]) {
	resultObj = [target mutableCopy];
} else {
    expectObj = (__bridge id)result;
}

复制代码

功能实现简介

1、方法替换为空实现

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

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

2、判断方法参数

核心点就是经过 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];
复制代码

3、替换方法参数

这个也是经过 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];
复制代码

4、更改方法返回值

[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];
复制代码

5、方法调用先后插入自定义代码

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

[NSClassFromString(@"UIViewController") aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo){       
        
    // viewDidLoad 执行前插入 self.view.backgroundColor = [UIColor redColor];
    target = ((id (*)(id, SEL))objc_msgSend)(aspectInfo.originalInvocation.target, NSClassFromString(@"view"));
    SEL selector = sel_registerName([@"setBackgroundColor:" UTF8String]);
    NSMethodSignature *signature = [[target class] instanceMethodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = target;
    invocation.selector = selector;
    id value = ((id(*)(id, SEL))objc_msgSend)([UIColor class], sel_registerName("redColor"));
    [invocation setArgument:&value atIndex:2];
    [invocation invoke];
    
    [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
相关文章
相关标签/搜索