Files
knowledge-kit/Chapter1 - iOS/1.70.md
2020-02-25 17:46:51 +08:00

312 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 不一样的动态化能力
> 对于热修复对于大多数公司来说都是可望而不可及的技术手段。热修复对于线上问题是杀手锏级别项目。Android 热修复方案很多,典型的属微信的 `Tinker` 莫属,而苹果公司对于安全的要求非常高,所以一些动态调用的能力都会被封杀,这篇文章主要研究下 iOS 端的热修复技术方案。
## 热修复方案
- 将下发的原生代码通过自己实现的代码解析引擎将代码转换为AST树然后存储在相关的模型里面在通过一个上下文注入到runtime里面当runtime回调到当前函数的时候上下文从存储的相关模型取出各个参数然后放到当前堆栈里面去执行相关的逻辑执行问之后在返回之前调用的地方这里跟腾讯的OCS有点像.
- JSPatch加加密多混淆关键词替换。其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数然后现在aop、hook、jspatch 都是离不开这些函数的。解决方案将 动态能力的 API 替换名字:而是本地已经处理好,写到代码的静态变量里面,执行的时候去按照相应的解密方法去解密,然后得到 respondsToSelector:, 再去执行)
- 几大app中的方案都是自己研发的不过大同小异有比较多的是从编译器层面出发直接把写的代码编译好然后自己再写解析器解析执行
- lua kithttps://github.com/alibaba/LuaViewSDKhttps://alibaba.github.io/LuaViewSDK/guide.html
其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数然后现在aop、hook、jspatch 都是离不开这些函数的。
## 思路
`JavaScriptCore` 是苹果给开发者操作 Javascript 的一个库,因此使用 JavaScriptCore 基本不存在问题。另外做热修复的基本思路就是在某个类执行某个类方法、某个类的对象执行某个对象方法的时候做一些处理。所以这里涉及到几个因素:类、类对象、类方法、实例方法、方法执行前、方法执行后、方法完全替换。 Objective-C 有运行时特性,所以可以很容易实现上面的几个点,但是直接使用 Runtime 会比较麻烦,这时候就不得不提一下一个面向切面编程的开源库-[Aspects](https://github.com/steipete/Aspects)。
所以剩下来的事情就是将 Aspects 的几个能力暴露给 JavascriptCore 对象。然后 App 在启动的时候去调用热修复接口,拿到修复的字符串,然后给 JavascriptCore 对象,然后 Javascript 对象去执行拿到的热修复的字符串,这样子整个流程下来,当我们去进入某个页面或者调用某个功能的时候,发现 A 类的 methodA 方法有问题,我们下发了热修复代码,就可以在 methodA 的前后加入逻辑,甚至是完全替换。
## 代码实现
<details>
<summary>FixManager</summary>
```Objective-C
#import <Foundation/Foundation.h>
#import "Aspects.h"
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
NS_ASSUME_NONNULL_BEGIN
@interface FixManager : NSObject
+ (FixManager *)sharedInstance;
+ (void)fixIt;
+ (void)evalString:(NSString *)javascriptString;
@end
NS_ASSUME_NONNULL_END
#import "FixManager.h"
@implementation FixManager
+ (FixManager *)sharedInstance
{
static FixManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] init];
});
return manager;
}
+ (JSContext *)context
{
static JSContext *_context;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_context = [[JSContext alloc] init];
[_context setExceptionHandler:^(JSContext *context, JSValue *exception) {
NSLog(@"Ooops, %@", exception);
}];
});
return _context;
}
+ (void)fixIt
{
[self context][@"fixInstanceMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl){
[self _fixWithMethod:NO
aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixInstanceMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:NO aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixInstanceMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:NO aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"fixClassMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
[self _fixWithMethod:YES aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};
[self context][@"runClassWithNoParamter"] = ^id(NSString *className, NSString *selectorName) {
return [self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runClassWith1Paramter"] = ^id(NSString *className, NSString *selectorName, id obj1) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runClassWith2Paramters"] = ^id(NSString *className, NSString *selectorName, id obj1, id obj2) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidClassWithNoParamter"] = ^(NSString *className, NSString *selectorName) {
[self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidClassWith1Paramter"] = ^(NSString *className, NSString *selectorName, id obj1) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidClassWith2Paramters"] = ^(NSString *className, NSString *selectorName, id obj1, id obj2) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInstanceWithNoParamter"] = ^id(id instance, NSString *selectorName) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runInstanceWith1Paramter"] = ^id(id instance, NSString *selectorName, id obj1) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runInstanceWith2Paramters"] = ^id(id instance, NSString *selectorName, id obj1, id obj2) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidInstanceWithNoParamter"] = ^(id instance, NSString *selectorName) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidInstanceWith1Paramter"] = ^(id instance, NSString *selectorName, id obj1) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidInstanceWith2Paramters"] = ^(id instance, NSString *selectorName, id obj1, id obj2) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInvocation"] = ^(NSInvocation *invocation) {
[invocation invoke];
};
// helper将 JS 的 console.log 用 Native Log 替换
[[self context] evaluateScript:@"var console = {}"];
[self context][@"console"][@"log"] = ^(id message) {
NSLog(@"Javascript log: %@",message);
};
}
+ (void)_fixWithMethod:(BOOL)isClassMethod aspectionOptions:(AspectOptions)option instanceName:(NSString *)instanceName selectorName:(NSString *)selectorName fixImpl:(JSValue *)fixImpl
{
Class klass = NSClassFromString(instanceName);
if (isClassMethod) {
klass = object_getClass(klass);
}
SEL sel = NSSelectorFromString(selectorName);
[klass aspect_hookSelector:sel withOptions:option usingBlock:^(id<AspectInfo> aspectInfo){
[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
} error:nil];
}
+ (id)_runClassWithClassName:(NSString *)className selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
{
Class klass = NSClassFromString(className);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [klass performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (id)_runInstanceWithInstance:(id)instance selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [instance performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (void)evalString:(NSString *)javascriptString
{
[[self context] evaluateScript:javascriptString];
}
@end
```
</details>
<details>
<summary>BugProtector</summary>
```Objective-C
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface BugProtector : NSObject
+ (instancetype)sharedInstance;
+ (void)getFixScript:(NSString *)scriptText;
@end
NS_ASSUME_NONNULL_END
#import "BugProtector.h"
#import "FixManager.h"
@interface BugProtector ()
@end
@implementation BugProtector
static BugProtector *_sharedInstance = nil;
#pragma mark - life cycle
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//because has rewrited allocWithZone use NULL avoid endless loop lol.
_sharedInstance = [[super allocWithZone:NULL] init];
[FixManager fixIt];
});
return _sharedInstance;
}
+ (FixManager *)fixManager
{
static FixManager *_manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_manager = [FixManager sharedInstance];
});
return _manager;
}
+ (id)allocWithZone:(struct _NSZone *)zone
{
return [BugProtector sharedInstance];
}
+ (instancetype)alloc
{
return [BugProtector sharedInstance];
}
- (id)copy
{
return self;
}
- (id)mutableCopy
{
return self;
}
- (id)copyWithZone:(struct _NSZone *)zone
{
return self;
}
#ifdef DEBUG
- (void)dealloc
{
NSLog(@"%s",__func__);
}
#endif
#pragma mark - public Method
+ (void)getFixScript:(NSString *)scriptText
{
[FixManager evalString:scriptText];
}
@end
```
</details>
完整的 [Demo](https://github.com/FantasticLBP/BlogDemos/tree/master/HotFix) 可以点此查看链接.
## 未完,待续