怎样创建一个xcode插件 第2部分/3部分

本文翻译自 https://www.raywenderlich.com/97756/creating-an-xcode-plugin-part-2

原作者:Derek Selander

译者:@yohunl

译者注:原文使用的是xcode6.3.2,我翻译的时候,使用的是xcode7.2.1,经过验证,本部分中说的依然是有效的.在文中你可以学习到一系列的技能,非常值得一看.这些技能不单单只是用来创建插件,对你平时的调试等,也有非常大的帮助.

欢迎你来到创建xcode插件教程的第二部分.在第一部分中,你已经了解了怎么通过NSNotification来窥探xcode的内部类,并且代码注入了其私有类DVTBezelAlertPanel,并且,你还添加了一个NSMenuItem菜单到xcode的菜单栏里,通过它来持久化是否开启我们自定义的插件Rayrolling的功能

在这个教程中,你会在第一部分中创建的Rayrolling的基础上继续向前,如果你并没有在这之前阅读第一部分或者你想从本处开始,你可以下载第一部分完成的demo.本章中,你将会通过深入了解一些可用的工具来进一步探索Xcode,利用你将会学到的新知识,来修改Xcode的标题栏,使标题栏改为展示Ray的某首歌的歌词
Plugin_Swizzled_Titlebar1-700x56.png

开始啦


打开Xcode和Terminal(终端),并且将它俩都平铺在桌面上,这样你可以同时看到它们,方便后续的操作.(yohunl注:后文将会一面在Xcode上点击,一面观察终端控制台的输出,一旦发现我们需要的输出,就要立马停止终端控制台,所以,平铺在桌面上才便于你操作,否则,你就忙不过来啦)
View_Organization-700x405.png
因为从上一章,你已经学会了从xcode运行一个xcode实例来创建插件和调试插件,所以我们换一种新的方式:你将会直接通过终端命令来使用LLDB来探索Xcode,你不用再像第一部分那样从一个Xcode启动另一个调试用的Xcode实例来探索Xcode是怎样工作的了.

LLDB的BFF和DTrace


DTrace是最好的探索Xcode的工具之一,它是一款相当优秀的调试工具,它是Instruments的基石.只要你了解怎么使用它,你将会发现它是非常有用的工具.
首先,为了对其有个了解,我们需要一个使用DTrace的demo,就如同我们学习一门新语言时候,总是会通过一个Hello Worlddemo一样.那么开始吧… 创建一个脚本,它是用来保持所有以IDE开头的类的运行计数,并在每次你调用该特定类方法或者实例方法时,增加计数.在你退出脚本时候,DTrace将会转储这些数据.
运行Xcode,然后,在Terminal的一个新窗口中,输入如下的脚本:

1
sudo dtrace -n 'objc$target:IDE*::entry { @[probemod] = count(); }'  -p `pgrep -xo Xcode`

虽然你键入命令后并不会看到任何的输出,但是DTrace已经在后台默默的生成了所有方法调用的踪迹(trace).回到Xcode,随便尝试一些操作.打开某些文件,随便做一些点击操作.回到Terminal,然后按Ctrl+C来结束脚本,脚本产生的内容将会输出到Terminal上 (只有在你Ctrl + C终止脚本后,才能在Terminal上看到脚本输出的内容)
Dtrace_Hello_World-700x199.png
相当酷吧:),其实使用Dtrace,你可以做到很多事,但是我们的教程不会覆盖你想了解的DTrace的全部,相反的,我们提供一个快速的Dtrace语法的概述,它将会帮助你开始:
Dtrace_breakdown_v2-700x204.png

  • Probe Description(探头说明,前序描述):提供者(Provider),模块(Module),功能(Function),名字(Name)组成,它们由冒号分隔.省略它们中的任何一个,将会使Probe Description包含所有的匹配项.你可以使用 * 和 ? 操作符来做模式匹配.
    • Provider:这个包含了一系列的类和方法,例如profile,fbt,或者io.在我们的教程中,主要使用objc来hookObjective-C的方法调用.
    • Module:在OC语言中,这部分指示的是你希望观察的指定的类名
    • Function:指示你希望观察的特定方法名
    • Name:虽然根据你的Provider的不同,有不同的name值可选,但是你在此只需要使用entry或者retrun,来匹配函数的开始或者结束.
      你要了解,你可以使用$target关键字来匹配进程ID,也可以使用p或者c标志来指定target.
  • Predicate(谓词部分): 可选的表达式,它是用来过滤哪些动作是满足条件的.你可以把它当做一个if语句的条件部分.
  • Action(动作): 将要执行的操作.它可以只是简单的输出一些内容到控制台,也可以是执行一个高级功能.

