美文网首页
Aspects扫盲

Aspects扫盲

作者: 李永开 | 来源:发表于2022-08-17 20:03 被阅读0次

一. 什么是Aspects

[ˈæspekts] n. 方面;方位;面貌(aspect 的复数)
Aspects是一个轻量的、简单的,面向切面(AOP - Aspect Oriented Programming)的library。

二. 怎么使用Aspects

2.0 先看下头文件

头文件很简单,提供了一个类方法和实例方法

  • 类方法:hook所有该类的实例对象的方法
  • 实例方法:只hook该实例的方法

参数总共有4个:
AspectPositionAfter && AspectPositionBefore: 比较简单,选择调用block的时机是在原方法之前或者之后
AspectOptionAutomaticRemoval: 只hook一次
AspectPositionInstead:替换原来的实现方法,其实是不调用以前的方法

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                           withOptions:(AspectOptions)options
                            usingBlock:(id)block
                                 error:(NSError **)error;

2.1 简单试下,hook下viewcontroller的viewWillAppear方法,好使
    [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];
2.2 试试自定义的方法🤥没问题
@interface AspectsDemo : NSObject
- (void)aspectTest;
@end

@implementation AspectsDemo
- (void)aspectTest {
    NSLog(@"%@", NSStringFromSelector(_cmd));//aspects__aspectTest
}
@end

//调用下,没问题
[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"%@", aspectInfo);
    } error:NULL];
[[[AspectsDemo alloc] init] aspectTest];
2.3 先调用,后hook试试, 啥也没发生👌🏻
@implementation AspectsDemo
- (void)aspectTest {
    printf(__func__);//-[AspectsDemo aspectTest]
}
@end

[[[AspectsDemo alloc] init] aspectTest];
    [AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"%@", aspectInfo);
} error:NULL];
2.4 多次hook试试

两个block都走了,也没啥问题 😲

    [AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"%@", aspectInfo);
    } error:NULL];
    [AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"%@", aspectInfo);
    } error:NULL];
    [[[AspectsDemo alloc] init] aspectTest];
2.5 试试实例方法

没问题☹️
只hook了demo1, demo2不受影响,很棒

AspectsDemo *demo1 = [[AspectsDemo alloc] initWithSting:@"666"];
[demo1 aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
    NSLog(@"%@", aspectInfo);
} error:NULL];
[demo1 aspectTest];

AspectsDemo *demo2 = [[AspectsDemo alloc] initWithSting:@"777"];
[demo2 aspectTest];
2.6 试试AspectPositionInstead参数

优秀啊,使用AspectPositionInstead原方法就不走了

[AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo){
    NSLog(@"%@", aspectInfo);
} error:NULL];
[[[AspectsDemo alloc] init] aspectTest];

2.7 hook init方法试试

没问题

@implementation AspectsDemo
- (instancetype)initWithSting:(NSString *)string {
    NSLog(@"%@",string);
    if (self = [super init]) {
        self.string = string;
    }
    return self;
}

[AspectsDemo aspect_hookSelector:@selector(initWithSting:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo, NSString *str){
        NSLog(@"%@", aspectInfo);
    } error:NULL];
[[AspectsDemo alloc] initWithSting:@"666"];
2.8 hook init方法, 并且多传入几个参数试试

GG了GG了😮😮😮😮😮😮,多传了id a, id b,参数对不上,就不走block了

[AspectsDemo aspect_hookSelector:@selector(initWithSting:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo,id a, id b){
    NSLog(@"%@", aspectInfo);
} error:NULL];
[[AspectsDemo alloc] initWithSting:@"666"];
2.9 hook 系统的init方法试试

😮😮😮😮😮😮😮😮😮😮😮😮不行了😮😮😮😮😮😮😮😮哈哈哈😮😮😮😮😮,不走block了

[NSString aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
        NSLog(@"%@", aspectInfo);
    } error:NULL];
[[NSString alloc] initWithString:@"666"];

所以我们需要研究一下一下问题

  1. 入参只能少,不能多,多的话hook失败
  2. hook 系统的init方法失败

三. 研究下源码

  1. 返回一个id类型对象,遵守@protocol AspectToken <NSObject>协议,可以使用remove方法撤销hook
    id ret = [AspectsDemo aspect_hookSelector:@selector(aspectTest) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
        NSLog(@"%@", aspectInfo);
    } error:NULL];
    [[[AspectsDemo alloc] init] aspectTest];
    [ret remove];
  1. 判断这个类是否能hook
