OC类底层探索 — 类的结构分析

万物皆对象,OC中的类实际上也是一种对象,也就是类对象。
类的真正类型为objc_class,查看objc_class是一个结构体,struct objc_class: objc_object 继承于objc_object

一、类的结构分析

1. 寻找类的结构

  • OC main.m
#import 
#import "DZPerson.h"
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZPerson *person = [DZPerson alloc];
        Class pClass     = object_getClass(person);
        NSLog(@"%@ - %p",person,pClass);
    }
    return 0;
}

通过OC对象底层探索 — isa的初始化和指向分析分析可知,打印第一个地址一定是isa,第二个地址是superclass,它代表的是继承关系,证明了DZPerson是继承自NSObject

我们用clang命令将OC代码转化为C++代码,即.cpp文件,查看底层:
Dezi$ clang -rewrite-objc main.m -o main.cpp

  • C++ main.cpp 关键代码如下:
typedef struct objc_class *Class;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
        Class pClass = object_getClass(person);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_main_4dc184_mi_9,person,pClass);

    }
    return 0;
}

通过上边的C++代码,我们可以看到pClassClass类型。
Class的真正类型是objc_class。下面我们从源码继续探索objc_class

2. 类结构

objc_class 源码:

  • 旧版本,在OBJC2中已废弃
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
  • 新版本,当前源码使用的版本:找到下面的结构体,我们会发现objc_class继承自objc_object,然后进一步又验证了 class 实际上也是一种对象,即类对象。
typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8,Class结构体指针
    cache_t cache;    // 16,cache_t结构体,formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    ...省略...
}

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

这段代码是objc_class继承objc_object的代码,所以objc_class中也有 isa 结构体。objc_objectobjc_class之间的关系如下图所示:

3. 类结构成员分析

  • Class ISA
    隐藏的成员Class继承自父类,是指向元类的指针,占8字节。
struct objc_object {
private:
    isa_t isa;
  ...省略其他的信息...
};
  • Class superclass
    当前类的父类,占8字节。

  • cache_t cache
    用于缓存指针和vtable(formerly cache pointer and vtable),占16字节。

truct cache_t {
    struct bucket_t *_buckets;  // 指针,占8字节
    mask_t _mask;               // 占4字节
    mask_t _occupied;           // 占4字节
    ...省略...
};
  • class_data_bits_t bits
    是一个结构体,类相关信息在前面三个成员里面都没找到,由此我们分析类的成员变量、方法应该都在这个结构体里面,我们继续探索。

二、探索类的属性存储properties

  • 创建DZPerson类,添加成员变量hoppy和属性nickName,添加实例方法sayHello和类方法sayHappy,然后进行断点调试:
@interface DZPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZPerson *person = [DZPerson alloc];
        Class pClass     = object_getClass(person);
        NSLog(@"%@ - %p", person, pClass);
    }
    return 0;
}
1. x/4xg打印pClass(可参考Xcode控制台常用调试命令),第一块内存是isa,第二块内存是superclass,第三块内存按照前文应该是cache_t,第四块内存为空
// x/4xg 打印pClass
(lldb) x/4xg pClass
0x1000023b0: 0x001d800100002389 0x0000000100b38140
0x1000023c0: 0x00000001003db260 0x0000000000000000
(lldb) 

按照Class结构体的成员变量顺序,以及内存对齐原则,我们用指针偏移的方法,来找一找第四块内存bits,看看bits里边存的到底是什么?

2. 根据首地址用指针偏移找到bits内存地址

首地址为0x1000023b0,其中isa指针占8字节,superClass占8字节,cache占16字节,按照内存对齐原则,用首地址偏移32个字节得到0x1000023d0应该就是bits的内容,但是打印结果和我们想的不太一样。
注意:这里需要强转一下,打印得到class_data_bits_t内存地址:

(lldb) p 0x1000023d0
(long) $1 = 4294976464
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $2 = 0x00000001000023d0
3. 根据bits.data()找到bits中存储的内容
struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8
    cache_t cache;    // 16 不是8         // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ...省略...
}

class_rw_t是类中存储属性和方法的地方,内部实现返回的是bits.data(),我们调用一下data方法得出一个class_rw_t类型的指针:

(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000100f92f90

我们对指针地址直接取值,结果如下:

(lldb) p *$3
(class_rw_t) $4 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002308
  methods = {
    list_array_tt = {
       = {
        list = 0x0000000100002240
        arrayAndFlag = 4294976064
      }
    }
  }
  properties = {
    list_array_tt = {
       = {
        list = 0x00000001000022f0
        arrayAndFlag = 4294976240
      }
    }
  }
  protocols = {
    list_array_tt = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}

我们可以看到熟悉的methods、properties、protocols等等。
对照源码,进一步验证:

4. 打印properties
(lldb) p $4.properties
(property_array_t) $5 = {
  list_array_tt = {
     = {
      list = 0x00000001000022f0
      arrayAndFlag = 4294976240
    }
  }
}

打印properties,得到property_array_t,它是一个二维数组,继续打印数组里的值:

(lldb) p $5.list
(property_list_t *) $6 = 0x00000001000022f0
(lldb) p $6.first
(property_t) $7 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  Fix-it applied, fixed expression was: 
    $6->first

到这里,我们找到了属性nickName

三、探索类的成员变量存储ro

根据上边,我们已经知道properties存放的是类的属性,结合class_rw_t里的方法名称,我们尝试探索一下ro部分

1. 探索ro

我们重新运行代码,然后直接取 $3.ro 的值,得到class_ro_t结构体,我们可以看到ivars,根据名字成员变量应该在这里边:

(lldb) p $3.ro
(const class_ro_t *) $4 = 0x0000000100002308
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f89 "\x02"
  name = 0x0000000100001f80 "DZPerson"
  baseMethodList = 0x0000000100002240
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000022a8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022f0
}
2. 打印ivars
  • 我们找到 ivars 并打印内容,找到NSString类型的成员变量hobby
