用到的技術(shù)點(diǎn):Method Swizzling + JavaScriptCore框架
本文Demo: https://github.com/3KK3/iOSHotFixDemo
結(jié)合Demo食用最佳 ~
案例預(yù)備:
首先假如我們項(xiàng)目中寫有如下代碼:
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;
}
問題所在:因?yàn)檎{(diào)用方法時(shí)候我們傳入的值為0扫尺,所以除以0會(huì)出現(xià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;
}
原理分析:
- 首先第一步的
[Felix fixIt];
執(zhí)行結(jié)果流椒,會(huì)生成一個(gè)全局的JSContext
對(duì)象來為執(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í)的模型可以理解為:
包裝的OC方法是什么呢奋隶?我們來看實(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方法是通過Aspects框架來實(shí)現(xiàn)hack
- 然后看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
類,即我們要來hack的目標(biāo)類 - 方法傳入的參數(shù)2是
divideUsingDenominator
奔垦,即我們要來hack的目標(biāo)類的方法 - 方法傳入的參數(shù)1是一個(gè)
function
屹耐,即我們要替換的方法實(shí)現(xiàn)
- 第三步
[Felix evalString:fixScriptString];
執(zhí)行后,會(huì)在全局的JSContent環(huán)境中, 執(zhí)行下面綠色部分fixInstanceMethodReplace
JS方法椿猎, 而綠色部分其實(shí)是對(duì)OC方法的封裝张症,所以實(shí)質(zhì)是對(duì)紅色部分OC方法的調(diào)用
紅色部分OC方法使用Method Swizzling黑魔法完成了目標(biāo)執(zhí)行函數(shù)的替換,即MightyCrash
類中的- (float)divideUsingDenominator:(NSInteger)denominator;
方法替換為 [fixImpl callWithArguments:@[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];
- 第四步
當(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文件中固定寫死一個(gè)JS方法(字符串)兆衅,在實(shí)際項(xiàng)目應(yīng)用中,寫死的JS方法可以通過從服務(wù)器請(qǐng)求來獲取嗜浮,已達(dá)到動(dòng)態(tài)修復(fù)線上bug的目的羡亩。
參考文章:
http://www.reibang.com/p/ac534f508fb0
https://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html
復(fù)雜情況修復(fù):
http://www.reibang.com/p/d4574a4268b3