就如同LLDB的命令image lookup -rn {Regex Query}一样,你也可以使用l标志来转储(dump)特定进程中的类和方法.
为了演示这点,我们来看这样一个简单的例子,运行Safari,然后在终端输入以下脚本

1
sudo dtrace -ln 'objc$target::-*ecret*:entry' -p `pgrep -xn Safari`

上述的DTrace脚本会打印出所有的名字中包含ecret字符串的实例方法.因为Probe Description部分 提供了Name参数entry,所有的方法都有entryreturn,所以基本上忽略了所有的重复查询请求.

注意:如果你想掌握更多的Dtarce知识,你可以参考Big Nerd的文章这本Dtrace书籍.如果并没有理解所有的这篇快速介绍中介绍的内容,也不需要沮丧,因为Dtrace是一个相当复杂的工具.

现在你了解到Dtace的最基本的内容,我们可以开始使用它来寻找我们感兴趣的NSViews了.因为Xcode含有大量的Views,你会发现,使用LLDB来试图找出它们,让你不堪重负.即使结合LLDB的条件断点,调试一些应用程序都包含的共同东西,也是一个丑陋的过程.

幸运的是,使用Dtrace,将能带给你对付此类问题的巨大的帮助.你将会使用DTrace来找出Xcode中组成标题栏的视图(NSViews).(⊙o⊙)…….那么该怎么做呢?

这是一种方法:当鼠标停止在,或者点击一个NSView,hitTest:方法将会被触发,这个方法将会返回在这个鼠标点下最深的子视图.你可以使用DTrace来顺着hitTest:方法来确定视图,那个你应该进一步探索其其父视图和子视图的视图.

在终端中输入以下脚本:

1
sudo dtrace -qn 'objc$target:NSView:-hitTest?:return /arg1 != 0/ { printf("UIView: 0x%x\n", arg1);  }' -p `pgrep -xo Xcode`

一旦这段脚本运行,请确定你的Xcode是第一响应者(通过在Xcode上任何地方点击一下即可成为第一响应者).在Xcode的窗口上移动你的鼠标指针,当你停止移动鼠标指针的时候,Dtrace将会输出一个内存地址多次,这是因为 hitTest: 被在视图层次上的多个视图调用.

定位到Xcode的标题栏,点击标题栏.选择出现在终端中最近的地址,拷贝到你的粘贴板中

打开一个新的终端窗口标签,运行LLDB,并将其附着到Xcode上,然后输出从DTrace中拷贝出的地址,像以下这样操作:

1
> lldb
(lldb) pro attach -n Xcode
... 
(lldb) po {上一步拷贝到粘贴板中的对象地址,例如是0x7fcc2687cd40,则该命令是 po 0x7fcc2687cd40}
...

其中,proprogress的缩写,LLDB使用的是前序匹配原则,在不引起歧义的情况下 process,proc,proce都是可以的.在下面你还会看到 pro at -n Xcode这种写法,都是因为这个原则,都是等效的. pro attach -n Xcode ,使用attach命令直接在LLDB中把一个正在运行的进程连接到LLDB中,以便于进行动态调试.也就是说这个命令直接可以将LLDB添加到任意的其它不是由它自己创建的进程中!!
你将会看到和以下相似的输出:
Dtrace_hitTest_2.gif

