iOS使用Aspects做簡(jiǎn)單熱修復(fù)原理

用到的技術(shù)點(diǎn):Method Swizzling + JavaScriptCore框架

本文Demo: https://github.com/3KK3/iOSHotFixDemo

結(jié)合Demo食用最佳 ~


案例預(yù)備:

首先假如我們項(xiàng)目中寫(xiě)有如下代碼:

MightyCrash *mc = [[MightyCrash alloc] init];

[mc divideUsingDenominator: 0];

實(shí)例化一個(gè)對(duì)象,然后調(diào)用對(duì)象的一個(gè)方法,這個(gè)方法內(nèi)部實(shí)現(xiàn)如下:

- (float)divideUsingDenominator:(NSInteger)denominator {
    return 1.f / denominator;
}

問(wèn)題所在:因?yàn)檎{(diào)用方法時(shí)候我們傳入的值為0,所以除以0會(huì)出現(xiàn)問(wèn)題


熱修復(fù)解決:

在Appdelegate實(shí)現(xiàn)文件中添加如下代碼即可:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [Felix fixIt];
    
    NSString *fixScriptString = @" \
    fixInstanceMethodReplace('MightyCrash', 'divideUsingDenominator:', function(instance, originInvocation, originArguments){ \
        if (originArguments[0] == 0) { \
            console.log('zero goes here'); \
        } else { \
            runInvocation(originInvocation); \
        } \
    }); \
    \
    ";
    
    [Felix evalString:fixScriptString];
    
    return YES;
}

原理分析:

  1. 首先第一步的[Felix fixIt];執(zhí)行結(jié)果,會(huì)生成一個(gè)全局的JSContext對(duì)象來(lái)為執(zhí)行JS方法提供環(huán)境:
+ (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;
}

同時(shí),使用匿名函數(shù)的方式包裝OC方法:

[self context][@"fixInstanceMethodBefore"] = ^(NSString *instanceName, NSString *selectorName, JSValue *fixImpl) {
    [self _fixWithMethod:NO aspectionOptions:AspectPositionBefore instanceName:instanceName selectorName:selectorName fixImpl:fixImpl];
};

此時(shí)的模型可以理解為:

圖1.0

包裝的OC方法是什么呢?我們來(lái)看實(shí)現(xiàn):

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

可以看出,該OC方法是通過(guò)Aspects框架來(lái)實(shí)現(xiàn)hack

  1. 然后看Appdelegate的第二步:
NSString *fixScriptString = @" \
fixInstanceMethodReplace('MightyCrash', 'divideUsingDenominator:', function(instance, originInvocation, originArguments){ \
        if (originArguments[0] == 0) { \
            console.log('zero goes here'); \
        } else { \
            runInvocation(originInvocation); \
        } \
    }); \
    \
 ";

聲明一個(gè)字符串fixScriptString,該字符串其實(shí)是一個(gè)JS方法,傳入3個(gè)參數(shù)

  • 方法傳入的參數(shù)1是MightyCrash類,即我們要來(lái)hack的目標(biāo)類
  • 方法傳入的參數(shù)2是divideUsingDenominator,即我們要來(lái)hack的目標(biāo)類的方法
  • 方法傳入的參數(shù)1是一個(gè)function,即我們要替換的方法實(shí)現(xiàn)
  1. 第三步[Felix evalString:fixScriptString];

執(zhí)行后,會(huì)在全局的JSContent環(huán)境中, 執(zhí)行下面綠色部分fixInstanceMethodReplaceJS方法, 而綠色部分其實(shí)是對(duì)OC方法的封裝,所以實(shí)質(zhì)是對(duì)紅色部分OC方法的調(diào)用

圖1.1

紅色部分OC方法使用Method Swizzling黑魔法完成了目標(biāo)執(zhí)行函數(shù)的替換,即MightyCrash類中的- (float)divideUsingDenominator:(NSInteger)denominator;方法替換為 [fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];

  1. 第四步

當(dāng)執(zhí)行

    MightyCrash *mc = [[MightyCrash alloc] init];
    [mc divideUsingDenominator:1];

這段代碼的時(shí)候,因?yàn)榈谌轿覀円呀?jīng)替換了方法實(shí)現(xiàn),所以其實(shí)是調(diào)用了[fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];方法,該方法會(huì)執(zhí)行第二步聲明的JS方法的第三個(gè)function參數(shù),即執(zhí)行:

function(instance, originInvocation, originArguments){ 
        if (originArguments[0] == 0) { 
            console.log('zero goes here'); 
        } else { 
            runInvocation(originInvocation); 
        } 

總結(jié):

上面案例是在Appdeelgate文件中固定寫(xiě)死一個(gè)JS方法(字符串),在實(shí)際項(xiàng)目應(yīng)用中,寫(xiě)死的JS方法可以通過(guò)從服務(wù)器請(qǐng)求來(lái)獲取,已達(dá)到動(dòng)態(tài)修復(fù)線上bug的目的。


參考文章:
http://www.itdecent.cn/p/ac534f508fb0
https://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html
復(fù)雜情況修復(fù):
http://www.itdecent.cn/p/d4574a4268b3


最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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