备注 (重要)


  • 1 目前这Aspects和JavascriptCore都能过审核,但是毕竟是热修复,过审有风险着情考虑,市场包请保持敬畏之心。

  • 2 目前这个是sdk只支持实例和类方法,对于同一个类,不能同时hook类方法和实例方法,只能二选一。

  • 3 不支持block语法,只在oc环境下简单测试,未应用于市场包。

  • 4 建议直接参考源码,能全面理解,下文只有部分关键节选。

参考资料


Aspects框架详解

IOS框架:JSPatch

码云源码:https://gitee.com/raychow-dev/TTVReFix.git

TTVReFix SDK

满足功能

  • 1 动态下发补丁

  • 2 function替换,不支持宏,block…

基于苹果的JavascriptCore框架

  • JavascriptCore框架是实现JS和OC互相交互的框架,常用在wkwebview加载网页,与原生交互。可在OC里面调用JS代码,也可以在JS中调用OC的代码。TTVReFix也是基于这套方案做的。

js解析对象挂载

  • global作为全局对象,常规的类使用前都需要require挂载在global对象,以生成对应映射类。以便调用__fn时,重新绑定__obj.
global.require = function(clsName) {
  if (!global[clsName]) {
    global[clsName] = {
      __clsName: clsName
    }
  }
  return global[clsName]
}

热修复流程

文件结构

image

  • 1 TTVRefixJsCore.js js解析翻译内核。

  • 2 TTVReFixManager 管理类,js与原生交互的桥梁。

  • 3 TTVAspects 用于方法替换,基于Aspects稍微修改了一点,block的引用类型

  • 4 TTVReFixDemo.js 为部分测试js写法

处理流程

image

对补丁js正则 节选

static NSString *_regexStr = @"(?<!\\\\)\\.\\s*(\\w+)\\s*\\(";
static NSString *_replaceStr = @".__fn(\"$1\")(";

- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL{
    if (!script || ![JSContext class]) {

        return nil;
    }
    if (script.length == 0) return nil;
    
    if (!_regex) {
        _regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];
    }
    NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];
    
    @try {
        if ([self.jsContext respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
            return [self.jsContext evaluateScript:formatedScript withSourceURL:resourceURL];
        } else {
            return [self.jsContext evaluateScript:formatedScript];
        }
    }
    @catch (NSException *exception) {
        //_exceptionBlock([NSString stringWithFormat:@"%@", exception]);
    }
    return nil;
}

对补丁js正则后

  • 任意js函数组成变为如下,按照js语法,每一级函数点语法,都转为使用了__fn作为调用函数入口,具体对比如下
UIView.alloc().init()
UIView.__fn('alloc')().__fn('init')()

fn调用参考

  • 其中_nature_callI和_nature_callC已经注入到JSContext对象中,执行__fn时,使用js拆分成如下
instance.navigationController().pushViewController_animated(vc,true)
instance.__fn('navigationController')().__fn('pushViewController:animated:')()
//js部分
global.methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
  var selectorName = methodName
  if (!isPerformSelector) {
    methodName = methodName.replace(/__/g, "-")
    selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
    var marchArr = selectorName.match(/:/g)
    var numOfArgs = marchArr ? marchArr.length : 0
    if (args.length > numOfArgs) {
      selectorName += ":"
    }
  }
  var ret = instance ? _nature_callI(instance, selectorName, args, isSuper):
                       _nature_callC(clsName, selectorName, args)
  return ret
}

global.customMethods = {
    __fn: function(methodName) {
        var slf = this
        if (!slf.__clsName){
            slf.__obj = this;
        }
      return function(){
        var args = Array.prototype.slice.call(arguments);
        var ret = global.methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper);
        return ret;
      }
    },
}

image

  • 上述回传给oc,使用Aspects替换执行
- (void)analysisReplaceOCMethod:(NSString*)jsFunctionIdentifier callText:(NSString*)callText argCount:(NSInteger)argCount isClassMethod:(BOOL)isClassMethod{
    NSArray* list = [jsFunctionIdentifier componentsSeparatedByString:@"_"];
    NSString* className = list.firstObject;
    NSString* selectorMethod = @"";
    for (int i = 0; i < list.count; i++) {
        if (i > 0) {
            selectorMethod = [selectorMethod stringByAppendingFormat:@"%@:",list[i]];
        }
    }
    
    if (argCount < 2 && [selectorMethod hasSuffix:@":"]) {
        selectorMethod = [selectorMethod substringToIndex:selectorMethod.length - 1];
    }
    
    [self replaceInstanceOCMethod:className selectorMethod:selectorMethod isClassMethod:isClassMethod block:^id(NSObject *instance, NSInvocation *originalInvocation, NSArray *arguments) {
        
        NSMutableArray* args = [NSMutableArray array];
        [args addObject:instance];
        [args addObjectsFromArray:arguments];
        
        NSMutableArray* ary = [NSMutableArray array];
        [ary addObject:jsFunctionIdentifier];
        [ary addObject:instance];
        [ary addObject:originalInvocation];
        [ary addObjectsFromArray:@[args]];
        return [self.jsContext[callText] callWithArguments:ary];
    }];
}

