支撑的才能:

1.支撑在原办法完成之前、后刺进新完成,或许替换

2.支撑修正原办法的参数、返回值(支撑 block 作为参数的办法)

3.支撑给已有类增加新的办法和完成(开发 UI 以及需求逻辑等)

4.支撑 GCD,UIView.animation 等 block 办法调用(但需求提前预埋这些办法)

1. 案例效果

首先介绍一个热修正的比如,然后再去看具体的完成原理,然后再去一层层的剥开那些并不奥秘的面纱。

例1:修正 crash 的办法, 如在异步线程调用 UI 的改写,导致的 crash,可经过下发脚本对该办法进行完成的替换,使其在主线程履行。

效果:

玩一下 Objective - C 热修复

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,并完成点击事情

效果:

玩一下 Objective - C 热修复

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 音讯转发

当给一个方针发送音讯的时分, 假如在其办法列表或父类办法列表中都没有找办法完成,那么就会进入到音讯转发流程,全体来看主要有三个进程, 如下图所示:

玩一下 Objective - C 热修复

音讯转发流程主要涉及到的办法有:

// 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.热修正原理(流程)

上面对热修正所用到的一些常识简略的介绍了一下,下面来具体的介绍一下热修正结构的原理,首先经过一张图来看下全体的流程:

玩一下 Objective - C 热修复

  • 经过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 方针,然后在指定的方位刺进自界说的代码完成。

主要的操作有:

  1. 经过 Runtime 替换方针办法的完成为 _objc_msgForward,以便触发音讯转发机制
  2. 将方针办法的原来的完成,保存在一个别号办法中,以便对原完成的调用
  3. 经过 Runtime 替换音讯转发中的 forwardInvocation 办法完成,替换为自界说的完成,相同,原完成也保存在了别号办法中
  4. forwardInvocation 自界说完成中,可对 JS 的脚本完成的调用,以及对方针办法原完成的调用

如图:

玩一下 Objective - C 热修复

中心代码如下:

/// 优化的 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 办法实际调用进程

当程序运转中,真实调用到方针办法时,经过咱们上述对其完成的替换,那么它的主要流程将是这样的:

  1. 程序调用方针的方针办法
  2. 找到方针办法的完成,因为完成现已被替换成 _objc_msgForward 指针,故走到音讯转发流程
  3. 来到音讯转发的 forwardInvocation 办法中,因为该办法的完成被替换成自界说的完成
  4. 来到自界说的 forwardInvocation 办法完成中,在这里,可调用在预备工作中保存下来的 JS 脚本
  5. 以及根据需求决定是否用方针办法的原始完成(前、后刺进),假如不调用原完成,就相当于办法完成的彻底替换操作
  6. 别的,假如是程序的 bug并不是咱们热修触发的音讯转发,而来到了 -forwardInvocation 办法时,仍然会走程序默许的原始完成,因为程序或许会在 NSObject 的分类中处理过这些异常。

如图:(图中序号对应上述进程 1-6)

玩一下 Objective - C 热修复

自界说的 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)
  })
})

其中:

  1. 调用 RestoreMethod 办法,其参数:用于指定修正 ViewController 类的 testMethod 办法,0 表明实例办法,1表明办法替换(而非刺进,0 前刺进,1 替换,2 后刺进),最终的回调参数便是 JS 的代码完成。

  2. 完成中的 runInvocation_dispatch_async_main 表明在 Native 中 GCD 办法的 dispatch_async_main 的回调中履行。

  3. 回调中的参数 instance 就相当于 Native 办法中运用的 self 实例

  4. 回调中用到的 runInstanceMethod 办法,表明调用实例办法,如:调用 self 实例的 view 办法,等同于 Native 办法中 self.view 语句(getter 办法),其他调用原理类似。

5.留意事项

  1. 在 JS 调用 OC 的办法时分, 办法字符串中不能出现空格。

  2. 留意办法的大小写等,建议copy办法名,否则很简单出错,不易排查。

Demo 源码


后记

因为水平有限,如有不对之处,欢迎我们批评指正。

参考:

Aspect

JSPatch完成原理详解

JavaScriptCore结构详解

iOS音讯转发机制

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。