Files
knowledge-kit/Chapter1 - iOS/1.140.md
2024-07-15 20:03:01 +08:00

9.0 KiB
Raw Blame History

Aspects

Aspects 核心原理涉及3个技术点

  • objc_msgForward(触发消息转发机制)
  • NSInvocation
  • block 的本质

函数指针、指针函数的区别

定义不同:

  • 函数指针:本质是一个指针,该指针指向一个函数
  • 指针函数:本质是一个函数,函数的返回值是一个指针类型

写法不同

  • 函数指针:int (*fun)(int x,int y)
  • 指针函数:int* fun(int x,int y)

用法不同:

  • 函数指针

    typedef int (*FuncPtr)(int, int);
    
    int add(int a, int b) {
        return a + b;
    }
    
    FuncPtr addPtr = add;
    int result = addPtr(3, 2);
    
  • 指针函数

    int (*getAddFunction())(int, int) {
        return add;
    }
    
    int add(int a, int b) {
        return a + b;
    }
    
    int (*addPtr)(int, int) = getAddFunction();
    int result = addPtr(3, 2);
    

block 本质

block 详细探索步骤请查看这篇文章 block 底层原理。接下去查看建议版本的分析。

第一步:编写一个基础 block

第二步:用 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp 转成 c++ 查看原理

分析:可以发现 block 本质就是结构体,和 OC 对象一样,也有 isa 指针。block 传递进去的方法,被包装成 block 的成员变量,是一个叫做 FuncPtr 的函数指针了。

是不是我们可以按照系统定义,来构造一个 struct承接一个 block然后发起调用呢

typedef NS_OPTIONS(int, AspectBlockFlags) {
    AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
    AspectBlockFlagsHasSignature = (1 << 30)
};

typedef struct AspectBlock {
    __unused Class isa;
    AspectBlockFlags Flags;
    __unused int Reserved;
    void (__unused *invoke)(struct AspectBlock *block, ...);

    struct {
        size_t reserved;
        size_t Block_size;
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
    } *descriptor;
} *AspectBlockRef;

void(^printBlock)(NSString *) = ^void(NSString *msg) {
    NSLog(@"%@", msg);
};
printBlock(@"Hello world");

struct AspectBlock *fakeBlock = (__bridge struct AspectBlock *)printBlock;
((void (*)(void *, NSString *))fakeBlock->invoke)(fakeBlock, @"Hello world");

思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block

NSInvocation 触发 block

一个方法需要成功调用并执行需要3要素

  • 方法名称 SEL
  • 方法签名(参数个人、参数类型、返回值类型等信息) Method Type Encoding
  • 方法地址、方法实现 IMP

如何从自定义的 block 结构体中获取这些信息呢?

AspectsIdentifier每做一次方法交换都会转换为一次 AspectsIdentifier。是核心逻辑。

以一个例子作为源码探索切入口,点击跳转到源码中

可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法

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

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

内部都走到 aspect_add 方法中。其中都会生成 AspectIdentifier ,看看是如何生成的?

第一步:生成 block 签名。

第二步:因为我们通过 AOP 给原始方法添加了 block最后的效果是既可以调用原始方法又可以调用 block 添加的代码。实现的前提是什么?

比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。

(lldb) po blockSignature
<NSMethodSignature: 0x600003829c20>
    number of arguments = 2
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?'
        flags {isObject, isBlock}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (@) '@"<AspectInfo>"'
        flags {isObject}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
            conforms to protocol 'AspectInfo'

OC 方法签名和 block 方法签名是有区别的

  • 比如在 Aspects 框架中block 方法签名的参数个数是比 oc 方法参数个数少的。oc 方法自带 id self, SEL _cmd 2个参数。
  • 都使用相同的类型编码系统,但是 block 签名可能包含额外的信息,例如捕获的变量类型。比如上面的 block 方法签名的最后一个参数 '@"<AspectInfo>"',其中的 AspectInfo 就代表捕获的变量类型。

比如2个不带参数的 OC 方法签名和 block 方法签名:

  • oc 方法签名:v@: = v + @ + :,返回值 void、参数1 @ 代表对象、参数2 代表 SEL 类型

  • block 方法签名:v@? = v + @?,返回值 void、参数1 @? 代表既是对象,又是 block

objc_msgForward

骚操作:

  • 将待 hook 的方法,和 objc_msgForward 进行交换。 objc_msgForward 不管对象有没有实现,都会触发消息转发流程
  • 此时会走 Runtime 的 NSObject forwardInvocation 流程。且 Aspects 将 forwardInvocation 方法指向了 __ASPECTS_ARE_BEING_CALLED__ 方法。

经历这么一波处理hook 最后都守口到了 __ASPECTS_ARE_BEING_CALLED__ 方法中。

前面研究过了 AspectIdentifier 的逻辑。接下去继续看看后续步骤。

可以看到内部执行 aspect_prepareClassAndHookSelector,其内部会调用 aspect_hookClass,又会调用 aspect_swizzleClassInPlace,最后调用 aspect_swizzleForwardInvocation 方法。

static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
    NSCParameterAssert(klass);
    // If there is no method, replace will act like class_addMethod.
    IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
    if (originalImplementation) {
        class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
    }
    AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}

该方法将被 hook 对象的 forwardInvocation: 方法替换为 __aspects_forwardInvocation:

回归头继续看下面的逻辑。

class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding) 可以实现将被 hook 类的 hook 方法,替换为 _objc_msgForward 或者某些版本下需要的 _objc_msgForward_stret

比如 ViewController 的 viewWillAppear hook 流程就是:

[UIViewController viewWillAppear:] -> _objc_msgForwar -> [UIViewController forwardInvocation:] -> __ASPECTS_ARE_BEING_CALLED__

总结

Aspects 是 Runtime 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。