SketchK's Studio.

关于 .d 文件一些思考与理解

字数统计: 2.4k阅读时长: 9 min
2021/07/07 Share

Reactive Cocoa 里的 .d 文件到底有什么用呢?

问题的由来

最近在开发过程中,遇到了一个自己还无法回答的问题,就是 Reactive Cocoa 里的两个 .d 文件到底有啥用,以及怎么用?

01.jpg

DTrace

DTrace 是一个动态追踪技术,说的可能更接地气一点,就是可以使用 DTrace 附加在一个已经运行的程序上,且不会打断当前程序的运行,也不需要重新编译或者启动此程序。

乍一听,感觉很不错,好像我们可以搞点事情了,但放到 macOS 和 iOS 的场景下就有了一些限制。

DTrace 只能在 macOS 上运行,Apple 也在 iOS 上使用 DTrace,用以支持像 Instruments 这样的工具,但对于第三方开发者,DTrace 只能运行于 macOS 或 iOS 模拟器。

基本概念

这篇文章本身的目的是为了解决文章开篇提到的问题,所以不会科普太多 DTrace 技术本身的基本概念。这里只是把下面用到的概念说明一下,方便读者理解。

在 DTrace 里有两个比较重要的概念,它们分别是probe(探针)和 dtrace file(DTrace 脚本)。

探针是指我们利用在代码里埋的点,插的桩,它有一套标准的定义,本文在这里不展开了,感兴趣可以阅读这个资料:DTrace Book

DTrace 脚本,是用 D 语言编写的脚本,既可以用 DTrace 脚本声明 probe,也可以触发 probe。

声明 probe 的例子:

1
2
3
4
// 声明 probe               
provider syncengine_sync {
probe strategy_go_to_state(int);
}

或者调用 probe 的例子:

1
2
3
4
5
// 调用 probe  
syncengine_sync*:::strategy_go_to_state
{
printf("Transitioning to state %d\n", arg0);
}

那么怎么启用 DTrace 呢?整体来说,有两种途径:

  • 使用 dtrace 脚本来触发,也就是 .d 文件
  • 使用 dtrace 命令来触发,也就是命令行里的 dtrace 命令

注意,如果想使用 dtrace 还需要关闭 System Integrity Protection,也就是常说的 Rootless,具体操作步骤是:

  • 重新启动你的macOS机器
  • 当屏幕变成空白时,按住 Command + R,直到出现苹果的启动标志。这将使你的电脑进入 Recovery Mode
  • 现在,从顶部找到 Utilities 菜单,然后选择 Terminal
  • 在终端窗口打开后,输入 csrutil disable && reboot
  • 只要 csrutil disable 命令成功,你的电脑就会在禁用 Rootless 后重新启动

DTrace 的使用场景

那么从使用者的角度来说,DTrace 只适用于两种场景:

  • 追踪系统内核代码:使用者只需要直接调用系统预埋的 probe 即可。
  • 追踪 App 侧的自定义代码:使用者一方面需要在 App 侧埋 probe,另一方面也需要去调用自己埋的 probe。

对于追踪系统内核的代码,其实有很多文章在说明,这里就不展开来说了,感兴趣可以看看网上的文章, 大多都是在将这种场景的使用方式.

今天的目标也是为了解释第二个场景,进而说明 Reactive Cocoa 里的 .d 文件的用途。

在自己的代码里使用 DTrace 技术

这里我们设计一个 CLI 工具,这个 CLI 工具不会停止,也不会做任何事儿,它的逻辑大概如下(其实就是一个无限循环):

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
while (1) { }
}
return 0;
}

声明探针

此时我们在工程里创建一个 provider.d 文件声明一个自定义的 probe

1
2
3
provider zsq {
probe go();
};

此时的文件目录是如下

1
2
3
4
▶ tree
.
├── main.m
└── zsq.d

预埋探针

在 Xcode 里面的 build rule 会有这么一个自动化操作,如果判断出目标文件是 .d 文件,也就是 dtrace 文件,会生成对应的 .h 文件。