注意:现在,Xcode被我们通过终端输入的LLDB命令所暂停了.你可以在任何时候通过输入process interrupt或者其缩写pro i来暂停Xcode,从而能够开始调试Xcode.另外,你也可以通过在任何时候输入continue或者缩写c来恢复Xcode的运行.请确定你总是了解附着于Xcode上的LLDB的状态,你可以认为,当Xcode被你的调试LLDB所暂停时候,它是不能够相应你的单击等操作的(如同你在Xcode中打了断点,调试的时候进入到断点一样)

取决于你在Xcode中点击的地方,你可能会命中一系列的视图.探索这些内存地址的父视图(通过在lldb中 po [对象地址 supview])或者子视图(po [对象地址 subvies]),直到你得到了视图 IDEActivityView

一旦你找到了IDEActivityView,怎样确定这个视图就是你所希望找寻的视图呢?
通过在LLDB中输入以下命令:

1
(lldb) po [{找到的IDEActivityView实例的地址} setHidden:YES]  在我的机子上是  po [0x7fcc2686b3c0 setHidden:YES]
...这里的点表示我们省略了lldb的某些不重要的输出.
(lldb) c
...

如果是我们所期望的那个标题视图,那么现在Xcode的标题栏就被隐藏了,这就能证实它的确是我们所期望的标题栏的视图啦.

通过如下命令来重新显示它:

1
(lldb) pro i
(lldb) po [{找到的IDEActivityView实例的地址} setHidden:NO]  比如在我的机子上是  po [0x7fcc2686b3c0 setHidden:NO]
(lldb) c

以下这个图是上述在LLDB中输入命令的gif动画,供你参考:
LLDB_setHidden.gif

按照以往的经验,你知道,当你Build或者Stop一个Xcode工程的时候,标题栏视图的内容将会变化.你可以用DTrace来观察相关的函数.IDEActivity前缀是一个独特的命名约定,你可以观察所有的以IDEActivity开头的类,以此来观察在这幕后的所有相联系的事情.

回到终端,通过Control + C来停止DTrace,然后输入以下命令到终端中:

1
sudo dtrace -qn 'objc$target:IDEActivity*::entry  { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }' -p `pgrep -xn Xcode`

这段脚本将会输出所有类名前缀是IDEActivity的所有方法调用.一旦你停止这个脚本命令(Control+C),它还会输出指定类方法的被调用次数.

yohunl备注: 当你输入这个命令的时候,可能会遇到译者遇到的这种情况

1
yohunldeMacBook-Pro:~ yohunl$ sudo dtrace -qn 'objc$target:IDEActivity*::entry  { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }' -p `pgrep -xn Xcode`
dtrace: invalid probe specifier objc$target:IDEActivity*::entry  { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }: probe description objc50360:IDEActivity*::entry does not match any probes

当遇到这种情况不要担心,不要茫然,你只要完全退出Xcode,再重新启动Xcode,再输入这个命令就好了.

开始这个DTrace命令,回到Xcode中,随便打开一个工程,编译并运行,然后再停止.注意观察Xcode的标题栏的内容变化,然后停止Dtrace命令,查看结果:
Dtrace_IDEActivity.gif

仔细分析上面输出的信息.IDEActivityView和其它类之间是怎么运转的都在控制台的输出信息中,但是也有大量的其它信息夹杂在其中(控制台的输出信息中),不是么?

幸运的是,你可以有选择的限制展示给你的信息量.通过浏览相关的类,来确定是否有值得你进一步选择性探索的内容….,也许 IDEActivityReport* 就是一个不错的候选类,因为以它开头的类看起来就是有联系的(在我们上述的操作中,最后在DTrace看到的很多都是它,所以我们有理由觉得它就是有联系的).

修改Dtrace脚本,使他看起来如下:

1
sudo dtrace -qn 'objc$target:IDEActivityReport*::entry  { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probemod] = count(); }' -p `pgrep -xn Xcode`

再运行,停止Xcode的动作的同时,密切关注控制台的输出信息.一旦停止Xcode,立马Control+C停止掉Dtrace的脚本,是否出现了一些类,看起来是值得我们进一步去探索的呢?

