基于Aspects,并参考JSPatch的热修复SDK探讨

备注 (重要)


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

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

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

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

参考资料


Aspects框架详解

IOS框架:JSPatch

TTVReFix SDK源码

源码: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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容