支撑的才能:
1.支撑在原办法完成之前、后刺进新完成,或许替换
2.支撑修正原办法的参数、返回值(支撑 block 作为参数的办法)
3.支撑给已有类增加新的办法和完成(开发 UI 以及需求逻辑等)
4.支撑 GCD,UIView.animation 等 block 办法调用(但需求提前预埋这些办法)
1. 案例效果
首先介绍一个热修正的比如,然后再去看具体的完成原理,然后再去一层层的剥开那些并不奥秘的面纱。
例1:修正 crash 的办法, 如在异步线程调用 UI 的改写,导致的 crash,可经过下发脚本对该办法进行完成的替换,使其在主线程履行。
效果:

JS 代码:
RestoreMethod('ViewController','testMethod',0,1,function(instance,invocation,arg){
runInvocation_dispatch_async_main(function(){
var self = instance;
var view = runInstanceMethod(self,'view')
var redColor = runClassMethod('UIColor','redColor')
runInstanceMethod(view,'setBackgroundColor:',redColor)
})
})
例2:可经过脚本下发一些简略 UI 需求,如在控制器的 viewDidLoad 办法中创立一个 UIButton,并完成点击事情
效果:

JS 代码:
RestoreMethod('ViewController','viewDidLoad',0, 2,function(instance,invocation,arg){
var self = instance;
var view = runInstanceMethod(self,'view')
var color = HexColor('#508CEE')
runInstanceMethod(view,'setBackgroundColor:',color)
var redColor = runClassMethod('UIColor','redColor')
var btn = runClassMethod('UIButton','new')
runInstanceMethod(btn,'setFrameX:y:width:height:',new Array(50,200,300,60))
runInstanceMethod(btn,'setBackgroundColor:',redColor)
var layer = runInstanceMethod(btn,'layer')
runInstanceMethod(layer,'setCornerRadius:',10)
runInstanceMethod(layer, 'setMasksToBounds:', 1)
runInstanceMethod(view,'addSubview:',btn)
runInstanceMethod(btn,'setTitle:forState:',new Array('This is a Btn',0))
var yellowColor = runClassMethod('UIColor','yellowColor')
runInstanceMethod(btn,'setTitleColor:forState:',yellowColor)
runInstanceMethod(btn,'addTouchupInsideSelector:target:',new Array('fixMethod',instance))
})
RestoreMethod('ViewController','fixMethod',0, 1,function(instance,invocation,arg){
runLog('新增 button 的点击事情')
})
2.技术布景介绍
2.1 JavaScriptCore
JavaScriptCore 是 WebKit 默许内嵌的 JS 引擎(下面简称 JSCore),iOS7 之后苹果对 WebKit 中的 JSCore 进行了 Objective-C 的封装。该结构给 iOS 开发者供给了调用 JS 的才能,可完成 OC 和 JS 代码之间相互调用。
本文并不对 JSCore 结构打开介绍,只是简略介绍一下本热修正功用主要用到的两个中心类:JSContext 和 JSValue,然后能快速理解热修功用底层中心原理。
JSContext
JSContext 是咱们再实际运用 JSCore时,用到最多的概念。
JSContext 上下文方针能够理解为是 JS 的运转环境,同一个JSVirtualMachine方针能够关联多个JSContext方针,一个 JSContext 表明了一次 JS 的履行环境。咱们能够经过创立一个 JSContext 去调用JS 脚本,访问一些 JS 界说的值和函数,一起也供给了让 JS 访问 Native 方针,办法的接口。
JSValue
JavaScript 和 Objective-C虽然都是面向方针言语,但其完成机制彻底不同,OC 是基于类的,JS 是基于原型的,而且他们的数据类型间也存在很大的差异。因此若要在 Native 和JS间无障碍的进行数据的传递,就需求一个中心方针做桥接,这个方针便是JSValue。JSValue 是不能独立存在,它必须存在与某一个 JSContext 中。
如何运用呢?下面举几个简略的列子
OC 中调用 JS 脚本代码
- (void)OC_Call_JS {
// 创立一个JSContext方针
JSContext *jsContext = [[JSContext alloc] init];
// 1.履行JS代码 核算js变量a和b之和
[jsContext evaluateScript:@"var a = 1; var b = 2;"];
// 返回值是 JSValue 类型的方针
JSValue *result = [jsContext evaluateScript:@"a + b"];
// 将 JSValue 类型转换成 OC 中的类型
NSInteger sum = [result toInt32];
NSLog(@"%ld", (long)sum); // 3
// 2.界说办法并调用
[jsContext evaluateScript:@"var addFunc = function(a, b) { return a + b }"];
JSValue *result = [jsContext evaluateScript:@"addFunc(a, b)"];
NSLog(@"%@", result.toNumber); // 3
// 3.也能够OC传参
JSValue *addFunc = jsContext[@"addFunc"];
// 在 OC 侧能够经过 callWithArguments:办法调用 js 的办法完成
JSValue *addResult = [addFunc callWithArguments:@[@20, @30]];
NSLog(@"%d", addResult.toInt32); // 50
}
JS 脚本中调用 OC 代码
- (void)js_Call_OC {
JSContext *jsContext = [[JSContext alloc] init];
// 向 JS 上下文中注入一个 addFunc 办法
jsContext[@"addFunc"] = ^(NSInteger a, NSInteger b) {
return a + b;
};
// 调用 JS 脚本履行 OC 中的办法
JSValue *addResult = [jsContext evaluateScript:@"addFunc(2, 4)"];
NSLog(@"%@", addResult.toNumber); // 6
}
经过以上的比如,扼要介绍了一下 OC与 JS 之间交互的基本办法,而本热修正功用也正式运用了这些基本办法,完成了经过下发的 JS 脚本来达到调用到 OC 办法的意图。
2.2 OC 的反射
在这里用到的主要是经过字符串反射到对应的类或SEL
经过字符串创立类:Class
//
NSClassFromString(@"NSObject");
//
objc_getClass("NSObject");
经过字符串创立办法:selector
//
NSSelectorFromString(@"init");
//
NSStringFromSelector(selector)
其他反射办法:
//
NSStringFromCGRect(rect);
//
NSStringFromRange(range);
...等等
2.3 Runtime
在这里并不打开介绍 Runtime 的细节,扼要介绍一下所用到的办法,主要用到了如下几个:
//获取元类
Class objc_getMetaClass(const char *name)
//向类中增加办法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
//替换办法的完成
BOOL class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
//返回办法的完成
IMP method_getImplementation ( Method m );
//获取描绘办法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
//获取实例办法的 Method
Method class_getInstanceMethod(Class cls, SEL name);
//获取类办法的 Method
Method class_getClassMethod(Class cls, SEL name);
2.4 音讯转发
当给一个方针发送音讯的时分, 假如在其办法列表或父类办法列表中都没有找办法完成,那么就会进入到音讯转发流程,全体来看主要有三个进程, 如下图所示:

音讯转发流程主要涉及到的办法有:
// 1.运转时动态增加办法
+ (BOOL)resolveInstanceMethod:(SEL)sel
// 2.快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector
// 3.构建办法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// 4.音讯转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
咱们正是运用了最终的 forwardInvocation:这个办法,其参数是一个 NSInvocation 方针。NSInvocation 方针包含了这个办法调用的一切信息,如:target、selector、参数、返回值类型等,而且你还能够更改这些信息。
当然,除了上面这些正常的转发流程,咱们能够经过一个神奇的指针 _objc_msgForward 来强制触发音讯转发。咱们下面即将介绍的热修正原理正是运用了这个指针,但是并没有显现的指定这个指针,而是经过 Runtime 获取一个不存在的办法完成时,其返回值便是这个指针了,现已验证过了。
3.热修正原理(流程)
上面对热修正所用到的一些常识简略的介绍了一下,下面来具体的介绍一下热修正结构的原理,首先经过一张图来看下全体的流程:

- 经过JS脚本经过 JSCore 调用到 OC 代码
- 在 OC 代码中,经过 NSInvocation 可完成对实例办法或类办法的调用
- 在 OC 代码中,经过 Runtime 完成的对 OC 类中办法完成的替换,以及增加办法操作
下面将针对这些流程进行具体的打开解释。
3.1 JS 对 OC 代码的调用
办法的预埋,基于以上对 JSCore 运用布景的介绍,看到这些办法就一目了然了。相同也得益于 OC 支撑的映射机制,然后, JS 传递到 OC 的字符串能够简单的映射出对应的类名、办法名等。
例如下面一段代码:初始化一个 JSContext 方针,然后向 JS 上下文注入 OC 的办法完成,回调的参数有:实例方针(or 类名)、办法名、参数,以及回调的返回值。
+ (void)initFix{
JSContext *context = [JSFix context];
//修正指定办法
/*
参数:
1.类名
2.办法名
3.是否是类办法
4.修正的办法(前、中、后)
5.js 办法完成
*/
context[@"RestoreMethod"] = ^(NSString *className, NSString *selectorName,BOOL isClassMethod,XSFixType fixType, JSValue *fixImp) {//办法完成
[JSFix restoreMethodWithClassName:className selector:selectorName isClassMethod:isClassMethod fixType:fixType fixImp:fixImp];
};
//调用类办法:经过类名即可调用 恣意办法, 有返回值
context[@"runClassMethod"] = ^id(NSString *className,NSString *selectorName,id arguments){
id obj = [JSFix runWithClassName:className selectorName:selectorName arguments:arguments];
return obj;
};
//调用实例办法, 有返回值
context[@"runInstanceMethod"] = ^(id instatnce,NSString *selectorName,id arguments){
id obj = [JSFix runWithInstance:instatnce selectorName:selectorName arguments:arguments];
return obj;
};
//...等等还有一些其他办法,暂不一一列举了。
}
3.2 NSInvocation 的运用
上述预埋办法的回调中,调用到了 runWithInstance 这个办法,其完成细节如下:
+ (id)runWithInstance:(id)instance selectorName:(NSString *)selectorName arguments:(NSArray *)arguments{
if (!instance) {
return nil;
}
if (arguments && ![arguments isKindOfClass:[NSArray class]]) {
arguments = @[arguments];
}
if ([instance isKindOfClass:[JSValue class]]) {
instance = [instance toObject];
}
SEL sel = NSSelectorFromString(selectorName);
NSMethodSignature *signature = [instance methodSignatureForSelector:sel];
if (!signature) {
return nil;
}
@try {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
invocation.arguments = arguments;
[invocation invokeWithTarget:instance];
return invocation.returnValue_obj;
} @catch (NSException *exception) {
NSLog(@"runWithInstance 异常:%@",exception);
}
}
效果: JS 可将实例方针或类名,以及 selector、参数等传递到 OC, 经过反射机制转换成所需求的类型,最终经过 NSInvocation 方针完成的对实例办法或类办法的调用。
3.3 Runtime 的运用
主要是经过 Runtime 相关办法来触发 OC 办法的音讯转发机制。咱们知道,在运转期间调用的 OC 办法的完成不存在时,会走到音讯转发机制,正是运用转发机制中最终一步的 forwardInvocation 办法,并从该办法参数中能够获取到办法的原始完成,也便是 NSInvocation 方针,然后在指定的方位刺进自界说的代码完成。
主要的操作有:
- 经过 Runtime 替换方针办法的完成为
_objc_msgForward
,以便触发音讯转发机制 - 将方针办法的原来的完成,保存在一个别号办法中,以便对原完成的调用
- 经过 Runtime 替换音讯转发中的
forwardInvocation
办法完成,替换为自界说的完成,相同,原完成也保存在了别号办法中 - 在
forwardInvocation
自界说完成中,可对 JS 的脚本完成的调用,以及对方针办法原完成的调用
如图:

