iOS利用Aspects做简单热修复原理

开发者 2024-9-30 20:57:59 24 0 来自 中国
我们都知道苹果对 Hotfix 抓得比力严,强盛好用的 JSPatch 也成为了已往式。但纵然测试地再过细,也难保线上 App 不出标题,小标题还能忍忍,大标题就得重新走发布流程,然后等待考核通过,等待用户升级,周期长且贫苦。如果有一种方式相对比力安全,不必要 JSPatch 那么完满,但也富足应付一样寻常场景,利用起来还比力轻量就好了,这也是本文要探究的主题。
要到达这个目的,Native 层只要透出两种本事就根本可以了:
1.在恣意方法前后注入代码的本事,大概的话最好还能更换掉。
2.调用恣意类/实例方法的本事。
第 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_hookSelectorselector(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)divideUsingDenominatorNSInteger)denominator;@end@implementation MightyCrash// 传一个 0 就 gg 了- (float)divideUsingDenominatorNSInteger)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 crashIt Works, 是不是有那么点意思了。以下是可以正常运行的代码,仅供参考。
#import <Aspects.h>#import <objc/runtime.h>#import <JavaScriptCore/JavaScriptCore.h>@interface Felix: NSObject+ (void)fixIt;+ (void)evalStringNSString *)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)evalStringNSString *)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)_fixWithMethodBOOL)isClassMethod aspectionOptionsAspectOptions)option instanceNameNSString *)instanceName selectorNameNSString *)selectorName fixImplJSValue *)fixImpl {    Class klass = NSClassFromString(instanceName);    if (isClassMethod) {        klass = object_getClass(klass);    }    SEL sel = NSSelectorFromString(selectorName);    [klass aspect_hookSelector:sel withOptionsption usingBlock:^(id<AspectInfo> aspectInfo){        [fixImpl callWithArguments[aspectInfo.instance, aspectInfo.originalInvocation, aspectInfo.arguments]];    } error:nil];}+ (id)_runClassWithClassNameNSString *)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) withObjectbj1 withObjectbj2];#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) withObjectbj1 withObjectbj2];#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 obj1bj1 obj2:nil];    };    [self context][@"runClassWith2Paramters"] = ^id(NSString *className, NSString *selectorName, id obj1, id obj2) {        return [self _runClassWithClassName:className selector:selectorName obj1bj1 obj2bj2];    };    [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 obj1bj1 obj2:nil];    };    [self context][@"runVoidClassWith2Paramters"] = ^(NSString *className, NSString *selectorName, id obj1, id obj2) {        [self _runClassWithClassName:className selector:selectorName obj1bj1 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参考文章:
https://www.jianshu.com/p/ac534f508fb0
https://limboy.me/tech/2018/03/04/ios-lightweight-hotfix.html
https://www.jianshu.com/p/d7b24016854e
复杂情况修复:
https://www.jianshu.com/p/d4574a4268b3
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-11-23 16:22, Processed in 0.139470 second(s), 32 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表