IDEActivityReportStringSegment看起来比较值得探索.缩小我们的脚本的搜索范围,聚焦到这个类上来,注意以下命令的变化,注意Probe的Function部分的变化(我们上面分析了Dtrace命令的组成,其中Probe部分有个Function组成部分)

1
sudo dtrace -qn 'objc$target:IDEActivityReportStringSegment::entry  { printf("%c[%s %s]\n", probefunc[0], probemod, (string)&probefunc[1]); @num[probefunc] = count(); }' -p `pgrep -xn Xcode`

通过编译,运行,停止Xcode工程;再一次停止Dtrace命令,观察IDEActivityReportStringSegment类的实例的方法各自被调用的次数(当Control+C停止命令后,在控制台会输出各个方法的调用次数).看起来,initWithString:priority:frontSeparator:backSeparator:initWithString:priority:值得进一步分析!

打开一个新的LLDB会话(打开一个终端标签页面,lldb进入LLDB),然后再运行如下命令:

1
(lldb) pro attach -n Xcode
(lldb) rb 'IDEActivityReportStringSegment\ initWithString'
Breakpoint 9: 2 locations.
(lldb) br command add
Enter your debugger command(s).  Type 'DONE' to end.
> po NSLog(@"customidentifier %s %@", $rsi, $rdx) 
> c 
> DONE
(lldb) c

在上面这段命令中,你添加了自定义的命令,当任何属于IDEActivityReportStringSegment类的以initWithString方法开头的方法被调用时,就会执行添加的自定义命令.它输出IDEActivityReportStringSegment实例的方法的 Selectorself (还记得教程一中提到的各个寄存器中存放的内容了吧)到控制台.

另外,使用了customidentifier 来标识 NSLog的输出信息,这样你可以更容易的从控制台的输出中容易的定位到这些Log.

回到终端,新建一个终端tab(使用快捷键 ⌘ + t ),建立一个customidentifiergrep尾搜索:

1
tail -f /var/log/system.log | grep customidentifier  #译者注,这句话输入后,并没有输出,当你切换到Xcode中,编译,运行,就产生输出了

编译,运行Xcode,以便让Xcode产生IDEActivityReportStringSegment变化.下面的gif动画,显示了上面所说的所有你输入到LLDB中的命令,看起来如下:
LLDB_String_Hunting.gif

比较控制台的输出和Xcode的标题栏的输出,你可以明确的得到:它就是你要寻找的内容!!

使用Swizzle的时候了


创建一个Objective-C的类NSObject的category,命名为Rayrolling IDEActivityReportStringSegment
添加下面的代码到建立的文件 NSObject+Rayrolling_IDEActivityReportStringSegment.m:中:

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
#import "NSObject+Rayrolling_IDEActivityReportStringSegment.h"
#import "NSObject+MethodSwizzler.h"
#import "Rayrolling.h"

@interface NSObject ()

// 1
- (id)initWithString:(id)arg1 priority:(double)arg2 frontSeparator:(id)arg3 backSeparator:(id)arg4;
@end

@implementation NSObject (Rayrolling_IDEActivityReportStringSegment)

// 2
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSClassFromString(@"IDEActivityReportStringSegment") swizzleWithOriginalSelector:@selector(initWithString:priority:frontSeparator:backSeparator:) swizzledSelector:@selector(Rayrolling_initWithString:priority:frontSeparator:backSeparator:) isClassMethod:NO];
});
}

- (id)Rayrolling_initWithString:(NSString *)string priority:(double)priority frontSeparator:(id)frontSeparator backSeparator:(id)backSeparator
{

// 3
static NSArray *lyrics;

// 4
static NSInteger index = 0;
static dispatch_once_t onceToken;

// 5
dispatch_once(&onceToken, ^{
lyrics = @[@"Never gonna live you up.",
@"Never gonna set you down."];


});

// 6
index = index >= lyrics.count -1 ? 0 : index + 1;

// 7
if ([Rayrolling isEnabled]) {
return [self Rayrolling_initWithString:lyrics[index] priority:priority frontSeparator:frontSeparator backSeparator:backSeparator];
}
return [self Rayrolling_initWithString:string priority:priority frontSeparator:frontSeparator backSeparator:backSeparator];
}