中心代码如下:
/// 优化的 replace
/// - Parameters:
/// - className: 类名
/// - selector: 办法名
/// - isClassMethod: 是否是类办法
/// - fixType: 修正的办法(前、中、后)
/// - fixImp: js 办法完成
+ (void)restoreMethodWithClassName:(NSString *)className selector:(NSString *)selector isClassMethod:(BOOL)isClassMethod fixType:(XSFixType)fixType fixImp:(JSValue *)fixImp {
if (className.length == 0 || selector.length == 0) {
return;
}
Class curClass = NSClassFromString(className);
if (isClassMethod) {
curClass = objc_getMetaClass(object_getClassName(curClass));
}
//1.处理方针办法
SEL oriSelector = NSSelectorFromString(selector);
Method oriMethod;
if (isClassMethod) {
oriMethod = class_getClassMethod(curClass, oriSelector);
}else{
oriMethod = class_getInstanceMethod(curClass, oriSelector);
}
IMP oriIMP = class_getMethodImplementation(curClass, oriSelector);
const char *methodTypes = method_getTypeEncoding(oriMethod);
//优先尝试增加方针办法
if (class_addMethod(curClass, oriSelector, oriIMP, methodTypes)) {
//增加成功, 说明之前没有该办法, 需求重新获取 method/imp
oriMethod = isClassMethod ? class_getClassMethod(curClass, oriSelector) : class_getInstanceMethod(curClass,oriSelector);
oriIMP = class_getMethodImplementation(curClass, oriSelector);
}
//2.处理方针办法_的别号办法
//方针办法的别号,用于保存方针办法的完成(因为方针办法的完成会被替换)
SEL swizzleSelector = originIMPSelector_forSelector(NSSelectorFromString(selector));
IMP swizzleIMP = class_getMethodImplementation(curClass, swizzleSelector);
//保存方针办法的完成, 存在别号办法名下
if (class_addMethod(curClass, swizzleSelector, oriIMP, methodTypes)) {
//保存成功后
//1.替换音讯转发中的 forwardInvocation 办法完成,为自界说的完成:swizzle_forwardInvocation
IMP oriFowardIMP = class_getMethodImplementation(curClass, @selector(forwardInvocation:));
if (oriFowardIMP != (IMP)swizzle_forwardInvocation) {
class_replaceMethod(curClass, @selector(forwardInvocation:), (IMP)swizzle_forwardInvocation, "v@:@");
if (oriFowardIMP) {
class_addMethod(curClass, NSSelectorFromString(@"alias_forwardInvocation:"), oriFowardIMP, "v@:@");//保存原来的完成
}
}
//2.替换方针办法的完成为 _objc_msgForward, 然后能触发音讯转发
method_setImplementation(oriMethod, swizzleIMP);
//3.生成 FixObject 方针,即将修正的方针办法的信息保存下来,当程序运转调用到该办法时,再履行相关操作
FixObject *fixObj = [FixObject new];
fixObj.selector = oriSelector;
fixObj.isClassMethod = isClassMethod;
fixObj.fixType = fixType;
fixObj.jsValue_IMP = fixImp;
[[JSFix shareInstance].fixObjs setObject:fixObj forKey:key_classForSelector(curClass, oriSelector)];
}
}
以上是对方针办法热修正做的预备工作,接下来咱们看下方针办法被实际调用后,具体是怎样运转的吧!
3.4 办法实际调用进程
当程序运转中,真实调用到方针办法时,经过咱们上述对其完成的替换,那么它的主要流程将是这样的:
- 程序调用方针的方针办法
- 找到方针办法的完成,因为完成现已被替换成 _objc_msgForward 指针,故走到音讯转发流程
- 来到音讯转发的 forwardInvocation 办法中,因为该办法的完成被替换成自界说的完成
- 来到自界说的 forwardInvocation 办法完成中,在这里,可调用在预备工作中保存下来的 JS 脚本
- 以及根据需求决定是否用方针办法的原始完成(前、后刺进),假如不调用原完成,就相当于办法完成的彻底替换操作
- 别的,假如是程序的 bug并不是咱们热修触发的音讯转发,而来到了 -forwardInvocation 办法时,仍然会走程序默许的原始完成,因为程序或许会在 NSObject 的分类中处理过这些异常。
如图:(图中序号对应上述进程 1-6)

