輕量級(jí)低風(fēng)險(xiǎn) iOS Hotfix 方案(附DEMO)

Demo:https://github.com/cjckkk/PPpatch (by myself)

輕量級(jí)低風(fēng)險(xiǎn) iOS Hotfix 方案

我們都知道蘋(píng)果對(duì) Hotfix 抓得比較嚴(yán),強(qiáng)大好用的 JSPatch 也成為了過(guò)去式。但即使測(cè)試地再細(xì)致,也難保線上 App 不出問(wèn)題,小問(wèn)題還能忍忍,大問(wèn)題就得重新走發(fā)布流程,然后等待審核通過(guò),等待用戶(hù)升級(jí),周期長(zhǎng)且麻煩。如果有一種方式相對(duì)比較安全,不需要 JSPatch 那么完善,但也足夠應(yīng)付一般場(chǎng)景,使用起來(lái)還比較輕量就好了,這也是本文要探討的主題。

要達(dá)到這個(gè)目的,Native 層只要透出兩種能力就基本可以了:

  1. 在任意方法前后注入代碼的能力,可能的話最好還能替換掉。
  2. 調(diào)用任意類(lèi)/實(shí)例方法的能力。

第 2 點(diǎn)不難,只要把 [NSObject performSelector:...] 那一套通過(guò) JSContext 暴露出來(lái)即可。難的是第 1 點(diǎn)。其實(shí)細(xì)想一下,這不就是 AOP 么,而 iOS 有一個(gè)很方便的 AOP Library: Aspects,只要把它的幾個(gè)方法通過(guò) JSContext 暴露給 JS 不就可以了么?

選擇 Aspects 的原因是它已經(jīng)經(jīng)過(guò)了驗(yàn)證,不光是功能上的,更重要的是可以通過(guò) 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 使用姿勢(shì):

[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];

前插、后插、替換某個(gè)方法都可以。使用類(lèi)的方式很簡(jiǎn)單,NSClassFromString 即可,Selector 也一樣 NSSelectorFromString,這樣就能通過(guò)外部傳入 String,內(nèi)部動(dòng)態(tài)構(gòu)造 Class 和 Selector 來(lái)達(dá)到 Fix 的效果了。

這種方式的安全性在于:

  1. 不需要中間 JS 文件,準(zhǔn)備工作全部在 Native 端完成。
  2. 沒(méi)有使用 App Store 不友好的類(lèi)/方法。

Demo

假設(shè)線上運(yùn)行這這樣一個(gè) Class,由于疏忽,沒(méi)有對(duì)參數(shù)做檢查,導(dǎo)致特定情況下會(huì) Crash。

@interface MightyCrash: NSObject
- (float)divideUsingDenominator:(NSInteger)denominator;
@end

@implementation MightyCrash
// 傳一個(gè) 0 就 gg 了
- (float)divideUsingDenominator:(NSInteger)denominator
{
    return 1.f/denominator;
}
@end

現(xiàn)在我們要避免 Crash,就可以通過(guò)這種方式來(lái)修復(fù)

[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];

運(yùn)行一下看看

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, 是不是有那么點(diǎn)意思了。以下是可以正常運(yùn)行的代碼,僅供參考。


#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--

轉(zhuǎn)載
http://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html

最后編輯于
?著作權(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)容