大数据

开源:EMClassIntrospection —— 一款基于Runtime的Class调试工具

转载请注明出处:http://www.jianshu.com/p/c04d89546237
这是一款用于Class调试的工具,类似的项目有「DLIntrospection」
项目在「GitHub」中开源,已集成CocoaPods

导语

  在我的上一篇文章「一次失败的内存泄露监测工具设计」中,我遇到了由于NSStringNSArray等类簇造成的问题。经过各方请教,也经过sunny大神 @我就叫Sunny怎么了 的测试,给出的结果为:

NSArray alloc 后会走 placeholder 的 allocWithZone,拿几个对象的不可变数组 NSArrayI 为例,将调用它的 +new::: 方法,进而调用 CF 的 __CFAllocateObject2 来实例化,NSDictionary 也是同理。

  出于对这个问题的思考,上面结论中的__NSArrayI等为类簇私有的类,我们无法接触到这些类。那么我们该如何去研究这些类呢?于是就有了本次项目。本次项目是在我看完 @我就叫Sunny怎么了 周三的关于iOS面试的直播后,得到的灵感。在直播中他用到了一款名为DLIntrospectionNSObject-category去介绍了研究一道和block有关的题的方法。结束以后我便打算自己写一个原理类似的框架以便今后的研究。
  那么,EMClassIntrospection到底可以做什么呢?它可以让你看到当前Runtime上下文中注册的所有类和协议,包括私有类和私有协议。任意协议所包含的方法,以及任意类的继承关系、所包含的实例方法、类方法、属性。任意方法所包含的参数类型以及返回类型。只要你想,这些都可以在lldb控制台中打印出来。

实现原理

  利用Runtime接口来取得我们需要研究的类的协议、方法、属性等信息。
  其原理是NSObject的结构中存在方法、协议、属性的列表,利用接口我们可以轻易的获取这些信息。具体可以看 「神经病院Objective-C Runtime入院第一天——isa和Class」 中关于NSObject的分析。由于Objective-C中除了NSProxy之外的类都是NSObject的子类,利用这点我们可以做一些有趣的事情。
  关于接口返回的数据类型,由于类型经过了编码,暂时没有找到通过解析编码后的字符串完全还原出原类型的字符串的方法。关于数据类型仅做了简单的字符串解析,具体内容可以参考:「官方文档: Type Encodings」
  EMClassIntrospectionDLIntrospection最大的区别在于,使用时接受消息的对象不同,适合不同使用习惯的用户使用。DLIntrospectionNSObject添加了category,执行相关指令时是给NSObject发消息,而EMClassIntrospection的操作习惯更接近终端。在终端中需要cd进某个目录,此后做的事情是在这个目录下发生的。EMClassIntrospection同样是先要sObjectsClass设定一个目标类,此后执行例如打印方法列表、查看方法细节等命令都是以目标类为目标发生的。

使用方法

  在文章后面有工具中的接口列表,包含注释。这里举2个简单的例子来展示EMClassIntrospection的使用方法。

范例1

  以 @我就叫Sunny怎么了 的block题为例,如何使用EMClassIntrospection进行研究:

  有一个block,在其构造以后进入断点:

    id block = ^{
        NSLog(@"block get called");
    };

  进入断点后将该对象设置为目标,此后的命令将全部作用在目标类上:

(lldb) e [EMCI sObject:block]//可以使用短接口:[EMCI SO:block]

  看出该对象是一个__NSGlobalBlock__对象:

  接着,打印其继承关系:

(lldb) e [EMCI pInherit]//可以使用短接口:[EMCI PI]

  此时我们我们得到了block对象的继承关系:

  之后所有的命令都在lldb中,因此不再做截图。
  继承链路中有个NSBlock引起了我的注意,因此我想看一看NSBlock里都有些什么。因此我将NSBlock设置为目标,并打印了其所有的协议:

(lldb) e [EMCI sClass:@"NSBlock"]//可以使用短接口[EMCI SC:@"NSBlock"]
NSBlock > 
(lldb) e [EMCI pProtocol]//可以使用短接口[EMCI PP]
Class [NSBlock] conforms 1 protocols:
No.0: NSCopying
(lldb)

  协议中似乎没有什么特别的,接着打印类方法:

(lldb) e [EMCI pClassMethod]//可以使用短接口[EMCI PCM]
Class [NSBlock] has 2 class methods:
No.0: (+)alloc
No.1: (+)allocWithZone:
(lldb)

  类方法中似乎也没有我们要的东西,打印实例方法:

(lldb) e [EMCI pInstanceMethod]//可以使用短接口[EMCI PIM]
Class [NSBlock] has 4 instance methods:
No.0: (-)copy
No.1: (-)copyWithZone:
No.2: (-)invoke
No.3: (-)performAfterDelay:
(lldb)

  眼前一亮的是,实例方法中有个序号为2invoke方法,看看这个方法的细节:

(lldb) e [EMCI pInstanceMethodDetail:2]//可以使用短接口[EMCI PIMD:2]
[NSBlock invoke] > 
Method has 2 arguments:
No.0 : @ -> (id)
No.1 : : -> (SEL)
Return v -> (void)
(lldb)

  这是个有2个参数的方法,第一个参数为id,第二个参数为SEL。事实上这两个参数并不是传统意义上的参数,这跟objc_msgSend有关,具体可以看这篇文章中关于objc_msgSend方法的介绍:「神经病院Objective-C Runtime住院第二天——消息发送与转发」。事实上我们可以暂时忽略前两个参数,invoke方法是个没有参数,返回值为void的方法,因此我们可以直接调用:

    id block = ^{
        NSLog(@"Hello");
    };
    [block invoke];

  这个block自然而然地就被调用了:

范例2

  使用短接口研究NSMutableArray的构造过程,最终hook其构造消息:
  现已知NSMutableArray构造后会出现__NSArrayM类,hook__NSArrayM的构造方法就可以hook到NSMutableArray的构造消息了。
  进入断点后使用接口打印出所有NSMutableArray的子类:

(lldb) e [EMCI PS:@"NSMutableArray"]
No.0 : UITableViewVisibleCells
No.1 : NSArrayChanges
No.2 : NSConcreteArrayChanges
No.3 : NSKeyValueMutableArray
No.4 : NSKeyValueNotifyingMutableArray
No.5 : NSKeyValueIvarMutableArray
No.6 : NSKeyValueSlowMutableArray
No.7 : NSKeyValueFastMutableArray
No.8 : NSKeyValueFastMutableArray2
No.9 : NSKeyValueFastMutableArray1
No.10 : NSCFArray
No.11 : __NSArrayM
No.12 : __NSPlaceholderArray
No.13 : __NSCFArray
(lldb)

  设置__NSArrayM类为目标,查看其类方法,:

(lldb) e [EMCI SC:@"__NSArrayM"]
__NSArrayM > 
(lldb) e [EMCI PI]
__NSArrayM -> NSMutableArray -> NSArray -> NSObject
(lldb) e [EMCI PCM]
Class [__NSArrayM] has 3 class methods:
No.0: (+)automaticallyNotifiesObserversForKey:
No.1: (+)allocWithZone:
No.2: (+)__new:::
(lldb)

  在类方法中发现__new:::方法,其编号为2,查看其细节:

(lldb) e [EMCI PCMD:2]
[__NSArrayM __new:::] > 
Method has 5 arguments:
No.0 : @ -> (id)
No.1 : : -> (SEL)
No.2 : r^@ -> r(pointer)(id)
No.3 : Q -> (unsigned long long)
No.4 : B -> (C++ bool or a C99 _Bool)
Return @ -> (id)
(lldb)

  利用method swizzing等方法hook该方法,测试在构造时是否执行了该方法。

接口列表

  所有的接口都是类方法。每一个方法都有一个短接口,为该方法的首字母。长接口和短接口具体功能在下面列出:

  • (void)pAllClass;
    —— 打印所有已经在Runtime中注册的类。
  • (void)pSubclass:(NSString *)clsName;
    —— 打印 clsName 为名字的类的所有子类。
  • (void)pAllProtocol;
    —— 打印所有已经在Runtime中注册的协议。
  • (void)sObject:(NSObject *)obj;
    —— 将对象 obj 的类设置为以下命令的目标。
  • (void)sClass:(NSString *)clsName;
    —— 将以 clsName 为名字的类设置为以下命令的目标。
  • (void)sBack;
    —— 撤销目标。
  • (void)pInherit;
    —— 打印目标类的继承关系。
  • (void)pProtocol;
    —— 打印目标类遵守的协议。
  • (void)pProtocolDetail:(int)index;
    —— 打印目标类遵守的协议中第index个方法的细节。
  • (void)pInstanceMethod;
    —— 打印目标类的实例方法。
  • (void)pClassMethod;
    —— 打印目标类的类方法。
  • (void)pInstanceMethodDetail:(int)index;
    —— 打印目标类实例方法的第index个方法的细节。
  • (void)pClassMethodDetail:(int)index;
    —— 打印目标类类方法的第index个方法的细节。
  • (void)pInstanceVariable;
    —— 打印目标类的类变量。

  以下为短接口:

  • (void)PAC;
  • (void)PS:(NSString *)clsName;
  • (void)PAP;
  • (void)SO:(NSObject *)obj;
  • (void)SC:(NSString *)clsName;
  • (void)SB;
  • (void)PI;
  • (void)PP;
  • (void)PPD:(int)index;
  • (void)PIM;
  • (void)PCM;
  • (void)PIMD:(int)index;
  • (void)PCMD:(int)index;
  • (void)PIV;

结语

  在上篇文章遇到困难以后经过了一些思考,在殿神 「酷酷的哀殿」 的帮助下,开始接触这些本来没有接触的更深层次的东西。在拜读了几篇冰霜大神 「一缕殇流化隐半边冰霜」 关于Runtime的文章后,在DLIntrospection的启发下,实践出了EMClassIntrospection。这不仅是对自己知识的巩固,更是一种学习的方法。希望这篇文章能帮到需要帮助的人。

参考资料