@end

下面是对上面代码的一些解释:

  1. 你需要前向声明私有类的初始化方法initWithString:priority:frontSeparator:backSeparator:,否则编译器直接就让你通过不了.
  2. load是通常被用来初始化添加swizzle代码的地方.
  3. 因为你使用一个category来交换方法,在category中建立实例变量是稍微有些复杂的.你可以通过associated objects给一个已存在的类添加实例变量,但这个是你自己应该掌握的内容.在这里,我们直接使用一个static NSArray来保持一个对象继续存在,即使它所在的方法已经执行完毕了.
  4. 您可以使用相同的static伎俩,使得index也如此。
  5. 使用dispatch_once初始化歌词NSArray
  6. 递增index,如果越界了,就重置它
  7. 检测插件是否是可用状态,如果是,那就更改string参数,否则,使用默认的参数

正如你所看到的,使用正确的工具可以帮助你快速定位你想要的东西.然而,要达到这个目的,却有不止一种方法 - 你将会发现,通过堆(heap)分析照样可以定位到你所感兴趣的内容

通过堆(heap)来重新寻找IDEActivityView


在本节中,你将会重新搜寻Xcode的IDEActivityViewIDEActivityReportStringSegment,你将会使用一种略微不同的方式来达到这一目的.

到目前为止,你都是通过自上而下的方式来进行定位的:你先找到一个可能的class,然后以某种方式在内存中寻找它的一个实例对象,再查找它的成员方法和属性.其实,以相反的方向来进行,似乎也是可行的,就是沿着参考链从下而上去寻找目标对象,直到找到它.

很幸运,Xcode中附带了一些Python脚本,让你能够做到这点!

结束掉终端中所有的已存在的LLDB和Dtrace的会话,然后重启Xcode.开始一个新的Xcode和LLDB会话,再一次执行LLDB附着到Xcode上的命令,然后,添加下面的命令脚本:

1
(lldb) pro at -n Xcode 
(lldb) command script import lldb.macosx.heap
"malloc_info", "ptr_refs", "cstr_refs", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.

这个脚本加载了一系列非常有用的方法到LLDB进程中.

  • malloc_info: 展示当前堆得对象的详细信息.尤其是结合MallocStackLogging环境变量的时候及其有用!
  • ptr_refs: 给它一个堆空间中的一个内存地址,它可以找到指向这个内存地址的其它对象.
  • cstr_refs: 参数类型是char * ,查找它在内存中出现的地方.
  • objc_refs: 找到内存中指定的类的实例

你可能想先了解了解最容易的objc_refs.

在LLDB中,通过以下脚本来找寻所有的IDEActivityView类的实例变量:

1
(lldb) objc_refs -o IDEActivityView
Searching for all instances of classes or subclasses of "IDEActivityView" (isa=0x10fffe650)
0x00007faaee9cbf30: malloc(   304) -> 0x7faaee9cbf30 IDEActivityView.DVTLayerHostingView.NSView.NSResponder.NSObject.isa
<IDEActivityView: 0x7faaee9cbf30>

这个脚本输出了内存中所有的IDEActivityView类的实例.其中的 -o 参数表示输出对象,也就是会调用对象的 description 方法

通过这个简单地额脚本,你就可以在内存中找属于指定类的的实例对象.这招也适用于继承于该对象的子类.

yohunl备注
很遗憾,在我电脑上,这句怎么都查询不到

1
(lldb) objc_refs -o IDEActivityView
error: libarclite_macosx.a(arclite.o) failed to load objfile for /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_macosx.a
error: libarclite_macosx.a(arclite.o) failed to load objfile for /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_macosx.a
Searching for all instances of classes or subclasses of "IDEActivityView" (isa=0x10ada9328)

试了很多次,都没能得到所期望的东西,但我相信这个方法是可以的

