基于AOP思想的HotFix库

热修复开发历程

最近一直在复习基础和整合知识文档,这里主要记录一下公司项目中采用的热修复技术。最早接触热修复正是JSPatch大行其道的时候,可惜愉快的使用不长时间就因为种种原因被苹果审核彻底封禁了。被封后笔者也采用了网上寻找到的诸如混淆关键字等策略,可惜最后也是无果而终。机缘巧合下接触到了AOP(面向切面编程),后来经过查询整合和对JSPatch的技术进行借鉴,最后封装了现在使用的热修复库。

什么是AOP

网上有很多资料,本篇主要阐述热修复的原理,AOP方面的知识可以参考笔者转载的一篇技术贴转:Aspects深度解析-iOS面向切面编程

热修复思想

这里主要借鉴JSPatch的思想,采用网络下发JS代码,并借用JavaScriptCore进行解析后调用AOP经典三方库Aspects在函数执行阶段进行插入、修改等操作,具体可以参考如下核心代码

核心代码

+ (Felix *)sharedInstance
{
    static Felix *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    
    return sharedInstance;
}

+ (void)evalString:(NSString *)javascriptString
{
    [[self context] evaluateScript:javascriptString];
}

+ (JSContext *)context
{
    static JSContext *_context;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _context = [[JSContext alloc] init];
        [_context setExceptionHandler:^(JSContext *context, JSValue *value) {
            NSLog(@"Oops: %@", value);
        }];
    });
    return _context;
}

