Objc与C(C++)之亲缘关系(一) Class

     众所周知,Objc是C的扩展,在Xcode中Objc和C甚至都可以混合编码。那么Objc与C到底是怎样的关系?编译器是如何处理Objc的呢?本系列文章的目的就是为了揭开这层被Apple封装起来的面纱,让我们知道在每一行Objc代码背后究竟执行了哪些指令,Objc在其华丽的外表下究竟隐藏了多少秘密。我相信只有知道这些,才能让iosers编写出更加优雅、稳定、高性能的程序,同时知道这些对逆向工程的同学们也会有莫大的帮助。
     我们将首先从最简单的NSObject类下手,看看他背后都隐藏了什么东西。本文将主要研究其内存实现机制。本文中大量的资料因此皆出自苹果给出的Runtime的开源代码:http://opensource.apple.com/source/objc4/objc4-532.2/runtime/ 
NSObject定义如下:
@interface NSObject <NSObject> {    Class     isa;
}
     其内部只有一个Class类型,其定义为:typedef struct objc_class *Class;哇喔,struct结构,如此熟悉的面孔,so,Class也就是C里面的一个结构指针,由此我们可以知道,当我们申请一个NSObject对象指针时,只是占用了4字节内存(本文所有涉及内存的部分均假设在32位系统下实现的)注意NSObject及其子类即不允许用sizeof操作符也不许直接声明对象(编译器报错:Interface type cannot be statically allocated),那么当使用[[obj alloc]init]之后呢?其实不过只是申请了内部对象的一些指针,也占用不了多少内存,只有对NSData,UIImage这种对象使用非init初始化时才是真正需要小心的时候。当然,这些已经超出本文内容范畴了。
     下面继续来看看这个objc_class结构是个神马东东,在objc_rutime.h中可以找到这个结构的定义,但是,亲,这个定义完全是用来坑爹的,你相信它你就错了。这个定义里有一句#if !__OBJ2__,人家也提醒了这个定义在Objc2.0中已经不用了,那究竟用的是什么呢?struct class_t,千万别问我为什么,因为我在runtime目录下找了半天也没找到下面这句:只能说要么是我眼拙,要么apple没把这句开源出来。
#if __OBJ2__
     typedef struct class_t *Class ;
#endif

     总之,把Class当作这个结构的指针就对了,事实胜于雄辩,后面会给出试验,现在来分析下这个结构是神马玩意儿。
     typedefstruct class_t {
    struct class_t *isa;          //指向自己的一个指针,暂时不知有何用,判断是不是类结构?maybe
    struct class_t *superclass;   //父类指针,如UIView class的话,改值指向UIResponder class地址
    Cache cache;      //函数缓存,为了减少函数查询时使用的时间
    IMP *vtable;    //虚函数表?
    uintptr_t data_NEVER_USE;  // class_rw_t * plus custom rr/alloc flags
    };
  哇噻,除了前2个指针,后面都不知道是神马东东,其实这个一个非常非常复杂结构,而且这个结构已经不是C了,完完全全一个C++的struct,结构函数有木有std::有木有(在objc_runtime_new.h文件中)。尼玛,一本本的书上写的都是:Objective-C是C的超集。坑爹啊,人家早就成C++的超集了,至少内部实现已经用上了C++!
     好了,吐槽完了,接着看这个结构,前面4个成员就不多说了,看注释吧。说下最后一个data_NEVER_USE,听名字是永远不用的数据,你要是信了你就真的输了。因为几乎所有的Class内部数据都在里面,Objective—C之所以在runtime时能随意的根据字符串构造一个类对象,随意的hook任何一个函数,其实现的原理全在这里面呢。这个data_NEVER_USE的末2位存放了几个标志符号,而剩余的位则存放了一个struct class_rw_t的地址,地址里面存放的是Class中定义的methods和properties以及protocols等信息——————的地址,额,又是地址,靠。
     为了方便大家了解这个结构,作者花了整整一个下午画了一个图,而且绘的奇丑无比,但是不妨碍大家了解这个结构的哈,由于图片太大,就放到最后了。
     下面我们来做点试验验证下这个结构是否正确,怎么验证呢,我们按照地址的结构取里面取一些字符串出来,看对不对。
     实验:根据对象取类名
#define GET_CLASS(address) (*(void**)address) #define GET_CLASS_RW_T(address)  (*(void**)((unsigned long)(GET_CLASS(address)+0x10) & ( ~(uintptr_t)3))) #define GET_CLASS_RO_T(address) (*(void**)(GET_CLASS_RW_T(address) + 0x8))
#define GET_CLASS_NAME(address) (char*)(*(void**)(GET_CLASS_RO_T(address) + 0x10))     
    //Usage:NSObject *obj=[[NSObject alloc]init];
    //char* className = GET_CLASS_NAME((__bridge void*)obj);
     想知道methods、properties和protocols的地址么?先来看下class_rw_t结构吧
