1. 你好,DTrace
DTrace可以使用prob钩住一个函数或一组函数。可以执行自定义操作来查询特定进程中的信息。如果曾经使用过Instruments应用程序,那么它下面的许多功能都是由DTrace
提供的。
1.2 初识DTrace
打开模拟器和终端窗口:
sudo dtrace -n 'objc$target:*ViewController::entry' -p `pgrep SpringBoard`
加上sudo是因为DTrace
是很强大,甚至可以查询电脑上其他用户的信息。这个DTrace
命令有两个选项,name选项(-n
)和PID选项(-p
)。
如果输入的所有内容都正确,将在终端窗口中得到类似的输出
以下内容:
dtrace: description 'objc$target:*ViewController::entry' matched 42264 probes
每次探测命中将打印包含以“ViewController”结尾的Objective-C类。由于将function字段留空,只要类名以ViewController
结尾,它就会输出每个匹配的Objective-C方法。
sudo dtrace -n 'objc$target:UIViewController:-viewWillAppear?:entry { ustack(); }' -p `pgrep SpringBoard`
- 将
*ViewController
查询更改为UIViewController
。 - 将
-viewWillAppear?
添加到了function位置。这个新的DTrace
脚本将只匹配-[UIViewController viewWillAppear:]
,而不是匹配任何包含字符串“ViewController”的类的每个函数。?
表示DTrace
中的通配符,它将解析为viewWillAppear:
方法中的“:”。 - 在一个大括号中使用一个名为
ustack()
的函数。每次命中-[UIViewController viewWillAppear:]
时都会调用此逻辑。ustack()
是DTrace
的内置函数之一。当这个方法被命中时,它会打印调用栈。
输入正确的话会得到以下输出:
dtrace: description 'objc$target:UIViewController:-viewWillAppear?:entry ' matched 1 probe
当-[UIViewController viewWillAppear:]
命中时,堆栈跟踪将在终端中打印出来。
当objc_msgSend
执行时,函数签名将如下所示:
objc_msgSend(self_or_class, SEL, ...);
可以使用arg0
参数在DTrace
中获取第一个参数,也就是UIViewController
的实例。不幸的是,我们只能获得指针的引用,不能运行任何Objective-C代码,如[arg0 title]
。
在DTrace
命令的ustack()
函数之前添加printf("\nUIViewcontroller is: 0x%p\n", arg0);
:
sudo dtrace -n 'objc$target:UIViewController:-viewWillAppear?:entry
{ printf("\nUIViewcontroller is: 0x%p\n", arg0); ustack(); }' -p `pgrep SpringBoard`
dtrace: description 'objc$target:UIViewController:-viewWillAppear?:entry ' matched 1 probe
CPU ID FUNCTION:NAME
0 142401 -viewWillAppear::entry
UIViewcontroller is: 0x7ff224830000
UIKitCore`-[UIViewController viewWillAppear:]
SpringBoard`-[SBIconController viewWillAppear:]+0x2a
UIKitCore`-[UIViewController _setViewAppearState:isAnimating:]+0x297
UIKitCore`-[UIViewController __viewWillAppear:]+0x73
BaseBoardUI`-[UIViewController(BaseBoardUI) bs_beginAppearanceTransition:animated:]+0x64
BaseBoardUI`-[UIViewController(BaseBoardUI) bs_beginAppearanceTransitionForChildViewController:toVisible:animated:]+0xb6
SpringBoard`-[SBHomeScreenViewController setIconControllerHidden:]+0x106
SpringBoard`-[SBUIController restoreContentWithOptions:]+0x54e
SpringBoard`-[SBUIController beginRequiringContentForReason:options:]+0x125
SpringBoard`-[SBToAppsWorkspaceTransaction transaction:performTransitionWithCompletion:]+0x10d
SpringBoard`-[SBSceneLayoutWorkspaceTransaction _beginLayoutTransition]+0x93
SpringBoard`__55-[SBSceneLayoutWorkspaceTransaction _performTransition]_block_invoke_2+0x3c
BaseBoard`-[BSBlockTransaction _begin]+0x85
BaseBoard`__22-[BSTransaction begin]_block_invoke+0xa5
BaseBoard`-[BSTransaction _preventTransactionCompletionForReason:ignoringAuditHistory:andExecuteBlock:]+0x55
BaseBoard`-[BSTransaction begin]+0x3b3
BaseBoard`-[BSTransaction _noteFinishedWork]+0x1fd
BaseBoard`-[BSTransaction _checkAndReportIfCompleted]+0xc2
BaseBoard`-[BSTransaction _removeMilestones:ignoringAuditHistory:]+0x498
BaseBoard`__49-[BSTransaction evaluateMilestone:withEvaluator:]_block_invoke+0x9c
现在在打印出堆栈跟踪之前,打印对调用viewWillAppear的UIViewController
的引用。如果复制DTrace
的这个指针的地址并将LLDB附着到SpringBoard
,我们会发现它指向一个有效的UIViewController
(如果还没有被释放的话)。
sudo dtrace -n 'objc$target:::entry { @[probemod] = count() }' -p `pgrep SpringBoard`
暂时还不会得到任何输出,但是一旦使用Ctrl + C终止这个脚本。我们将得到一个聚合列表,列出了执行特定类的方法的所有次数。从我的输出中可以看到,SpringBoard
有12878个由NSObject
实现的方法调用。
区分父类子类的调用很重要。例如,调用-[UIViewController class]
将被视为对NSObject
执行的方法总数的命中。因为UIViewController
没有重写Objective-C方法class
,UIViewController
的父类UIResponder
也没有重写。
1.2 DTrace专业用语
我们可以将探测视为查询。这些探测是DTrace
可以在特定进程中监视的事件,也可以跨计算机全局监视。
dtrace -n 'objc$target:NSObject:-description:entry / arg0 != 0 / { @[probemod] = count(): }' -p `pgrep SpringBoard`
这个例子将监视NSObject
在名为SpringBoard
的进程中对description
方法的实现。此外,一旦description
方法开始,就执行逻辑来聚合调用该方法的次数。
-
Probe Description:封装一组指定0个或多个探测的项。它由
provider
、module
、function
和name
组成,每个都用冒号分隔。在冒号之间省略这些项将导致探测描述包含所有匹配项。你可以用*
或?
用于模式匹配的运算符。?
运算符将充当单个字符的通配符,而*
将匹配任何字符。 -
Provider:将
provider
视为一组代码或公共功能。我们将主要使用objcprovider
程序来跟踪Objective-C方法调用。objcprovider
程序将聚合所有Objective-C代码。
注意:
$target
关键字是一个特殊关键字,它将匹配我们给DTrace提供的任何PID。某些provider
(比如objc)希望提供这个关键字。把
$target
看作实际PID的占位符,它监视特定进程中的Objective-C。如果确实引用了$target
占位符,则必须在DTrace命令中通过-p
或-c
选项标志指定目标PID。通常,如果我们知道确切的PID,这可以通过
-p
PID完成,或者可能通过-p "pgrep NameOFProcess"
完成。pgrep
命令将查找进程名为NameOFProcess
的PID,然后返回该PID,然后将其应用于$target
变量。
-
Module:在objc提供程序中,
Module
部分是指定要观察的类名的位置。在这个意义上,使用objc提供程序有点独特。因为通常模块用于引用代码来自的库。事实上,在一些提供商中,根本没有模块!然而,objc provider
的作者选择使用模块来引用Objective-C类名。对于这个例子来说,模块是NSObject
。 -
Function:探测描述中可以指定要观察的函数名。对于这个例子来说,函数是-description。
objc provider
的作者使用+
或-
来确定Objective-C函数是类还是实例方法。如果将函数更改为+description
,它将查询任何带有+[NSObject description]
的探测。 -
Name:这通常指定函数中探测的位置。entry表示进入函数,return表示离开函数。此外,在
objc provider
中,还可以指定要在其上创建探测的任何汇编指令偏移量!对于这个例子来说,名称是entry
,或者函数的开始。 -
Predicate:一个可选表达式,用于评估操作是否是
action
的候选者。把谓词看作if语句中的条件。只有谓词的计算结果为true时,才会执行action
部分。如果省略谓词部分,则每次对给定探测执行操作块。对于这个特定的例子,谓词是/ arg0 = 0 /
,这意味着只有当arg0
不是nil时,才会计算谓词后面的内容。 -
Action:如果探测与探测描述匹配,并且谓词的计算结果为true,则要执行的操作。可以执行将某些内容打印到控制台,或者更高级的功能。对于本例,操作是
@[probemod] = count();
代码。
简单来说,它的结构是这样:
provider:module:function:name / predicate / { action }
DTrace
可以包含多个子句。这些子句可以使用探测描述监视不同的项,检查谓词中的不同条件,并使用不同的操作执行不同的逻辑。
dtrace -n 'objc$target:NSView:-init*:entry' -p `pgrep -x Xcode`
有一个objc$target:NSView:-init*:entry
的探测描述,其中包括NSView
作为模块,-init*
作为函数,entry
作为名称,没有谓词和操作。DTrace
生成一个用于跟踪的默认输出(可以使用-q
选项使其保持沉默)。这个默认输出仅显示函数和名称。例如,如果在跟踪-[NSObject init]
时没有使默认DTrace
操作静音,则DTrace输出将如下所示:
dtrace: description ’objc$target:NSObject:-init:entry’ matched 1 probe
CPU ID FUNCTION:NAME
2 512130 -init:entry
2 512130 -init:entry
2 512130 -init:entry
2 512130 -init:entry
从输出来看,跟踪进程时-[NSObject init]
被命中4次。我们可以告诉DTrace
使用不同格式的输出,方法是将-q
选项与一个打印函数组合起来,以显示输出的其他格式。
-n
参数指定可以采用provider:module:function:name
、module:function:name
或function:name
格式的DTrace名称。此外,name
选项可以接受可选的探测子句。这就是为什么要将所有一行脚本内容用单引号括起来传递给-n
参数的原因。
1.3 列出探测器
-l
将列出在探测描述中匹配的所有探测。当我们使用-l
选项时,DTrace
将只列出探测,而不执行任何操作。这使得-l
选项成为一个很好的工具,可以用来学习哪些要工作,哪些不工作。
在构建DTrace脚本时,我们将再次查看探测描述并系统地限制其范围。请考虑以下情况,但不要执行此操作:
sudo dtrace -ln ’objc$target:::’ -p `pgrep -x Finder`
这将在Finder
应用程序中的每个Objective-C类、方法和汇编指令上创建探测描述。对于DTrace
脚本来说,这是一个非常糟糕的主意。最好不要运行,因为将获得大量的命中。
注意:我们向
pgrep
提供了-x
选项。因为我们可能获得多个pid
,这将破坏占位符$target
。-x
选项表示返回与Finder
名称完全匹配的PID。如果一个进程有多个实例,可以使用-o
或-n
选项在pgrep
中获得最老的或最新的实例。
sudo dtrace -ln 'objc$target:NSView::' -p `pgrep -x Finder`
这将列出NSView
实现的每个方法以及每个方法中的每个汇编指令的探测。仍然是一个可怕的想法,但至少这个会在一秒钟后打印出来。这有多少个探测器?我们可以通过将输出发送到wc
命令来获得答案:
~> sudo dtrace -ln 'objc$target:NSView::' -p `pgrep -x Finder` | wc -l
41307
我们再过滤一下:
sudo dtrace -ln 'objc$target:NSView:-initWithFrame?:' -p `pgrep -x Finder`
这将把探测描述筛选到-[NSView initWithFrame:]
中执行的每个汇编指令。注意到?
的用法了吗?而不是冒号来指定Objective-C选择器。如果使用冒号,则DTrace
将错误地分析输入,认为函数部分已完成,并已转到DTrace
探测中指定名称。函数描述的开头还有-
表示这是一个实例Objective-C方法。
仍然输出太多,我们只想监视-[NSView initWithFrame:]
方法的开头。
~> sudo dtrace -ln 'objc$target:NSView:-initWithFrame?:entry' -p `pgrep -x Finder`
Password:
ID PROVIDER MODULE FUNCTION NAME
1156826 objc358 NSView -initWithFrame: entry
1.4 一个创建DTrace脚本的脚本
在使用DTrace
时,不仅要处理异常陡峭的学习曲线,如果遇到构建时或运行时DTrace错误,还要处理一些神秘错误。
为了帮助您在学习DTrace时减轻这些构建问题,这里创建了一个的小脚本tobjectivec.py
(trace Objective-C)。这是一个LLDB Python脚本,会为我们生成一个自定义DTrace脚本。
通过tobjectivec.py探索DTrace
运行Allocator项目,然后在调试器中暂停。
(lldb) tobjectivec -g
通常,tobjectivec
脚本将在计算机的/tmp/
目录中生成一个脚本。但是,这个-g
选项表示我们正在调试脚本并将输出显示到LLDB,而不是在/tmp/
中创建文件。使用-g
(--debug
)选项时,当前脚本将显示在控制台上。这个没有额外参数的tobjectivec.py
运行将产生以下输出:
#!/usr/sbin/dtrace -s /* 1 */
#pragma D option quiet /* 2 */
dtrace:::BEGIN { printf("Starting... use Ctrl + c to stop\n"); } /* 3 */
dtrace:::END { printf("Ending...\n" ); }
/* Script content below */
objc$target:::entry /* 5 */
{
printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]); /* 6 */
}
- 执行
DTrace
脚本时,第一行必须是#!/usr/sbin/dtrace-s
,否则脚本可能无法正常运行。 - 表示在探测触发时不列出探测计数,也不执行默认的
DTrace
操作。相反,我们将给DTrace
设置自定义操作。 - 这是此脚本中
DTrace
子句的三分之一。让DTrace
用于监视某些DTrace
事件。比如当DTrace
脚本即将启动时。一旦DTrace开始,就打印出“Starting... use Ctrl + c to stop“字符串。 -
DTrace
脚本完成时,打印“Ending...”。 - 我们感兴趣的
DTrace
探测描述。意思是在提供给脚本的进程ID中跟踪找到的所有Objective-C代码。 - 这个子句的
action
部分,输出触发的Objective-C探测的实例,然后输出Objective-C样式的输出。在这里,可以看到使用probefunc
和probemod
,这将是函数和模块的char*
表示。DTrace
有几个可以使用的内置变量,probefunc
、probemod
、probeprov
和probename
。记住,模块将表示类名,而函数将表示Objective-C方法。这里用到了probemod
和probefunc
,并以我们习惯的C语法显示它。
重新执行:
(lldb) tobjectivec
Copied script to clipboard... paste in Terminal
在终端窗口粘贴执行:
~> sudo /tmp/lldb_dtrace_profile_objc.d -p 73610
Password:
Starting...use Ctrl + c to stop
在Xcode的LLDB中输入po [NSObject class]
:
如果我们随便玩一玩这个app会发现有大量的输出。东西太多了,下面我们通过向模块说明符添加内容来过滤一些噪声。在LLDB中键入以下内容:
tobjectivec -m *StatusBar* -g
我们看一下这次的探测描述:
objc$target:*StatusBar*::entry
{
printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]);
}
注意探针的模块部分是如何改变的。在正则表达式中,*
可以被认为是任何我们感兴趣的。当探测进入函数的开头时,查询包含任何Objective-C类的区分大小写单词StatusBar的探测。在LLDB中,删除-g选项以便将此脚本复制到剪贴板,然后重新执行该命令。
(lldb) tobjectivec -m *StatusBar*
终端中粘贴:
~> sudo /tmp/lldb_dtrace_profile_objc.d -p 73610
Password:
Starting...use Ctrl + c to stop
跳转到模拟器并使用⌘ + Y切换通话状态栏,或使用⌘ + ←或⌘ + →旋转模拟器,同时注意DTrace终端窗口。再次得到大量的输出。我们可以使用DTrace
在代码上撒下一个大网,并在需要时快速向下深挖。
跟踪调试命令
我们来观察一下要调用多少Objective-C方法才能生成一个简单的Objective-C NSString。在LLDB中,输入以下内容:
(lldb) tobjectivec
然后在LLDB中输入:
(lldb) po @"hi this is a long string to avoid tagged pointers"
测试
我们刚刚打印了一个简单的
NSString
,看看这需要多少Objective-C调用!
返回到LLDB并键入以下内容:
expression -l swift -O -- class b { }; let a = b()
我们使用Swift调试上下文创建一个纯Swift类,然后将其实例化。创建这个类时,请观察Objective-C方法调用。
0x0000000105f81c58 +[_TtCs12_SwiftObject class]
0x00000001088db7f8 +[_TtCs12_SwiftObject initialize]
0x00000001088db7f8 -[_TtCs12_SwiftObject self]
如果把DTrace
抛出的地址复制出来,然后po
一下。你会看到这个纯Swift类调用了多少Objective-C方法。一个“纯粹的”Swift并不像我们想象的那样纯粹,对吧?
跟踪一个对象
我们可以使用DTrace
轻松跟踪特定引用的方法调用。暂停应用程序,使用LLDB获取对UIApplication
的引用。
(lldb) po UIApp
<UIApplication: 0x7ffc65601750>
复制引用并使用它来构建一个谓词,该谓词仅在该引用为arg0
时停止。
(lldb) tobjectivec -g -p 'arg0 == 0x7ffc65601750'
#!/usr/sbin/dtrace -es
#pragma D option quiet
dtrace:::BEGIN { printf("Starting...use Ctrl + c to stop\n"); }
dtrace:::END { printf("Ending...\n" ); }
/* Script content below */
objc$target:::entry / arg0 == 0x7ffc65601750 /
{
printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]);
}
然后去掉-g
选项:
(lldb) tobjectivec -p 'arg0 == 0x7ffc65601750'
触发模拟器中的home
按钮(⌘+Shift + H)或状态栏(⌘ + Y)。
这将打印[UIApplication sharedApplication]
实例上的每个Objective-C方法调用。
是不是输出太多内容了?我们来将内容聚合:
(lldb) tobjectivec -g -p 'arg0 == 0x7ffc65601750' -a '@[probefunc] = count()'
#!/usr/sbin/dtrace -es
#pragma D option quiet
dtrace:::BEGIN { printf("Starting...use Ctrl + c to stop\n"); }
dtrace:::END { printf("Ending...\n" ); }
/* Script content below */
objc$target:::entry / arg0 == 0x7ffc65601750 /
{
@[probefunc] = count()
}
在不使用-g
选项的情况下重新运行上面的tobjectivec
命令,然后将剪贴板内容粘贴到终端并在LLDB中继续执行。这时终端中尚未显示任何内容。但DTrace
正在悄悄地聚合发送到UIApplication
实例的每个方法。
在模拟器中随意玩一玩,获取发送到UIApplication的方法的正常计数。一旦使用通常的Ctrl + C终止脚本,DTrace
将打印应用于UIApplication
实例的所有Objective-C方法的总数。
其他DTrace小技巧
追踪所有对象的所有初始化方法:
(lldb) tobjectivec -f ?init*
检测进程内通信相关的逻辑(比如,Webviews、keyboards等等):
(lldb) tobjectivec -m NSXPC*
打印出在iOS设备上处理开始触摸事件的UIControl
的子类 :
(lldb) tobjectivec -m UIControl -f -touchesBegan?withEvent?
网友评论