美文网首页
基于AOP思想的HotFix库

基于AOP思想的HotFix库

作者: LamSpeech | 来源:发表于2020-04-26 15:12 被阅读0次

    热修复开发历程

    最近一直在复习基础和整合知识文档,这里主要记录一下公司项目中采用的热修复技术。最早接触热修复正是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进行重构

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

    相关文章

      网友评论

          本文标题:基于AOP思想的HotFix库

          本文链接:https://www.haomeiwen.com/subject/fodfwhtx.html