輕量級低風險 iOS 熱更新方案
點選上方“iOS開發”,選擇“置頂公眾號”
關鍵時刻,第一時間送達!
我們都知道蘋果對 Hotfix 抓得比較嚴,強大好用的 JSPatch 也成為了過去式。但即使測試地再細緻,也難保線上 App 不出問題,小問題還能忍忍,大問題就得重新走釋出流程,然後等待稽核通過,等待使用者升級,週期長且麻煩。如果有一種方式相對比較安全,不需要 JSPatch 那麼完善,但也足夠應付一般場景,使用起來還比較輕量就好了,這也是本文要探討的主題。
要達到這個目的,Native 層只要透出兩種能力就基本可以了:
在任意方法前後注入程式碼的能力,可能的話最好還能替換掉。
呼叫任意類/例項方法的能力。
第 2 點不難,只要把 [NSObject performSelector:...] 那一套通過 JSContext 暴露出來即可。難的是第 1 點。其實細想一下,這不就是 AOP 麼,而 iOS 有一個很方便的 AOP Library: Aspects
選擇 Aspects 的原因是它已經經過了驗證,不光是功能上的,更重要的是可以通過 AppStore 的稽核。
This is stable and used in hundreds of apps since it’s part of PSPDFKit, an iOS PDF framework that ships with apps like Dropbox or Evernote.
Aspects 使用姿勢:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
前插、後插、替換某個方法都可以。使用類的方式很簡單,NSClassFromString 即可,Selector 也一樣 NSSelectorFromString,這樣就能通過外部傳入 String,內部動態構造 Class 和 Selector 來達到 Fix 的效果了。
這種方式的安全性在於:
不需要中間 JS 檔案,準備工作全部在 Native 端完成。
沒有使用 App Store 不友好的類/方法。
Demo
假設線上執行這這樣一個 Class,由於疏忽,沒有對引數做檢查,導致特定情況下會 Crash。
@interface MightyCrash: NSObject
- (float)divideUsingDenominator:(NSInteger)denominator;
@end
@implementation MightyCrash
// 傳一個 0 就 gg 了
- (float)divideUsingDenominator:(NSInteger)denominator
{
return 1.f/denominator;
}
@end
現在我們要避免 Crash,就可以通過這種方式來修復
[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];
執行一下看看
MightyCrash *mc = [[MightyCrash alloc] init];
float result = [mc divideUsingDenominator:3];
NSLog(@"result: %.3f", result);
result = [mc divideUsingDenominator:0];
NSLog(@"won't crash");
// output
// result: 0.333
// Javascript log: zero goes here
// won't crash
It Works, 是不是有那麼點意思了。以下是可以正常執行的程式碼,僅供參考。
#import <Aspects.h>
#import <objc/runtime.h>
#import <JavaScriptCore/JavaScriptCore.h>
@interface Felix: NSObject
+ (void)fixIt;
+ (void)evalString:(NSString *)javascriptString;
@end
@implementation Felix
+ (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 obj1:(id)obj1 obj2:(id)obj2 {
Class klass = NSClassFromString(className);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [klass performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (id)_runInstanceWithInstance:(id)instance selector:(NSString *)selector obj1:(id)obj1 obj2:(id)obj2 {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [instance performSelector:NSSelectorFromString(selector) withObject:obj1 withObject:obj2];
#pragma clang diagnostic pop
}
+ (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][@"runClassWithNoParamter"] = ^id(NSString *className, NSString *selectorName) {
return [self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runClassWith1Paramter"] = ^id(NSString *className, NSString *selectorName, id obj1) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runClassWith2Paramters"] = ^id(NSString *className, NSString *selectorName, id obj1, id obj2) {
return [self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidClassWithNoParamter"] = ^(NSString *className, NSString *selectorName) {
[self _runClassWithClassName:className selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidClassWith1Paramter"] = ^(NSString *className, NSString *selectorName, id obj1) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidClassWith2Paramters"] = ^(NSString *className, NSString *selectorName, id obj1, id obj2) {
[self _runClassWithClassName:className selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInstanceWithNoParamter"] = ^id(id instance, NSString *selectorName) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runInstanceWith1Paramter"] = ^id(id instance, NSString *selectorName, id obj1) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runInstanceWith2Paramters"] = ^id(id instance, NSString *selectorName, id obj1, id obj2) {
return [self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runVoidInstanceWithNoParamter"] = ^(id instance, NSString *selectorName) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:nil obj2:nil];
};
[self context][@"runVoidInstanceWith1Paramter"] = ^(id instance, NSString *selectorName, id obj1) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:nil];
};
[self context][@"runVoidInstanceWith2Paramters"] = ^(id instance, NSString *selectorName, id obj1, id obj2) {
[self _runInstanceWithInstance:instance selector:selectorName obj1:obj1 obj2:obj2];
};
[self context][@"runInvocation"] = ^(NSInvocation *invocation) {
[invocation invoke];
};
// helper
[[self context] evaluateScript:@"var console = {}"];
[self context][@"console"][@"log"] = ^(id message) {
NSLog(@"Javascript log: %@",message);
};
}
@end
作者: Limboy’s HQ
連結:http://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html
iOS開發整理髮布,轉載請聯絡作者授權