在经过由于Hotfix被下架大量App的风波后,强大好用的JSPatch已成为过去式,虽然JSPatch团队声称是被苹果误杀,也已经在和苹果进行沟通,并且提供了暂时的解决方案,但是在应用中使用JSPatch还是有很大被下架的风险。
那现在有没有轻量级低风险的库可以实现Hotfix呢?Aspects 就是这样一个库。虽然没有JSpatch那么强大,那么完善,但也足够应付一般场景。
LBYFix 就是依赖 Aspects 实现的一套轻量级低风险的 iOS Hotfix 的方案,LBYFix 提供了三种能力:
- 通过JS代码在任意方法前后注入代码的能力。
- 通过JS代码替换任意方法实现的能力。
- 通过JS代码调用任意类/实例方法的能力。
第一、二两点就是用 Aspects 来实现的。第三点是用NSInvocation来实现的,当然也可以用[NSObject performSelector:...]来调用任意类/实例方法,但是当有两个以上参数的时候[NSObject performSelector:...]就不好使了。
Aspects 使用姿势:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
前插、后插、替换某个方法都可以。使用类的方式很简单,NSClassFromString 即可,Selector 也一样 NSSelectorFromString,这样就能通过外部传入 String,内部动态构造 Class 和 Selector 来达到 Fix 的效果了。
这种方式的安全性在于:
不需要中间 JS 文件,准备工作全部在 Native 端完成。
没有使用 App Store 不友好的类/方法。
LBYFix 使用姿势:
导入方式:
- 方式一:直接将LBYFix和Aspects拖到项目中。
- 方式二:通过pod库导入。
pod 'LBYFix', '~> 1.0.0'
使用流程:
-
初始化LBYFix
在application:didFinishLaunchingWithOptions:中初始化LBYFix
[LBYFix fixIt];
- 替换方法实现
NSString *jsString = @"fixMethod('LBYFixDemo', 'instanceMightCrash:', 1, false, \
function(instance, originInvocation, originArguments) { \
if (originArguments[0] == null) { \
runErrorBranch('LBYFixDemo', 'instanceMightCrash'); \
} else { \
runInvocation(originInvocation); \
} \
}); \
";
[LBYFix evalString:jsString];
上面的js代码的意思是通过调用LBYFix暴露给JavaScript的fixMethod方法,将LBYFixDemo的instanceMightCrash实例方法替换成function实现,如果function中originArguments[0]参数等于null,则调用runErrorBranch方法,否则走原来的逻辑。
- 在方法前插入代码
NSString *jsString = @"fixMethod('LBYFixDemo', 'runBeforeInstanceMethod', 2, false, \
function(){ \
runInstanceMethod('LBYFixDemo', 'beforeInstanceMethod:param2:', new Array('LBYFix', 888)); \
});";
[LBYFix evalString:jsString];
上面js代码的意思是在LBYFixDemo类的runBeforeInstanceMethod方法前插入function实现。
- 在方法后插入代码
NSString *jsString = @"fixMethod('LBYFixDemo2', 'runAfterClassMethod', 0, true, \
function(){ \
runClassMethod('LBYFixDemo2', 'afterClassMethod:param2:', new Array('LBYFix', 999)); \
}); \
";
[LBYFix evalString:jsString];
上面js代码的意思是在LBYFixDemo2的runAfterClassMethod方法后插入function实现。
- 执行没有参数的方法
NSString *jsString = @"runInstanceMethod('LBYFixDemo3', 'instanceMethodHasNoParams')";
[LBYFix evalString:jsString];
上面js代码的意思是调用LBYFixDemo3类的instanceMethodHasNoParams实例方法。
- 执行带多个参数的方法
NSString *jsString = @"runInstanceMethod('LBYFixDemo3', 'instanceMethodHasMultipleParams:size:rect:', new Array({x: 1.1, y: 2.2}, {width: 3.3, height: 4.4}, {origin: {x: 5.5, y: 6.6}, size: {width: 7.7, height: 8.8}}))\
";
[LBYFix evalString:jsString];
上面js代码的意思是调用LBYFixDemo3类的instanceMethodHasMultipleParams:size:rect:实例方法,并通过数组传入参数。
LBYFix源码分析
初始化方法主要是提供了几个给JavaScript调用的方法。
+ (void)fixIt {
JSContext *context = [self context];
context[@"fixMethod"] = ^(NSString *instanceName, NSString *selectorName, LBYFixOptions options, BOOL isClassMethod, JSValue *fixImpl) {
[self fixWithMethod:isClassMethod options:options instanceName:instanceName selectorName:selectorName fixImp:fixImpl];
};
context[@"runInvocation"] = ^(NSInvocation *invocation) {
[invocation invoke];
};
context[@"runErrorBranch"] = ^(NSString *instanceName, NSString *selectorName) {
NSLog(@"runErrorBranch: instanceName = %@, selectorName = %@", instanceName, selectorName);
};
context[@"runClassMethod"] = ^id(NSString *className, NSString *selectorName, NSArray *arguments) {
return [self runClassWithClassName:className selector:selectorName arguments:arguments];
};
context[@"runInstanceMethod"] = ^id(NSString * className, NSString *selectorName, NSArray *arguments) {
return [self runInstanceWithInstance:className selector:selectorName arguments:arguments];
};
}
fixWithMethod: options:方法是通过Aspects进行前插、后插、替换方法。
+ (void)fixWithMethod:(BOOL)isClassMethod options:(LBYFixOptions)options instanceName:(NSString *)instanceName selectorName:(NSString *)selectorName fixImp:(JSValue *)fixImpl {
Class klass = NSClassFromString(instanceName);
if (isClassMethod) {
klass = object_getClass(klass);
}
SEL sel = NSSelectorFromString(selectorName);
[klass aspect_hookSelector:sel withOptions:(AspectOptions)options usingBlock:^(id<AspectInfo> aspectInfo) {
[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
} error:nil];
}
runClassWithClassName:selector:arguments:通过NSInvocation调用类方法。
+ (id)runClassWithClassName:(NSString *)className selector:(NSString *)selector arguments:(NSArray *)arguments {
Class klass = NSClassFromString(className);
if (!klass) return nil;
SEL sel = NSSelectorFromString(selector);
if (!sel) return nil;
if (![klass respondsToSelector:sel]) return nil;
NSMethodSignature *signature = [klass methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
[self setInv:invocation withSig:signature andArgs:arguments];
[invocation invokeWithTarget:klass];
return [self getReturnFromInv:invocation withSig:signature];
}
runInstanceWithInstance:selector:arguments:通过NSInvocation调用实例方法。
+ (id)runInstanceWithInstance:(NSString *)className selector:(NSString *)selector arguments:(NSArray *)arguments {
Class klass = NSClassFromString(className);
if (!klass) return nil;
SEL sel = NSSelectorFromString(selector);
if (!sel) return nil;
id instance = [[klass alloc] init];
if (![instance respondsToSelector:sel]) return nil;
NSMethodSignature *signature = [instance methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = sel;
[self setInv:invocation withSig:signature andArgs:arguments];
[invocation invokeWithTarget:instance];
return [self getReturnFromInv:invocation withSig:signature];
}
getReturnFromInv: withSig:用来获取方法的返回值。
+ (id)getReturnFromInv:(NSInvocation *)inv withSig:(NSMethodSignature *)sig {
NSUInteger length = [sig methodReturnLength];
if (length == 0) return nil;
char *type = (char *)[sig methodReturnType];
while (*type == 'r' || // const
*type == 'n' || // in
*type == 'N' || // inout
*type == 'o' || // out
*type == 'O' || // bycopy
*type == 'R' || // byref
*type == 'V') { // oneway
type++; // cutoff useless prefix
}
#define return_with_number(_type_) \
do { \
_type_ ret; \
[inv getReturnValue:&ret]; \
return @(ret); \
} while(0)
switch (*type) {
case 'v': return nil; // void
case 'B': return_with_number(bool);
case 'c': return_with_number(char);
case 'C': return_with_number(unsigned char);
case 's': return_with_number(short);
case 'S': return_with_number(unsigned short);
case 'i': return_with_number(int);
case 'I': return_with_number(unsigned int);
case 'l': return_with_number(int);
case 'L': return_with_number(unsigned int);
case 'q': return_with_number(long long);
case 'Q': return_with_number(unsigned long long);
case 'f': return_with_number(float);
case 'd': return_with_number(double);
case 'D': { // long double
long double ret;
[inv getReturnValue:&ret];
return [NSNumber numberWithDouble:ret];
};
case '@': { // id
void *ret;
[inv getReturnValue:&ret];
return (__bridge id)(ret);
};
case '#' : { // Class
Class ret = nil;
[inv getReturnValue:&ret];
return ret;
};
default: { // struct / union / SEL / void* / unknown
const char *objCType = [sig methodReturnType];
char *buf = calloc(1, length);
if (!buf) return nil;
[inv getReturnValue:buf];
NSValue *value = [NSValue valueWithBytes:buf objCType:objCType];
free(buf);
return value;
};
}
#undef return_with_number
}
setInv: withSig: andArgs:设置方法的参数。
+ (void)setInv:(NSInvocation *)inv withSig:(NSMethodSignature *)sig andArgs:(NSArray *)args {
#define args_length_judgments(_index_) \
[self argsLengthJudgment:args index:_index_] \
#define set_with_args(_index_, _type_, _sel_) \
do { \
_type_ arg; \
if (args_length_judgments(_index_-2)) { \
arg = [args[_index_-2] _sel_]; \
} \
[inv setArgument:&arg atIndex:_index_]; \
} while(0)
#define set_with_args_struct(_dic_, _struct_, _param_, _key_, _sel_) \
do { \
if (_dic_ && [_dic_ isKindOfClass:[NSDictionary class]]) { \
if ([_dic_.allKeys containsObject:_key_]) { \
_struct_._param_ = [_dic_[_key_] _sel_]; \
} \
} \
} while(0)
NSUInteger count = [sig numberOfArguments];
for (int index = 2; index < count; index++) {
char *type = (char *)[sig getArgumentTypeAtIndex:index];
while (*type == 'r' || // const
*type == 'n' || // in
*type == 'N' || // inout
*type == 'o' || // out
*type == 'O' || // bycopy
*type == 'R' || // byref
*type == 'V') { // oneway
type++; // cutoff useless prefix
}
BOOL unsupportedType = NO;
switch (*type) {
case 'v': // 1:void
case 'B': // 1:bool
case 'c': // 1: char / BOOL
case 'C': // 1: unsigned char
case 's': // 2: short
case 'S': // 2: unsigned short
case 'i': // 4: int / NSInteger(32bit)
case 'I': // 4: unsigned int / NSUInteger(32bit)
case 'l': // 4: long(32bit)
case 'L': // 4: unsigned long(32bit)
{ // 'char' and 'short' will be promoted to 'int'
set_with_args(index, int, intValue);
} break;
case 'q': // 8: long long / long(64bit) / NSInteger(64bit)
case 'Q': // 8: unsigned long long / unsigned long(64bit) / NSUInteger(64bit)
{
set_with_args(index, long long, longLongValue);
} break;
case 'f': // 4: float / CGFloat(32bit)
{
set_with_args(index, float, floatValue);
} break;
case 'd': // 8: double / CGFloat(64bit)
case 'D': // 16: long double
{
set_with_args(index, double, doubleValue);
} break;
case '*': // char *
{
if (args_length_judgments(index-2)) {
NSString *arg = args[index-2];
if ([arg isKindOfClass:[NSString class]]) {
const void *c = [arg UTF8String];
[inv setArgument:&c atIndex:index];
}
}
} break;
case '#': // Class
{
if (args_length_judgments(index-2)) {
NSString *arg = args[index-2];
if ([arg isKindOfClass:[NSString class]]) {
Class klass = NSClassFromString(arg);
if (klass) {
[inv setArgument:&klass atIndex:index];
}
}
}
} break;
case '@': // id
{
if (args_length_judgments(index-2)) {
id arg = args[index-2];
[inv setArgument:&arg atIndex:index];
}
} break;
case '{': // struct
{
if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint point = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, point, x, @"x", doubleValue);
set_with_args_struct(dict, point, y, @"y", doubleValue);
}
[inv setArgument:&point atIndex:index];
} else if (strcmp(type, @encode(CGSize)) == 0) {
CGSize size = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, size, width, @"width", doubleValue);
set_with_args_struct(dict, size, height, @"height", doubleValue);
}
[inv setArgument:&size atIndex:index];
} else if (strcmp(type, @encode(CGRect)) == 0) {
CGRect rect;
CGPoint origin = {0};
CGSize size = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
NSDictionary *pDict = dict[@"origin"];
set_with_args_struct(pDict, origin, x, @"x", doubleValue);
set_with_args_struct(pDict, origin, y, @"y", doubleValue);
NSDictionary *sDict = dict[@"size"];
set_with_args_struct(sDict, size, width, @"width", doubleValue);
set_with_args_struct(sDict, size, height, @"height", doubleValue);
}
rect.origin = origin;
rect.size = size;
[inv setArgument:&rect atIndex:index];
} else if (strcmp(type, @encode(CGVector)) == 0) {
CGVector vector = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, vector, dx, @"dx", doubleValue);
set_with_args_struct(dict, vector, dy, @"dy", doubleValue);
}
[inv setArgument:&vector atIndex:index];
} else if (strcmp(type, @encode(CGAffineTransform)) == 0) {
CGAffineTransform form = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, form, a, @"a", doubleValue);
set_with_args_struct(dict, form, b, @"b", doubleValue);
set_with_args_struct(dict, form, c, @"c", doubleValue);
set_with_args_struct(dict, form, d, @"d", doubleValue);
set_with_args_struct(dict, form, tx, @"tx", doubleValue);
set_with_args_struct(dict, form, ty, @"ty", doubleValue);
}
[inv setArgument:&form atIndex:index];
} else if (strcmp(type, @encode(CATransform3D)) == 0) {
CATransform3D form3D = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, form3D, m11, @"m11", doubleValue);
set_with_args_struct(dict, form3D, m12, @"m12", doubleValue);
set_with_args_struct(dict, form3D, m13, @"m13", doubleValue);
set_with_args_struct(dict, form3D, m14, @"m14", doubleValue);
set_with_args_struct(dict, form3D, m21, @"m21", doubleValue);
set_with_args_struct(dict, form3D, m22, @"m22", doubleValue);
set_with_args_struct(dict, form3D, m23, @"m23", doubleValue);
set_with_args_struct(dict, form3D, m24, @"m24", doubleValue);
set_with_args_struct(dict, form3D, m31, @"m31", doubleValue);
set_with_args_struct(dict, form3D, m32, @"m32", doubleValue);
set_with_args_struct(dict, form3D, m33, @"m33", doubleValue);
set_with_args_struct(dict, form3D, m34, @"m34", doubleValue);
set_with_args_struct(dict, form3D, m41, @"m41", doubleValue);
set_with_args_struct(dict, form3D, m42, @"m42", doubleValue);
set_with_args_struct(dict, form3D, m43, @"m43", doubleValue);
set_with_args_struct(dict, form3D, m44, @"m44", doubleValue);
}
[inv setArgument:&form3D atIndex:index];
} else if (strcmp(type, @encode(NSRange)) == 0) {
NSRange range = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, range, location, @"location", unsignedIntegerValue);
set_with_args_struct(dict, range, length, @"length", unsignedIntegerValue);
}
[inv setArgument:&range atIndex:index];
} else if (strcmp(type, @encode(UIOffset)) == 0) {
UIOffset offset = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, offset, horizontal, @"horizontal", doubleValue);
set_with_args_struct(dict, offset, vertical, @"vertical", doubleValue);
}
[inv setArgument:&offset atIndex:index];
} else if (strcmp(type, @encode(UIEdgeInsets)) == 0) {
UIEdgeInsets insets = {0};
if (args_length_judgments(index-2)) {
NSDictionary *dict = args[index-2];
set_with_args_struct(dict, insets, top, @"top", doubleValue);
set_with_args_struct(dict, insets, left, @"left", doubleValue);
set_with_args_struct(dict, insets, bottom, @"bottom", doubleValue);
set_with_args_struct(dict, insets, right, @"right", doubleValue);
}
[inv setArgument:&insets atIndex:index];
} else {
unsupportedType = YES;
}
} break;
case '^': // pointer
{
unsupportedType = YES;
} break;
case ':': // SEL
{
unsupportedType = YES;
} break;
case '(': // union
{
unsupportedType = YES;
} break;
case '[': // array
{
unsupportedType = YES;
} break;
default: // what?!
{
unsupportedType = YES;
} break;
}
NSAssert(!unsupportedType, @"arg unsupportedType");
}
}
参考
LBYFix Demo下载
轻量级低风险 iOS Hotfix 方案
Aspects源码解析
JavaScriptCore 使用
YYKit
Issues: "When exchanged a class method after a instance method exchanged, the class method will be invalid"