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

265 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aspects
> Aspects 核心原理涉及3个技术点
>
> - objc_msgForward(触发消息转发机制)
> - NSInvocation
> - block 的本质
## 函数指针、指针函数的区别
定义不同:
- 函数指针:本质是一个指针,该指针指向一个函数
- 指针函数:本质是一个函数,函数的返回值是一个指针类型
写法不同
- 函数指针:`int (*fun)(int x,int y)`
- 指针函数:`int* fun(int x,int y)`
用法不同:
- 函数指针
```c++
typedef int (*FuncPtr)(int, int);
int add(int a, int b) {
return a + b;
}
FuncPtr addPtr = add;
int result = addPtr(3, 2);
```
- 指针函数
```c++
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 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)。接下去查看建议版本的分析。
第一步:编写一个基础 block
第二步:用 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp` 转成 c++ 查看原理
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockExplore.png" style="zoom:30%" />
分析:可以发现 block 本质就是结构体,和 OC 对象一样,也有 isa 指针。block 传递进去的方法,被包装成 block 的成员变量,是一个叫做 FuncPtr 的函数指针了。
是不是我们可以按照系统定义,来构造一个 struct承接一个 block然后发起调用呢
```objective-c
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");
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectMockBlock.png" style="zoom:30%" />
思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block
## NSInvocation 触发 block
一个方法需要成功调用并执行需要3要素
- 方法名称 `SEL`
- 方法签名(参数个人、参数类型、返回值类型等信息) `Method Type Encoding`
- 方法地址、方法实现 `IMP`
如何从自定义的 block 结构体中获取这些信息呢?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcess.png" style="zoom:60%" />
AspectsIdentifier每做一次方法交换都会转换为一次 AspectsIdentifier。是核心逻辑。
以一个例子作为源码探索切入口,点击跳转到源码中
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsIdentifierModule.png" style="zoom:30%" />
可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法
```objective-c
+ (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 签名。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockSignature.png" style="zoom:30%" />
第二步:因为我们通过 AOP 给原始方法添加了 block最后的效果是既可以调用原始方法又可以调用 block 添加的代码。实现的前提是什么?
比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsSignatureCompare.png" style="zoom:30%" />
```shell
(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
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectInvokeBlock.png" style="zoom:30%" />
## objc_msgForward
骚操作:
- 将待 hook 的方法,和 `objc_msgForward` 进行交换。 `objc_msgForward` 不管对象有没有实现,都会触发消息转发流程
- 此时会走 Runtime 的 NSObject `forwardInvocation` 流程。且 Aspects 将 `forwardInvocation` 方法指向了 `__ASPECTS_ARE_BEING_CALLED__` 方法。
经历这么一波处理hook 最后都守口到了 `__ASPECTS_ARE_BEING_CALLED__` 方法中。
前面研究过了 `AspectIdentifier` 的逻辑。接下去继续看看后续步骤。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookForwardInvocation.png" style="zoom:30%" />
可以看到内部执行 `aspect_prepareClassAndHookSelector`,其内部会调用 `aspect_hookClass`,又会调用 `aspect_swizzleClassInPlace`,最后调用 `aspect_swizzleForwardInvocation` 方法。
```objective-c
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:`。
回归头继续看下面的逻辑。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookMethodWithObjcMsgForward.png" style="zoom:30%" />
`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 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcessXmind.png" style="zoom:30%" />