(lldb) p $5.ivars
(const ivar_list_t *const) $6 = 0x00000001000022a8
(lldb) p *$6
(const ivar_list_t) $7 = {
  entsize_list_tt = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100002378
      name = 0x0000000100001e64 "hobby"
      type = 0x0000000100001fa6 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
  • 但是我们发现ivar_list_t中的count = 2 ,而我们只定义了一个成员变量hobby,所以我们继续往下打印:
(lldb) p $7.get(1)
(ivar_t) $8 = {
  offset = 0x0000000100002380
  name = 0x0000000100001e6a "_nickName"
  type = 0x0000000100001fa6 "@\"NSString\""
  alignment_raw = 3
  size = 8
}

我们发现了一个_nickName成员变量,这也印证了 属性会自动生成对应的带下划线的成员变量

四、探索类的对象方法存储methods

1. 探索methods

按照上边的方法,我们继续打印methods.list

(lldb) p $3.methods
(method_array_t) $4 = {
  list_array_tt = {
     = {
      list = 0x0000000100002240
      arrayAndFlag = 4294976064
    }
  }
}
(lldb) p $4.list
(method_list_t *) $5 = 0x0000000100002240
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001b90 (LGTest`-[DZPerson sayHello] at DZPerson.m:13)
    }
  }
}

通过打印,我们看到定义的对象方法sayHello存储在 methods 里边。

2. 打印list中的所有方法

我们看到count = 4,而上边只显示了一个first,那么继续打印出methods中的所有方法:

(lldb) p $6.get(1)
(method_t) $7 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8b "v16@0:8"
  imp = 0x0000000100001c60 (LGTest`-[DZPerson .cxx_destruct] at DZPerson.m:11)
}
(lldb) p $6.get(2)
(method_t) $8 = {
  name = "setNickName:"
  types = 0x0000000100001f9b "v24@0:8@16"
  imp = 0x0000000100001c20 (LGTest`-[DZPerson setNickName:] at DZPerson.h:16)
}
(lldb) p $6.get(3)
(method_t) $9 = {
  name = "nickName"
  types = 0x0000000100001f93 "@16@0:8"
  imp = 0x0000000100001bf0 (LGTest`-[DZPerson nickName] at DZPerson.h:16)
}

由打印我们发现其中并没有我们定义的类方法,而是C++的析构函数destruct方法(系统默认添加),以及属性 nickNamesettergetter方法。
由此我们印证了 属性的定义,会自动生成setter和getter 而成员变量没有生成setter和getter。

五、探索类中类方法的存储

通过对 bits 的探索,我们找到了属性的存储位置、成员变量的存储位置、实例方法的存储位置,但是唯独没有找到类方法sayHappy存储在哪里。

1. 在元类中找到类方法

经过验证,类方法其实存储在元类里边,而探索元类,我们就必须知道类的isa指针是指向元类的,通过与上0x00007ffffffffff8ULL就可以找到元类,这个之前已经做过探索 OC对象底层探索 — isa的初始化和指向分析,此处不再叙说。

(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b38140
0x1000023c0: 0x0000000102121570 0x000000040000000f
(lldb) p/x 0x001d800100002389 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100002388
(lldb) po 0x0000000100002388
DZPerson
// 找到元类之后,继续按照之前找ro的步骤执行
(lldb) x/4gx 0x0000000100002388
0x100002388: 0x001d800100b380f1 0x0000000100b380f0
0x100002398: 0x000000010237f7c0 0x0000000200000003
(lldb) p (class_data_bits_t *)0x1000023a8
(class_data_bits_t *) $9 = 0x00000001000023a8
(lldb) p $9->data()
(class_rw_t *) $13 = 0x000000010237f720
(lldb) p $13->ro
(const class_ro_t *) $15 = 0x00000001000021f8
(lldb) p *$15
(const class_ro_t) $16 = {
  flags = 389
  instanceStart = 40
  instanceSize = 40
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100001f80 "DZPerson"
  baseMethodList = 0x00000001000021d8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
}
2. 打印baseMethodList

我们继续打印baseMethodList,至此我们终于找到了类方法sayHappy

(lldb) p $16.baseMethodList
(method_list_t *const) $17 = 0x00000001000021d8
(lldb) p *$17
(method_list_t) $18 = {
  entsize_list_tt = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`+[DZPerson sayHappy] at DZPerson.m:17)
    }
  }
}

六、总结

  1. 万物皆对象,类的本质就是对象,也叫类对象。
  2. 类和元类是在 编译期 创建的,即在进行alloc操作之前,类和元类就已经被编译器创建出来了。
  3. 类在 class_rw_t 结构体中存储了编译时确定的成员变量、属性、对象方法、协议等。
  4. 属性的定义,会 自动生成 对应的 带下划线的成员变量settergetter 方法。
  5. 成员变量不会生成 settergetter 方法。
  6. 对象方法(实例方法) 存储在 中,而 类方法 则是 以实例方法形式 存储在 元类 中。
  7. sel:是类成员方法的指针(是一个方法编号),IMP:是一个保存了方法地址的函数指针。

你可能感兴趣的:(OC类底层探索 — 类的结构分析)