关于 .d 文件一些思考与理解
Reactive Cocoa 里的 .d
文件到底有什么用呢?
问题的由来
最近在开发过程中,遇到了一个自己还无法回答的问题,就是 Reactive Cocoa 里的两个 .d
文件到底有啥用,以及怎么用?
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 的例子:
// 声明 probe
provider syncengine_sync {
probe strategy_go_to_state(int);
}
或者调用 probe 的例子:
// 调用 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 工具不会停止,也不会做任何事儿,它的逻辑大概如下(其实就是一个无限循环):
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
while (1) { }
}
return 0;
}
声明探针
此时我们在工程里创建一个 provider.d 文件声明一个自定义的 probe
provider zsq {
probe go();
};
此时的文件目录是如下
▶ tree
.
├── main.m
└── zsq.d
预埋探针
在 Xcode 里面的 build rule 会有这么一个自动化操作,如果判断出目标文件是 .d
文件,也就是 dtrace 文件,会生成对应的 .h
文件。
结合上面的例子,此时 Xcode 的 build system 就会生成一个 zsq.h
文件,在 build log 里我们可以查看到它。
此时我们可以看一下 zsq.h
里的内容,对我们比较有用的是两个基于探针行为定义的宏 ZSQ_GO_ENABLED
和 ZSQ_GO
,感兴趣可以展开下面的代码来查看。
/*
* 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
#defineZSQ_GO() \
do { \
__asm__ volatile(".reference " ZSQ_TYPEDEFS); \
__dtrace_probe$zsq$go$v1(); \
__asm__ volatile(".reference " ZSQ_STABILITY); \
} while (0)
#defineZSQ_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
#defineZSQ_GO() \
do { \
} while (0)
#defineZSQ_GO_ENABLED() (0)
#endif /* !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED */
#ifdef__cplusplus
}
#endif
#endif/* _ZSQ_H */
通过这个自动生成的头文件,我们就可以在自己的代码中预埋自定义的 probe,大体的逻辑使用方式就是先判断 probe 是否 enable,如果 enable,再真的执行它。
#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 命令后
// 启用 cli 命令行工具
..../SQTool
在没有触发 DTrace 之前,终端不会有任何输出
此时我们在另一个 terminal 里去启用 dtrace 来追踪预埋的探针
// -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()
包裹的逻辑之一。
总结
至此,我们完成了自定义探针的定义,预埋和调用。
那基于前面的 demo,我们来理解下 Reactive Cocoa 里的 .d
文件到底干了什么?以及怎么用?
以 Reactive Cocoa 里的 RACCompoundDisposableProvider 为例,它只是定义了一个 provider 为 RACCompoundDisposable,probe 为 added 和 removed 的探针。
而在 RACCompoundDisposable.m
中,会在 addDisposable
中预埋 RACCOMPOUNDDISPOSABLE_ADDED
的探针,感兴趣可以展开下面的代码来查看。
- (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 里面。
那么同理到我们的 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
文件的作用和在组件化的时候要怎么对待它了,希望这篇文章能对你有所帮助!