基于AOP思想的HotFix庫(kù)

熱修復(fù)開發(fā)歷程

最近一直在復(fù)習(xí)基礎(chǔ)和整合知識(shí)文檔,這里主要記錄一下公司項(xiàng)目中采用的熱修復(fù)技術(shù)。最早接觸熱修復(fù)正是JSPatch大行其道的時(shí)候,可惜愉快的使用不長(zhǎng)時(shí)間就因?yàn)榉N種原因被蘋果審核徹底封禁了。被封后筆者也采用了網(wǎng)上尋找到的諸如混淆關(guān)鍵字等策略,可惜最后也是無果而終。機(jī)緣巧合下接觸到了AOP(面向切面編程),后來經(jīng)過查詢整合和對(duì)JSPatch的技術(shù)進(jìn)行借鑒,最后封裝了現(xiàn)在使用的熱修復(fù)庫(kù)。

什么是AOP

網(wǎng)上有很多資料,本篇主要闡述熱修復(fù)的原理,AOP方面的知識(shí)可以參考筆者轉(zhuǎn)載的一篇技術(shù)貼轉(zhuǎn):Aspects深度解析-iOS面向切面編程

熱修復(fù)思想

這里主要借鑒JSPatch的思想,采用網(wǎng)絡(luò)下發(fā)JS代碼,并借用JavaScriptCore進(jìn)行解析后調(diào)用AOP經(jīng)典三方庫(kù)Aspects在函數(shù)執(zhí)行階段進(jìn)行插入、修改等操作,具體可以參考如下核心代碼

核心代碼

+ (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];
    }
}

上面主要是一些常用的方法,可以實(shí)現(xiàn)諸如調(diào)用類方法、實(shí)例方法,KVC方式獲取屬性等及基本操作,其他的復(fù)雜操作諸位可以借鑒這些實(shí)現(xiàn)方式自行添加

JS代碼下發(fā)

考慮到代碼下發(fā)階段被惡意篡改等安全問題,筆者主要將JS源碼MD5操作后采用RSA非對(duì)稱加密生成秘鑰文件,然后將秘鑰文件和JS源碼共同下發(fā),執(zhí)行階段在進(jìn)行秘鑰比對(duì),如果核實(shí)通過執(zhí)行

總結(jié)和不足

1、熱修復(fù)雖然實(shí)現(xiàn)了一些基礎(chǔ)方法,但是如創(chuàng)建block、多線程操作等因?yàn)闀r(shí)間原因一直沒有進(jìn)行迭代優(yōu)化
2、公司采用的私有cocoapods部署,一直沒有機(jī)會(huì)放到github進(jìn)行公開
3、Aspects作為優(yōu)秀的AOP庫(kù),但由于時(shí)間過于悠久,執(zhí)行效率有些低下,后期有機(jī)會(huì)可以使用Stinger進(jìn)行重構(gòu)

不足之處請(qǐng)各位踴躍指出,有需要的同學(xué)可以向筆者詢問具體源碼,大家共同努力完善

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容