结合上面的例子,此时 Xcode 的 build system 就会生成一个 zsq.h 文件,在 build log 里我们可以查看到它。

02.jpg

此时我们可以看一下 zsq.h 里的内容,对我们比较有用的是两个基于探针行为定义的宏 ZSQ_GO_ENABLEDZSQ_GO,感兴趣可以展开下面的代码来查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
* Generated by dtrace(1M).
*/

#ifndef _ZSQ_H
#define _ZSQ_H

#if !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED
#include <unistd.h>

#endif /* !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED */

#ifdef __cplusplus
extern "C" {
#endif

#define ZSQ_STABILITY "___dtrace_stability$zsq$v1$1_1_0_1_1_0_1_1_0_1_1_0_1_1_0"

#define ZSQ_TYPEDEFS "___dtrace_typedefs$zsq$v2"

#if !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED

#define ZSQ_GO() \
do { \
__asm__ volatile(".reference " ZSQ_TYPEDEFS); \
__dtrace_probe$zsq$go$v1(); \
__asm__ volatile(".reference " ZSQ_STABILITY); \
} while (0)
#define ZSQ_GO_ENABLED() \
({ int _r = __dtrace_isenabled$zsq$go$v1(); \
__asm__ volatile(""); \
_r; })


extern void __dtrace_probe$zsq$go$v1(void);
extern int __dtrace_isenabled$zsq$go$v1(void);

#else

#define ZSQ_GO() \
do { \
} while (0)
#define ZSQ_GO_ENABLED() (0)

#endif /* !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED */


#ifdef __cplusplus
}
#endif

#endif /* _ZSQ_H */

通过这个自动生成的头文件,我们就可以在自己的代码中预埋自定义的 probe,大体的逻辑使用方式就是先判断 probe 是否 enable,如果 enable,再真的执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>
#import "zsq.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
while (1) {
if(ZSQ_GO_ENABLED()) {
NSLog(@"Hello");
ZSQ_GO();
}
}
}
return 0;
}

触发探针

首先正常启用 CLI 命令后

1
2
// 启用 cli 命令行工具
..../SQTool

在没有触发 DTrace 之前,终端不会有任何输出

03.jpg

此时我们在另一个 terminal 里去启用 dtrace 来追踪预埋的探针

1
2
3
4
5
// -s 参数的 zsq.d 指的是定义的 probe 文件, 
// -P 参数的 zsq30629 指的是 probe name + PID, probe name 在 .d 里查看, PID 命令可以通过 ps -A 查看
sudo dtrace -s zsq.d -P zsq30629
// 或者
sudo dtrace -P zsq30629 // 如果你的 .d 文件已经被 dtrace 加载了,就无须重复使用 -s 参数重复加载

此时,dtrace 服务被激活,CLI 里预埋的 probe 生效,我们就看到执行 CLI 里的 terminal 不断的在输出 Hello,也就是被 ZSQ_GO_ENABLED() 包裹的逻辑之一。

04.jpg

总结

至此,我们完成了自定义探针的定义,预埋和调用。

那基于前面的 demo,我们来理解下 Reactive Cocoa 里的 .d 文件到底干了什么?以及怎么用?

以 Reactive Cocoa 里的 RACCompoundDisposableProvider 为例,它只是定义了一个 provider 为 RACCompoundDisposable,probe 为 added 和 removed 的探针。

05.jpg

