基于Aspects,并参考JSPatch的热修复SDK探讨
备注 (重要)
-
1 目前这Aspects和JavascriptCore都能过审核,但是毕竟是热修复,过审有风险着情考虑,市场包请保持敬畏之心。
-
2 目前这个是sdk只支持实例和类方法,对于同一个类,不能同时hook类方法和实例方法,只能二选一。
-
3 不支持block语法,只在oc环境下简单测试,未应用于市场包。
-
4 建议直接参考源码,能全面理解,下文只有部分关键节选。
参考资料
码云源码: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]
}
热修复流程
文件结构
-
1 TTVRefixJsCore.js js解析翻译内核。
-
2 TTVReFixManager 管理类,js与原生交互的桥梁。
-
3 TTVAspects 用于方法替换,基于Aspects稍微修改了一点,block的引用类型
-
4 TTVReFixDemo.js 为部分测试js写法
处理流程
对补丁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;
}
},
}
- 上述回传给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;
},
});
实例方法 使用效果参考
//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);
},
}, {});