通过上一步 objc_refs发现的内存地址,你可以采用如下的脚本来展开一个IDEActivityView实例的成员变量:

1
(lldb) malloc_info -t 0x7faaee9cbf30
0x00007faaee9cbf30: malloc(   304) -> 0x7faaee9cbf30 IDEActivityView.DVTLayerHostingView.NSView.NSResponder.NSObject.isa (IDEActivityView) *addr = {
  DVTLayerHostingView = {
    NSView = {
      NSResponder = {
...

这还不是它能做到的全部!你甚至可以用它,通过指定的内存地址来获得其它对象的引用:

1
(lldb) ptr_refs  0x7faaee9cbf30
0x00007faaeed9a308: malloc(    16) -> 0x7faaeed9a300 + 8     
0x00007faaeedb4148: malloc(   176) -> 0x7faaeedb4140 + 8      IDEActivityViewBackgroundButton.NSButton.NSControl.NSView.NSResponder._nextResponder
0x00007faaeedb4190: malloc(   176) -> 0x7faaeedb4140 + 80     IDEActivityViewBackgroundButton.NSButton.NSControl._aux.NSObject.isa
0x00007faaeedb4928: malloc(   160) -> 0x7faaeedb4920 + 8      NSView.NSResponder._nextResponder
...

注意:这些命令,如果结合 im loo -rn {classmethod regex} 方式来查询,甚至可以帮助你快速的定位到,并且探查到在内存中存活的该类的所有实例.当然了,尝试之前,请确定你的LLDB还是暂停状态,否则的话,这些命令是不会起作用的.

你设置可以更进一步,有一个环境变量MallocStackLogging,它可以在你初始化任何一个对象的时候用于栈(stack)回溯.虽然很多引用都可以指向同一个对象,但是它可以指出来,哪个才是这个对象的”最终拥有者”. 这段还要再分析一下

由于上文的LLDB已经附着(attach)在xcode上,所以你要先杀掉Xcode进程,再通过带上MallocStackLogging环境参数的LLDB来重启它:

1
(lldb) pro kill 
Process 65196 exited with status = 9 (0x00000009)
(lldb) pro launch -v MallocStackLogging=1 -n
Process 67920 launched: '/Applications/Xcode.app/Contents/MacOS/Xcode' (x86_64)

注意:如果你比较懒,不断的杀掉LLDB/Xcode会话,launchctl是一个更加方便的启用环境变量MallocStackLogging的方式:

1
launchctl setenv MallocStackLogging 1

这个命令将通过launch创建的每一个进程的MallocStackLogging都设置成了true.所以你要记得,通过如下的命令来移除它们:

1
launchctl unsetenv MallocStackLogging

为了测试上面的内容,首先,确保你的Xcode的Edit菜单下的菜单项是 如下图所示的 Enable Rayrolling :
Enable_RayRolling-700x378.png

现在,打开任何一个Xcode工程,保证源码编辑窗口是打开的,编译,运行工程.伴随着工程的运行,你将会看到在编译完成和运行完成的时候,IDEActivityView(假装你不知道此时此刻,谁调用它)改变它的内容.

暂停执行这个Xcode工程,如果需要,使用下面的命令来重新加载LLDB的heap(堆)方法集:

1
(lldb) pro i 
(lldb) command script import lldb.macosx.heap
"malloc_info", "ptr_refs", "cstr_refs", and "objc_refs" commands have been installed, use the "--help" options on these commands for detailed help.

提醒:如果你对多次输入这个命令感到厌烦,你可以去网上了解下怎么在 ~/.lldbinit 方法中建立 command aliases (命令的别名,或者缩写).这个在 ~/.lldbinit 文件的内容,在每次创建一个LLDB的会话实例的时候,都会被加载.

现在,使用cstr_ref方法,给它传递Xcode标题栏的字符串内容.举个例子,如果这个工程的名字叫Rayrolling,那么你的Xcode的标题栏上显示的应该是诸如:”Running Rayrolling : Rayrolling:”此类的字符串:

1
(lldb) cstr_ref "Running Rayrolling : Rayrolling" 
0x0000000118cb21d0: malloc(    32) -> 0x118cb21d0
0x000000012079ae21: malloc(    48) -> 0x12079ae10 + 17     __NSCFString1 bytes after __NSCFString
0x00000001207a3bb3: malloc(    64) -> 0x1207a3b90 + 35     __NSCFString19 bytes after __NSCFString
0x00000001223834d1: malloc(    48) -> 0x1223834c0 + 17     __NSCFString1 bytes after __NSCFString
0x000000011f9126a1: malloc(    48) -> 0x11f912690 + 17     __NSCFString1 bytes after __NSCFString

正如你所看到的那样,主要的参考信息一般都是 NSString类型的参考,多数情况下,你并不能从字符串本身得到更多的信息.所以你可能想了解这样字符串是怎样被创建的?因为有MallocStackLogging,做到这点,相当容易!

1
(lldb) cstr_ref "Running Rayrolling : Rayrolling"  -s

这个脚本输出堆栈帧的信息,当这个char * 被创建的时候.

通过所有的堆栈信息去跟踪实例变量,由于他们都是在内存中,内存地址的数量和其在内存中的相对位置都有可能不同,这些都取决于你和Xcode的互动方式的不同而不同.

通过阅读分析堆栈的跟踪信息,你将会获得一组类,这些类就是负责Xcode中这方面的.

举例来说,取决于你运行的这些LLDB查询命令,你可能会看到这些class,如: IDEActivityScrollingTextLayer , IDERunOperation , IDEActivityView 等等诸如此类的class.

你还可以使用 ptr_ref 来作用于 cstr_ref 输出的信息,来看那些对象保留了这个string对象的引用.

繁杂的随机探索技巧


在Xcode中寻找正确的api是很复杂的,因为你是想在成千上万的类和方法中寻找符合条件的几个类而已.

使用LLDB的正则表达式,可以帮助你更好的找到它们.某些时候,最好的获取信息的地方是代码中使用的单例(Singleton),查看Xcode中存在的单例,看看是否值得进一步深入的单例.
当LLDB附着到Xcode,并且处于暂停状态时候,输入以下命令:

1
(lldb) i loo -rn "\+\[IDE.*\ shared"

这条命令将会查找以IDE开头的类名,并且以”shared”开头的类方法.这条命令等价于 target modules lookup -rn "\+\[IDE.*\ shared",-r是指后面的是正则表达式

另外一个吸引力的替代方案是:

1
(lldb) i loo -rn "\+\[[A-Za-z]*\ [A-Za-z]*\]"

它将会输出所有的没有参数的类方法.

正如你前面看到的,你可以很容易的找出你刚兴趣的类,通过给一个名字给上面提到的Python脚本,就可以搜索到.另外还有一个流行的工具,可以用来搜索类和类方法,这个工具的名字叫class-dump,它可以在 这里下载.它是LLDB命令im loo -rn {regex} 的另一种替代方式,因为它(Class-dump)可以输出更加整洁,清晰的头文件形式的输出.

当然了,class-dump也有小缺点,它的缺点就是你必须限制你的搜索在指定的framework或者plugin中,而LLDB的image lookup 命令搜索的是整个的Xcode进程中的framework和plugin!也就是image lookup的搜索范围更大,更广.

下一步该做什么?


这里是第二部分最后完成的domo工程.
在这个教程中,你学习到了最基本的DTrace技能,并且学习和使用了一些你可以得到的高级LLDB方法.
如果你想了解更多的关于DTrace的知识,你可以阅读另一篇obcj.io上的优秀文章,还有就是官方的 DTrace文档

在本系列教程的第三部分,我们将再一次关注 汇编,你将会学到另一种灵巧的工具,它的名字叫 Cycript.

文章目录
  1. 1. 开始啦
  2. 2. LLDB的BFF和DTrace
  3. 3. 使用Swizzle的时候了
  4. 4. 通过堆(heap)来重新寻找IDEActivityView
  5. 5. 繁杂的随机探索技巧
  6. 6. 下一步该做什么?
,