Demo:https://github.com/cjckkk/PPpatch (by myself)
輕量級低風險 iOS Hotfix 方案
我們都知道蘋果對 Hotfix 抓得比較嚴官册,強大好用的 JSPatch 也成為了過去式深滚。但即使測試地再細致,也難保線上 App 不出問題玷或,小問題還能忍忍敌完,大問題就得重新走發(fā)布流程,然后等待審核通過,等待用戶升級,周期長且麻煩刃宵。如果有一種方式相對比較安全,不需要 JSPatch 那么完善徘公,但也足夠應付一般場景,使用起來還比較輕量就好了哮针,這也是本文要探討的主題关面。
要達到這個目的,Native 層只要透出兩種能力就基本可以了:
- 在任意方法前后注入代碼的能力十厢,可能的話最好還能替換掉等太。
- 調用任意類/實例方法的能力。
第 2 點不難蛮放,只要把 [NSObject performSelector:...]
那一套通過 JSContext
暴露出來即可缩抡。難的是第 1 點。其實細想一下包颁,這不就是 AOP 么瞻想,而 iOS 有一個很方便的 AOP Library: Aspects,只要把它的幾個方法通過 JSContext 暴露給 JS 不就可以了么娩嚼?
選擇 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普碎,內部動態(tài)構造 Class 和 Selector 來達到 Fix 的效果了。
這種方式的安全性在于:
- 不需要中間 JS 文件录平,準備工作全部在 Native 端完成随常。
- 沒有使用 App Store 不友好的類/方法潜沦。
Demo
假設線上運行這這樣一個 Class,由于疏忽绪氛,沒有對參數(shù)做檢查唆鸡,導致特定情況下會 Crash。
@interface MightyCrash: NSObject
- (float)divideUsingDenominator:(NSInteger)denominator;
@end
@implementation MightyCrash
// 傳一個 0 就 gg 了
- (float)divideUsingDenominator:(NSInteger)denominator
{
return 1.f/denominator;
}
@end
現(xiàn)在我們要避免 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
--EOF--
轉載
http://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html