mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 20:00:40 +00:00
312 lines
11 KiB
Markdown
312 lines
11 KiB
Markdown
# 不一样的动态化能力
|
||
|
||
> 对于热修复,对于大多数公司来说都是可望而不可及的技术手段。热修复对于线上问题是杀手锏级别项目。Android 热修复方案很多,典型的属微信的 `Tinker` 莫属,而苹果公司对于安全的要求非常高,所以一些动态调用的能力都会被封杀,这篇文章主要研究下 iOS 端的热修复技术方案。
|
||
|
||
|
||
## 热修复方案
|
||
|
||
- 将下发的原生代码,通过自己实现的代码解析引擎,将代码转换为AST树,然后存储在相关的模型里面,在通过一个上下文注入到runtime里面,当runtime回调到当前函数的时候,上下文从存储的相关模型取出各个参数,然后放到当前堆栈里面去执行相关的逻辑,执行问之后,在返回之前调用的地方,这里跟腾讯的OCS有点像.
|
||
|
||
- JSPatch:加加密,多混淆,关键词替换。(其实重要封杀的是respondsToSelector:, performSelector:, method_exchangeImplementations() 这些函数,然后现在aop、hook、jspatch 都是离不开这些函数的。解决方案将 动态能力的 API 替换名字:而是本地已经处理好,写到代码的静态变量里面,执行的时候去按照相应的解密方法去解密,然后得到 respondsToSelector:, 再去执行)
|
||
|
||
- 几大app中的方案都是自己研发的,不过大同小异,有比较多的是从编译器层面出发,直接把写的代码编译好,然后自己再写解析器解析执行
|
||
|
||
- lua kit:https://github.com/alibaba/LuaViewSDK;https://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) 可以点此查看链接.
|
||
|
||
|
||
## 未完,待续 |