// 这4个方法不能hook
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];

//dealloc可以hook,但是只能在调用之前hook
[selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore

// 自省,不响应的不hook
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) 

//静态变量,记录哪些类被hook了
static NSMutableDictionary *swizzledClassesDict  
// 子类的这个方法被hook过了, 就不hook
[tracker subclassHasHookedSelectorName:selectorName]
// 父类被hook过了, 就不hook
[tracker.selectorNames containsObject:selectorName]
  1. 检测方法签名,验证我们传入的block是否合法
//如果方法签名的参数个数大于原方法的参数个数,不走下面的逻辑了
    NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
    if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
      return NO};
  1. 通过以上测试,开始真正hook的部分
// 替换原来的imp,改成自己的imp
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
// 给类增加一个新的方法,imp为原来的实现方法
if (originalImplementation) {
    class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}

// 这个函数就是自己的imp,内部会先调用自己的block
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {}

    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // 如果是替换,就不调用原方法了
    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);

    //先调用原方法,在调用hook的方法
    if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
    }
    // After hooks.
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

四. 解决问题

  • 2.8 hook init方法, 并且多传入几个参数试试
    这个问题看源码已经解决了,是因为我们block传入的参数个数大于原方法入参个数
  • 2.9 hook 系统的init方法试试
    这个问题打断点,发现originalImplementation为nil,所以没有走接下来的hook流程
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }

经过百度,其实是因为NSString *realString = [[NSString alloc] initWithString:@"666"];里面的NSString, 在真正创建的时候,用的其实是它的子类,可以参考这篇文章

(lldb) po realString.class
__NSCFConstantString

(lldb) po realString.superclass
__NSCFString

(lldb) po realString.superclass.superclass
NSMutableString

(lldb) po realString.superclass.superclass.superclass
NSString

所以, 我们应该这样hook

[[NSString alloc].class aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
        NSLog(@"%@", aspectInfo);
} error:NULL];
NSString *realString = [[NSString alloc] initWithString:@"666"];

同理,对NSAttributedString的hook也是如此,因为NSAttributedString真正使用的也是其簇类。

五. 总结

5.0 什么是Aspects
  • Aspects使用runtime的class_replaceMethod方法交换类的forwardInvocation方法,并用block将相关信息传递到调用处的一个AOP框架。
5.1 使用Aspects注意事项
  1. 不能hook @"retain", @"release", @"autorelease", @"forwardInvocation
  2. 可以hook dealloc方法,但是只能在调用之前hook
  3. block入参内部放法签名的参数个数大于原方法的参数个数,会hook失败
  4. 对于NSString等拥有簇类的类,需要使用其真正的类名去调用hook方法
5.2 如何看到Aspects
  1. github上作者说了,不建议在生产环境部署Aspects,因为其内部的实现不能保证在未来也是100%安全。
  2. 使用Method Swizzle也可以做到方法交换, 而且很容易在代码层次做校验判断。 Aspects则不太容易做到替换参数等,不是很直观。
// Method Swizzle
- (instancetype)safe_initWithString:(NSString *)aString{
    return [self safe_initWithString:aString?:@""];
}

//Aspects
[[NSString alloc].class aspect_hookSelector:@selector(initWithString:) withOptions:0 usingBlock:^(id<AspectInfo> aspectInfo) {
    需要在这里做判断,替换参数
} error:NULL];
NSString *realString = [[NSString alloc] initWithString:@"666"];
  1. Aspects hook分散在各个地方,同一个hook可以被执行多次(我测试的),容易产生bug。 当然Method Swizzle也会有这样的问题,建议将hook的代码收拢到一起,便于管理和排查。

六. 待解决的问题

6.1 多次hook依然能走的问题
6.2 实例方法的hook和类的hook是否一致

Final. 学到的知识点:

objc_getAssociatedObject 直接用SEL aliasSelector,

OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

说明SEL是个const void *
SEL aliasSelector = aspect_aliasForSelector(selector);
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);

相关文章

网友评论

      本文标题:Aspects扫盲

      本文链接:https://www.haomeiwen.com/subject/babrgrtx.html