- (void)replaceInstanceOCMethod:(NSString*)className  selectorMethod:(NSString*)selectorMethod isClassMethod:(BOOL)isClassMethod block:(ttvReplaceFuncBlock)block{
    
    
    Class aClass =  isClassMethod ? object_getClass(NSClassFromString(className)) : NSClassFromString(className);
    [aClass ttv_aspect_hookSelector:NSSelectorFromString(selectorMethod)
                                          withOptions:TTVAspectPositionInstead
                                           usingBlock:^(id<TTVAspectInfo> info){
        if (block == nil) return ;
        
        NSObject* instance = [info instance];
        NSArray* arguments = [info arguments];
        JSValue* retObject = block(instance,info.originalInvocation,arguments);
        if (retObject.toObject == nil) return;
        
        NSMethodSignature* signature = info.originalInvocation.methodSignature;
        NSInvocation* invocation = info.originalInvocation;
        const char *argType = [signature methodReturnType];
        switch (argType[0]) {
#define fix_return(_typeChar, _type,_func) \
                case _typeChar: {   \
                    NSNumber* num = retObject.toNumber;\
                    _type arg = [num _func];  \
                    [invocation setReturnValue:&arg];    \
                    break;  \
                }
                fix_return('c', char,charValue)
                fix_return('C', unsigned char,unsignedCharValue)
                fix_return('s', short,shortValue)
                fix_return('S', unsigned short,unsignedShortValue)
                fix_return('i', int,intValue)
                fix_return('I', unsigned int,unsignedIntValue)
                fix_return('l', long,longValue)
                fix_return('L', unsigned long,unsignedLongValue)
                fix_return('q', long long,longLongValue)
                fix_return('Q', unsigned long long,unsignedLongValue)
                fix_return('f', float,floatValue)
                fix_return('d', double,doubleValue)
                fix_return('B', BOOL,boolValue)
            default:{
                id obj = retObject.toObject;
                [info.originalInvocation setReturnValue:&obj];
                break;
            }
        }
    } error:NULL];
}

在真实项目中测试

类方法 使用效果参考

//oc
+ (NSString *)unixStampToTimeSinceNow:(double)unixStamp {
    NSString *retString = @"";
    long long unixTimeInterval = unixStamp/1000;
    long long currentTimeInterval = [[NSDate date] timeIntervalSince1970] ;
    long long timeIntervalSinceNow = currentTimeInterval - unixTimeInterval;
    
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:unixTimeInterval];
    //测试用
    if (timeIntervalSinceNow < 0) {
        retString = @"";
    }
    if (timeIntervalSinceNow < 60) {
        //60秒
        retString = @"刚刚";
    } else if (timeIntervalSinceNow < 60*60) {
        //1分钟
        retString = [NSString stringWithFormat:@"%lld分钟前", timeIntervalSinceNow/60];
    } else if (timeIntervalSinceNow < 60*60*24) {
        //24小时
        retString = [NSString stringWithFormat:@"%lld小时前", timeIntervalSinceNow/(60*60)];
    } else{        
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];        
        formatter.dateFormat = @"yyyy-MM-dd HH:mm";
        retString = [formatter stringFromDate:date];        
        formatter.dateFormat = @"HH:mm";
        formatter.timeZone = [NSTimeZone timeZoneWithName:@"Asia/Shanghai"];
        retString = [formatter stringFromDate:date];
        if([NSDate isYesterday:date]){
            retString = [NSString stringWithFormat:@"昨天 %@", retString];
        }
        else if ([NSDate isBeforeYesterday:date]){
            retString = [NSString stringWithFormat:@"前天 %@", retString];
        }
        else{
            //大于72小时
            long long day =  [NSDate getBeforDayCount:date];
            retString = [NSString stringWithFormat:@"%lld天前 %@",day,retString];
        }
        //48小时
        //timeIntervalSinceNow = timeIntervalSinceNow - 60*60*24;
    }
    return retString;
}
//js
defineClass("TTVBOTimeIntervalHandle", {},{
            "unixStampToTimeSinceNow":function(instance,unixStamp) {
                return 111;
            },
});

image

image

实例方法 使用效果参考

//oc
- (void)homeHeaderView:(TTVHomeNewHeaderView *)headView didClickRattle:(id)obj{
    [self.logoHeaderView updateRedDot:NO];
    RattleListViewController* vc = [[RattleListViewController alloc] init];
    [TTVJumpManager pushViewControllerFromViewController:self toViewController:vc hidesBottomBarWhenPushed:true animated:true];
}
//js
require("UIView")
require("TTVUserViewController")
defineClass("HomeViewController", {
            "homeHeaderView_didClickSearch":function(instance,tabLogoview,obj) {
                //var aa = orgFunc(instance.__naturalOrgFunc); //调用原方法
                var vc = TTVUserViewController.alloc().init();
                vc.setHidesBottomBarWhenPushed(true);
                instance.navigationController().pushViewController_animated(vc,true)
                UIView.MakeToast_view_duration("我是UIApplications", UIApplication.sharedApplication().keyWindow(), 100);
            },
            }, {});

image