LBYFix-依赖Aspects的轻量级低风险的 iOS Hotfix 方案

在经过由于Hotfix被下架大量App的风波后,强大好用的JSPatch已成为过去式,虽然JSPatch团队声称是被苹果误杀,也已经在和苹果进行沟通,并且提供了暂时的解决方案,但是在应用中使用JSPatch还是有很大被下架的风险。

那现在有没有轻量级低风险的库可以实现Hotfix呢?Aspects 就是这样一个库。虽然没有JSpatch那么强大,那么完善,但也足够应付一般场景。

LBYFix 就是依赖 Aspects 实现的一套轻量级低风险的 iOS Hotfix 的方案,LBYFix 提供了三种能力:

    1. 通过JS代码在任意方法前后注入代码的能力。
    1. 通过JS代码替换任意方法实现的能力。
    1. 通过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"

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

推荐阅读更多精彩内容