基於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
相關文章
相關標籤/搜索