自界说的 forwardInvocation:
办法完成如下:
static void swizzle_forwardInvocation(__unsafe_unretained NSObject *target, SEL selector, NSInvocation *invocation){
FixObject *fixObj = [[JSFix shareInstance].fixObjs objectForKey:key_classForSelector([target class],invocation.selector)];
//1.查看走到转发的音讯,是否是热修正的音讯
if (fixObj) {
fixObj.object = target;
fixObj.arguments = invocation.arguments;
//修正selector 为方针办法的别号办法, 这里保存着原完成
invocation.selector = originIMPSelector_forSelector(invocation.selector);
fixObj.originInvocation = invocation;
//触发调用 JS 脚本
[fixObj callJsValue_withOriginInvocation:invocation];
}else{
//2.继续履行原来的完成
SEL oriIMPSelector = originIMPSelector_forSelector(invocation.selector);
if ([target respondsToSelector:oriIMPSelector]) {
objc_msgSend(target, oriIMPSelector,invocation.arguments);
}else{
SEL oriForwarIMP_Selector = alias_forwardInvocation_sel();
if ([target respondsToSelector:oriForwarIMP_Selector]) {
objc_msgSend(target, oriForwarIMP_Selector ,invocation);
}
}
}
}
4.运用
4.1 结构初始化
能够在应用启动阶段对结构进行初始化操作,以及对 JS 脚本的加载和履行。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//1.初始化结构
[JSFix initFix];
//2.加载 js 脚本
NSString *path = [[NSBundle mainBundle]pathForResource:@"test" ofType:@"js"];
NSString *jsString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[JSFix evaluateJSString:jsString];
return YES;
}
4.2 如何写JS 脚本
那么如何经过 JS 写热修正的脚本代码呢,下面经过对文章开头比如中的代码进行一下剖析。
test.js 脚本中的代码:
//调用热修办法
RestoreMethod('ViewController','testMethod',0,1,function(instance,invocation,arg){
//JS完成
runInvocation_dispatch_async_main(function(){
var self = instance;
var view = runInstanceMethod(self,'view')
var redColor = runClassMethod('UIColor','redColor')
runInstanceMethod(view,'setBackgroundColor:',redColor)
})
})
其中:
-
调用
RestoreMethod
办法,其参数:用于指定修正ViewController
类的testMethod
办法,0
表明实例办法,1
表明办法替换(而非刺进,0 前刺进,1 替换,2 后刺进),最终的回调参数
便是 JS 的代码完成。 -
完成中的
runInvocation_dispatch_async_main
表明在 Native 中 GCD 办法的dispatch_async_main
的回调中履行。 -
回调中的参数
instance
就相当于 Native 办法中运用的 self 实例 -
回调中用到的
runInstanceMethod
办法,表明调用实例办法,如:调用 self 实例的 view 办法,等同于 Native 办法中 self.view 语句(getter 办法),其他调用原理类似。
5.留意事项
-
在 JS 调用 OC 的办法时分, 办法字符串中不能出现空格。
-
留意办法的大小写等,建议copy办法名,否则很简单出错,不易排查。
Demo 源码
后记
因为水平有限,如有不对之处,欢迎我们批评指正。
参考:
Aspect
JSPatch完成原理详解
JavaScriptCore结构详解
iOS音讯转发机制