25

BlockHook with Invocation

 4 years ago
source link: https://www.tuicool.com/articles/iIVBBni
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

BlockHook 在业界已经率先解决了在 同步 调用场景下对 Objective-C Block 的 AOP 问题,在很多场景需要先调用一段自己的逻辑,然后再 异步延时 执行 Block。

比如从外部跳转到 App 某个页面前需要检查下登录态,如果未登录则需要走完登录流程后才能继续跳转页面,而几乎所有基于 Block callback 的路由组件都没提供路由拦截器的功能。不同的路由组件内部实现不同,想要实现拦截器就需要针对不同的内部实现来修改路由组件源码。

因此我实现了 BlockHook 的异步拦截功能,所有基于 Block 的路由组件就都有了通用的路由拦截器!

当然,Block 拦截器的应用场景不仅于此。只要是需要『同步改异步执行』 Block 的场景都可以用到。

让子弹再飞一会儿!

使用方法

BlockHook 拦截器用法很简单,在已有 BHInvocation 参数的基础上,增加了一个 completion 回调。当拦截器的逻辑异步执行完后,调用 completion 即可继续执行原来的 Block。如果拦截器的逻辑是同步的,也依然可以用这个接口,只是没必要罢了,推荐直接用原来的 block_hookWithMode:usingBlock: 接口。

typedef void(^IntercepterCompletion)(void);

/**
 Interceptor for blocks. When your interceptor completed, call `completion` callback.
 You can call `completion` asynchronously!

 @param interceptor You **MUST** call `completion` callback in interceptor, unless you want to cancel invocation.
 @return BHToken instance.
 */
- (BHToken *)block_interceptor:(void (^)(BHInvocation *invocation, IntercepterCompletion completion))interceptor;

举个例子,拦截时修改传入的参数,并延迟 0.5 秒再执行 Block:

NSObject *testArg = [NSObject new];
NSObject *testArg1 = [NSObject new];
    
NSObject *(^testblock)(NSObject *) = ^(NSObject *a) {
    return [NSObject new];
};
    
[testblock block_interceptor:^(BHInvocation *invocation, IntercepterCompletion  _Nonnull completion) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        *(void **)(invocation.args[1]) = (__bridge void *)(testArg1);
        completion();
    });
}];
    
testblock(testArg);

实现原理

首先想想如果要延迟一个 Objective-C 方法的执行,需要怎么做?

答案是利用消息转发机制, NSInvocation 调用 retainArguments 将方法执行所需的上下文持有,这样才能保证方法执行时所需的参数、 target 等不会被释放。

对于 Block 来说,虽然也能通过 NSInvocation 来进行调用,但是经过 Hook 过后已经不再适用。因为 NSInvocation 的实现机制以及生命周期管理是个黑盒,且无法承载 Hook 相关的信息,需要自己来实现个 BHInvocation

qU3mMzr.png!web

BHInvocation 结构

我之前的 BlockHook with Struct 这篇文章提到了个技术点:在 x86 架构下,当 Block 返回值是大于 16 Byte 的 struct 时,参数列表有些变化:

ZZRn22j.png!web

为了兼容这种情况,需要两套 argsretValue 。一套『真的』用于传给 libffi 调用原始函数指针,另一套『假的』提供给使用方读写参数和返回值。这样使用方无需关心底层特殊逻辑,直接用就行了。

BHInvocation 主要结构如下:

RrA3Evu.png!web

PS: BHInvocationNSInvocation 的场景和用法有些不同,所以实现上也会有差异。 NSInvocation 没有公开源码,想了解原理的可以看看 mikeash 的实现: MAInvocation 。但我并没有参考过 mikeash 的源码。

retainArguments 实现

retainArguments 实现策略:

void **args
retain
copy
strcpy

6VvEzyE.png!web

需要注意的是这里依然要考虑两套 argsretValue 的问题。代码就不贴了,有兴趣的可以自己去看。

block_interceptor 实现

解决了 retainArguments 的实现,一切都好说了。只要基于原有的 block_hookWithMode:usingBlock: 接口稍加改装即可:

- (BHToken *)block_interceptor:(void (^)(BHInvocation *invocation, IntercepterCompletion completion))interceptor {
    return [self block_hookWithMode:BlockHookModeInstead usingBlock:^(BHInvocation *invocation) {
        if (interceptor) {
            IntercepterCompletion completion = ^() {
                [invocation invokeOriginalBlock];
            };
            interceptor(invocation, completion);
            [invocation retainArguments];
        }
    }];
}

后记

写了这么多关于 BlockHook 的文章,我越来越发现自己在苹果爸爸面前所表现出的无知。几乎每一步都要去踩很多坑,看很多源码。而这次是看着苹果爸爸的文档脑补如何实现,业界也没有能参考的先例。

这种感觉犹如自己在黑暗中不断探索,并享受着这种孤独。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK