在經(jīng)過由于Hotfix被下架大量App的風(fēng)波后绑咱,強(qiáng)大好用的JSPatch已成為過去式,雖然JSPatch團(tuán)隊(duì)聲稱是被蘋果誤殺旺拉,也已經(jīng)在和蘋果進(jìn)行溝通漾脂,并且提供了暫時(shí)的解決方案,但是在應(yīng)用中使用JSPatch還是有很大被下架的風(fēng)險(xiǎn)辆亏。
那現(xiàn)在有沒有輕量級(jí)低風(fēng)險(xiǎn)的庫可以實(shí)現(xiàn)Hotfix呢风秤?Aspects 就是這樣一個(gè)庫。雖然沒有JSpatch那么強(qiáng)大扮叨,那么完善缤弦,但也足夠應(yīng)付一般場(chǎng)景。
LBYFix 就是依賴 Aspects 實(shí)現(xiàn)的一套輕量級(jí)低風(fēng)險(xiǎn)的 iOS Hotfix 的方案彻磁,LBYFix 提供了三種能力:
- 通過JS代碼在任意方法前后注入代碼的能力碍沐。
- 通過JS代碼替換任意方法實(shí)現(xiàn)的能力。
- 通過JS代碼調(diào)用任意類/實(shí)例方法的能力衷蜓。
第一累提、二兩點(diǎn)就是用 Aspects 來實(shí)現(xiàn)的。第三點(diǎn)是用NSInvocation來實(shí)現(xiàn)的磁浇,當(dāng)然也可以用[NSObject performSelector:...]來調(diào)用任意類/實(shí)例方法斋陪,但是當(dāng)有兩個(gè)以上參數(shù)的時(shí)候[NSObject performSelector:...]就不好使了。
Aspects 使用姿勢(shì):
[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];
前插扯夭、后插鳍贾、替換某個(gè)方法都可以。使用類的方式很簡(jiǎn)單交洗,NSClassFromString 即可骑科,Selector 也一樣 NSSelectorFromString,這樣就能通過外部傳入 String构拳,內(nèi)部動(dòng)態(tài)構(gòu)造 Class 和 Selector 來達(dá)到 Fix 的效果了咆爽。
這種方式的安全性在于:
不需要中間 JS 文件,準(zhǔn)備工作全部在 Native 端完成置森。
沒有使用 App Store 不友好的類/方法斗埂。
LBYFix 使用姿勢(shì):
導(dǎo)入方式:
- 方式一:直接將LBYFix和Aspects拖到項(xiàng)目中。
- 方式二:通過pod庫導(dǎo)入凫海。
pod 'LBYFix', '~> 1.0.0'
使用流程:
-
初始化LBYFix
在application:didFinishLaunchingWithOptions:中初始化LBYFix
[LBYFix fixIt];
- 替換方法實(shí)現(xiàn)
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代碼的意思是通過調(diào)用LBYFix暴露給JavaScript的fixMethod方法呛凶,將LBYFixDemo的instanceMightCrash實(shí)例方法替換成function實(shí)現(xiàn),如果function中originArguments[0]參數(shù)等于null行贪,則調(diào)用runErrorBranch方法漾稀,否則走原來的邏輯模闲。
- 在方法前插入代碼
NSString *jsString = @"fixMethod('LBYFixDemo', 'runBeforeInstanceMethod', 2, false, \
function(){ \
runInstanceMethod('LBYFixDemo', 'beforeInstanceMethod:param2:', new Array('LBYFix', 888)); \
});";
[LBYFix evalString:jsString];
上面js代碼的意思是在LBYFixDemo類的runBeforeInstanceMethod方法前插入function實(shí)現(xiàn)。
- 在方法后插入代碼
NSString *jsString = @"fixMethod('LBYFixDemo2', 'runAfterClassMethod', 0, true, \
function(){ \
runClassMethod('LBYFixDemo2', 'afterClassMethod:param2:', new Array('LBYFix', 999)); \
}); \
";
[LBYFix evalString:jsString];
上面js代碼的意思是在LBYFixDemo2的runAfterClassMethod方法后插入function實(shí)現(xiàn)崭捍。
- 執(zhí)行沒有參數(shù)的方法
NSString *jsString = @"runInstanceMethod('LBYFixDemo3', 'instanceMethodHasNoParams')";
[LBYFix evalString:jsString];
上面js代碼的意思是調(diào)用LBYFixDemo3類的instanceMethodHasNoParams實(shí)例方法尸折。
- 執(zhí)行帶多個(gè)參數(shù)的方法
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代碼的意思是調(diào)用LBYFixDemo3類的instanceMethodHasMultipleParams:size:rect:實(shí)例方法,并通過數(shù)組傳入?yún)?shù)。
LBYFix源碼分析
初始化方法主要是提供了幾個(gè)給JavaScript調(diào)用的方法。
+ (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進(jìn)行前插浙炼、后插、替換方法亮航。
+ (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調(diào)用類方法。
+ (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調(diào)用實(shí)例方法谍倦。
+ (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:設(shè)置方法的參數(shù)泪勒。
+ (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下載
輕量級(jí)低風(fēng)險(xiǎn) iOS Hotfix 方案
Aspects源碼解析
JavaScriptCore 使用
YYKit
Issues: "When exchanged a class method after a instance method exchanged, the class method will be invalid"