typedefstruct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    union {
        method_list_t **method_lists;  // RW_METHOD_ARRAY == 1
        method_list_t *method_list;    // RW_METHOD_ARRAY == 0
    };
    struct chained_property_list *properties;
    const protocol_list_t ** protocols;
    struct class_t *firstSubclass;
    struct class_t *nextSiblingClass;
} class_rw_t;
信息出来了有没有,method_list、chained_properties_list和protocol_list_t,这还不是全部,结构名中的rw表示readwrite,即这里的东西都是可读可写的属性,其中有个class_ro_t结构,这个结构就是只读(readOnly)的了。来看一下:

typedefstruct class_ro_t {
    uint32_t flags;          //标志
    uint32_t instanceStart;     //??
    uint32_t instanceSize;      //??
    const uint8_t * ivarLayout; //??
    constchar * name;     //类名
    const method_list_t * baseMethods;     //只读方法
    const protocol_list_t * baseProtocols;     //只读协议
    const ivar_list_t * ivars;          //??
    const uint8_t * weakIvarLayout;     //??
    const property_list_t *baseProperties;     //??
} class_ro_t;
其实这个结构作者暂时只知道3个字段的含义,如上注释,ivar猜了半天也没猜出来是干嘛的。其中有个类名,在我最初做试验的时候,就是通过找到它来验证解析方法正确与否的,后面的程序里我还用它来验证给定的地址是否是一个Class结构的。   好了,结构基本上介绍玩了,下面根据class的结构讨论下OjbectiveC中几种东西的实现原理。
1,属性(Properties) 
     Objc里的属性与C结构(或者C++类)的成员变量不是一回事。具体表现有:
     Properties不能使用sizeof操作符,也就是说它可能不是成员变量那样通过基地址加一个偏移量就可以访问的地址了。
     Properties有setter和getter2个方法实现存取操作。
     Properties有个name的字段,这个字段是放在内存里的,这也就是NSObject中setValue:forKey得以实现的原因。
typedefstruct objc_property {
    constchar *name;
    constchar *attributes; 
} property_t;
2,方法(methods)
     用过iOS中hook的人都应该发现了,这种hook要比windows下hook起来要方便的多,通过简单使用<objc/runtime.h>提供的函数
class_getInstanceMethod、method_getImplementation、class_replaceMethod等函数即可。而实际上,这种hook的代价也是想到小的。看下面的结构,在C语言中就是加一个method_t对象,交换了2个IMP地址而已。
     此外,值得一说的当我们用[obj method]调用方法的时候,要跳转的地址在IMP没错,但是IMP是C中的(void)(*IMP)(void)。函数的参数与返回值都是void,而原本method的类型保存在了types字符串中的,通过正则解析这个字符串才能正确调用。这代价似乎比C中的要高很多啊,大概正因如此,所以没个Class结构中才会有个Cache结构来缓存一些方法,以减少这个代价。
struct method_t {
    SEL name;
    constchar *types;
     IMP *imp;

3.协议(protocols)
     协议结构如下,协议名称、链表、4类方法链表和一个属性。这部分作者未能提供什么有价值的信息,倒是希望有人能指点指点。在后面的程序中,我都还没有正确解析这个结构,连name都取不到,原因未知。这里有个有趣的字段“实例属性”,我尝试在协议中定义了一个@property编译并未报错,但运行时crash了。
typedefstruct protocol_t {
    id isa;
    constchar *name;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    constchar **extendedMethodTypes;

4,category(类别?好别扭,真心不知道该如何用中文称呼它)
     上面的结构中似乎并未看到category字段,那么category与Class之间的是如何关联的呢?在看到下面结构之前,我一直以为category也会象协议一样包含在Class里面,但实际上,category中包含了一个cls的指针。这里也没什么过多想说的,大家就看一下这个结构吧,category与class的关系需要读了http://opensource.apple.com/source/objc4/objc4-532.2/runtime/objc-runtime-new.mm才能知道,我就不误导人了,等以后有时间了再把这里补齐吧。
typedefstruct category_t {
    constchar *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;
     
总结,Class存储了大量的字符串信息来支持Objc的runtime功能。而且这些字符串属于常驻内存性的,因此,我们常见的内存不足crash,或许可以在软件架构时期通过减少可有可无的类、属性、方法、协议等信息节约那么一点点内存。要知道iphone硬件内存有128M,但程序员使用的只有30M,其中有多少M是用了存储这些类结构信息的我不知道,但是在运行时查看结构的时候,经常能看到连续的64K都是可以识别的字符串。
     文章的最后给一个作者平时玩的代码地址,里面有使用C来解析Class结构的部分实现。相信运行过之后你能看到更多的东西。地址:https://github.com/jiazhh/FunnyPlay/tree/version0.0 

 

PS:最后矫正一下,图片中class结构的大小是14,偏移地址也有些错误。鉴于作者实在对绘图表示压力山大,若对您造成误解,敬请见谅。 

 

posted on 2013-09-09 01:36  小朝  阅读(730)  评论(0编辑  收藏  举报