而在 RACCompoundDisposable.m 中,会在 addDisposable 中预埋 RACCOMPOUNDDISPOSABLE_ADDED 的探针,感兴趣可以展开下面的代码来查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)addDisposable:(RACDisposable *)disposable {
NSCParameterAssert(disposable != self);
if (disposable == nil || disposable.disposed) return;

BOOL shouldDispose = NO;

OSSpinLockLock(&_spinLock);
{
if (_disposed) {
shouldDispose = YES;
} else {
#if RACCompoundDisposableInlineCount
for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) {
if (_inlineDisposables[i] == nil) {
_inlineDisposables[i] = disposable;
goto foundSlot;
}
}
#endif

if (_disposables == NULL) _disposables = RACCreateDisposablesArray();
CFArrayAppendValue(_disposables, (__bridge void *)disposable);

if (RACCOMPOUNDDISPOSABLE_ADDED_ENABLED()) {
RACCOMPOUNDDISPOSABLE_ADDED(self.description.UTF8String, disposable.description.UTF8String, CFArrayGetCount(_disposables) + RACCompoundDisposableInlineCount);
}

#if RACCompoundDisposableInlineCount
foundSlot:;
#endif
}
}
OSSpinLockUnlock(&_spinLock);

// Performed outside of the lock in case the compound disposable is used
// recursively.
if (shouldDispose) [disposable dispose];
}

那么基于这个探针,我们就可以使用 dtrace 去追踪 add 的行为。

那这种功能有什么用呢?

如果你在开发前端的时候使用 redux,估计你八成会使用到这样的一个 debug tool - reaction,它能将 saga 里的每个动作,展示在 debug tool 里面。

06.jpg

那么同理到我们的 Reactive Cocoa 中,我们就可以搞一个 debug tool 实时监控代码里某些行为,方便我们调试。

那 .d 文件对于我们意味着什么?

从组件化角度看,分为两种业务场景,一种是源码形式,一种是二进制形式

  • 在源码形式下,需要保留 .d 文件:由于组件的内的 .m 文件还会依赖 由 .d 自动生成的 .h 文件,如果想让编译通过,就需要保留 .d 文件。例如 RACCompoundDisposable.m 会依赖 RACCompoundDisposableProvider.d 生成的 RACCompoundDisposableProvider.h 文件,
  • 在二进制形式下,不需要保留 .d 文件:由于组件内的 .m 文件已经被编译成二进制,不再需要编译行为,.d 文件已经没有存在价值,也就不依赖 RACCompoundDisposableProvider.d 生成的 RACCompoundDisposableProvider.h 文件,

那结合上面的两个视角,我们在 CI 上又应该干点什么呢?

  • 基于现在的状况(即没有人把 .d 生成的 .h 放到公开头文件里),我们就可以把把 .d 文件当做 .m 文件一样看待,即在二进制产物中删掉 .d 文件即可。

那么可能就会有人,为啥不能把 .d 生成的 .h 放到公开头文件里呢,这个行为合理么?

  • 我认为是没有必要的,原因大致如下:
    • 首先 .d 文件生成的 .h 只是与 probe 相关的逻辑,为自己的组件提供了 dtrace 能力,方便自己的调试或者行为追踪。
    • 这种埋点自查的能力应当只在自己的组件内(例如组件 A)使用,即使提供给外界(组件 B)使用,那么组件 B 也无法追踪组件 A 的行为,组件 B 只能追踪自己的行为,如果想追踪自己的行为,那又为什么要用 A 里的 probe 呢?对吧。自己追踪自己就好,不要用别人的探针,避免歧义。
    • 所以这个 .d 文件从原理上是可以放到公开的 .h 文件中,但这并不是那么合理,所以从实际使用的角度上来说,是不应该将 .d 自动生成的 .h 文件放到公开的 .h 文件中。

好了,说到这里,我想你也大概明白了 .d 文件的作用和在组件化的时候要怎么对待它了,希望这篇文章能对你有所帮助!

参考资料

CATALOG
  1. 1. 问题的由来
  2. 2. DTrace
    1. 2.1. 基本概念
    2. 2.2. DTrace 的使用场景
  3. 3. 在自己的代码里使用 DTrace 技术
    1. 3.1. 声明探针
    2. 3.2. 预埋探针
    3. 3.3. 触发探针
    4. 3.4. 总结
  4. 4. 那 .d 文件对于我们意味着什么?
  5. 5. 参考资料