+ (void)_fixWithMethod:(BOOL)isClassMethod aspectionOptions:(AspectOptions)option instanceName:(NSString *)instanceName selectorName:(NSString *)selectorName fixImpl:(JSValue *)fixImpl {
    Class klass = NSClassFromString(instanceName);
    if (isClassMethod) {
        klass = object_getClass(klass);
    }
    SEL sel = NSSelectorFromString(selectorName);
    [klass aspect_hookSelector:sel withOptions:option usingBlock:^(id<AspectInfo> aspectInfo){
        [fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
    } error:nil];
}

+(id)_runClassWithClassName:(NSString *)className selector:(NSString *)selector arguments:(NSArray *)arguments
{
    Class class = NSClassFromString(className);
    SEL sel = NSSelectorFromString(selector);
    NSMethodSignature *methodSignature = [class methodSignatureForSelector:sel];
    return [self safePerformAction:sel target:class params:arguments methodSig:methodSignature];
}

+(id)_runInstanceWithInstance:(id)instance selector:(NSString *)selector arguments:(NSArray *)arguments
{
    SEL sel = NSSelectorFromString(selector);
    NSMethodSignature *methodSignature = [[instance class] instanceMethodSignatureForSelector:sel];
    return [self safePerformAction:sel target:instance params:arguments methodSig:methodSignature];
}

+ (void)fixIt
{
    [self context][@"fixInstanceMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:NO aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };
    
    [self context][@"fixInstanceMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:NO aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };
    
    [self context][@"fixInstanceMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:NO aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };
    
    [self context][@"fixClassMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:YES aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };
    
    [self context][@"fixClassMethodReplace"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:YES aspectionOptions:AspectPositionInstead instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };
    
    [self context][@"fixClassMethodAfter"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
        [self _fixWithMethod:YES aspectionOptions:AspectPositionAfter instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
    };

    [self context][@"runClassMethod"] = ^id(NSString *className, NSString *selectorName, NSArray * _Nullable params) {
        return [self _runClassWithClassName:className selector:selectorName arguments:params];
    };

    [self context][@"runInstanceMethod"] = ^id(id instance, NSString *selectorName, NSArray * _Nullable params) {
        return [self _runInstanceWithInstance:instance selector:selectorName arguments:params];
    };
    
    [self context][@"runInstanceWithKeyPath"] = ^id(id instance, NSString *keyPath, NSString *selector,  NSArray * _Nullable params) {
        id subInstance = [instance valueForKeyPath:keyPath];
        return [self _runInstanceWithInstance:subInstance selector:selector arguments:params];
    };
    
    [self context][@"runInvocation"] = ^(NSInvocation *invocation) {
        [invocation invoke];
    };
    [self context][@"runInvocationWithParams"] = ^(NSInvocation *invocation, NSArray * _Nullable params) {
        if (params && [params isKindOfClass:[NSArray class]]) {
            for (int i=0; i<params.count; i++) {
                id value = params[i];
                [self setArgument:invocation value:value atIndex:i+2];
            }
        }
        [invocation invoke];
    };
    
    [self context][@"runInvocationWithReturn"] = ^(NSInvocation *invocation, id returnValue) {
        [invocation setReturnValue:&returnValue];
    };
    
    [self context][@"valueForKeyPath"] = ^id(id instance, NSString *keyPath) {
        id value = [instance valueForKeyPath:keyPath];
        return [JSValue valueWithObject:value inContext:[JSContext currentContext]];
    };
    
    [self context][@"setValueForKeyPath"] = ^(id instance, NSString *keyPath, id value) {
        [instance setValue:value forKeyPath:keyPath];
    };
    [self context][@"CGRectMake"] = ^NSValue *(CGFloat x,CGFloat y,CGFloat width,CGFloat height) {
        return @(CGRectMake(x, y, width, height));
    };
    
    [self context][@"CGSizeMake"] = ^NSValue *(CGFloat width,CGFloat height) {
        return @(CGSizeMake(width, height));
    };
    
    [self context][@"CGPointMake"] = ^NSValue *(CGFloat x,CGFloat y) {
        return @(CGPointMake(x, y));
    };

    // log方法
    [self context][@"log"] = ^(id message) {
        NSLog(@"Javascript log: %@",message);
    };
}

+(void)setArgument:(NSInvocation *)invocation value:(id)value atIndex:(NSInteger)index
{
    const char *c = [invocation.methodSignature getArgumentTypeAtIndex:index];
    if (strcmp(c, "@") == 0) {
        [invocation setArgument:&value atIndex:index];
    }
    else if(strcmp(c, "q") == 0) {
        long argu = [value longValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "l") == 0) {
        long long argu = [value longLongValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "d") == 0) {
        double argu = [value doubleValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "f") == 0) {
        float argu = [value floatValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "B") == 0) {
        bool argu = [value boolValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "s") == 0) {
        short argu = [value shortValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else if (strcmp(c, "c") == 0) {
        char argu = [value charValue];
        [invocation setArgument:&argu atIndex:index];
    }
    else {
        [invocation setArgument:&value atIndex:index];
    }
}

+ (id)safePerformAction:(SEL)action target:(id)target params:(NSArray *)params methodSig:(NSMethodSignature *)medSig
{
    NSMethodSignature* methodSig;
    if (medSig) {
        methodSig = medSig;
    }
    else {
        methodSig = [target methodSignatureForSelector:action];
    }
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];
    
    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setTarget:target];
        [self addInvocation:invocation params:params isBlock:action==nil];
        if (action) {
            [invocation setSelector:action];
        }
        [invocation invoke];
        return nil;
    }
    
    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setTarget:target];
        [self addInvocation:invocation params:params isBlock:action==nil];
        if (action) {
            [invocation setSelector:action];
        }
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }
    
    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setTarget:target];
        [self addInvocation:invocation params:params isBlock:action==nil];
        if (action) {
            [invocation setSelector:action];
        }
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }
    
    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setTarget:target];
        [self addInvocation:invocation params:params isBlock:action==nil];
        if (action) {
            [invocation setSelector:action];
        }
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }
    
    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setTarget:target];
        [self addInvocation:invocation params:params isBlock:action==nil];
        if (action) {
            [invocation setSelector:action];
        }
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
    [invocation setTarget:target];
    [self addInvocation:invocation params:params isBlock:action==nil];
    if (action) {
        [invocation setSelector:action];
    }
    [invocation invoke];
    void *obj;
    [invocation getReturnValue:&obj];
    return (__bridge NSObject *)obj;
}

+(void)addInvocation:(NSInvocation *)invocation params:(NSArray *)params isBlock:(BOOL)isBlock
{
    if (!params || ![params isKindOfClass:[NSArray class]]) {
        return;
    }
    for (int i=0; i<params.count; i++) {
        [self setArgument:invocation value:params[i] atIndex:isBlock?i+1:i+2];
    }
}

上面主要是一些常用的方法,可以实现诸如调用类方法、实例方法,KVC方式获取属性等及基本操作,其他的复杂操作诸位可以借鉴这些实现方式自行添加

JS代码下发

考虑到代码下发阶段被恶意篡改等安全问题,笔者主要将JS源码MD5操作后采用RSA非对称加密生成秘钥文件,然后将秘钥文件和JS源码共同下发,执行阶段在进行秘钥比对,如果核实通过执行

总结和不足

1、热修复虽然实现了一些基础方法,但是如创建block、多线程操作等因为时间原因一直没有进行迭代优化
2、公司采用的私有cocoapods部署,一直没有机会放到github进行公开
3、Aspects作为优秀的AOP库,但由于时间过于悠久,执行效率有些低下,后期有机会可以使用Stinger进行重构

不足之处请各位踊跃指出,有需要的同学可以向笔者询问具体源码,大家共同努力完善

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

推荐阅读更多精彩内容