类是我们iOS开发中的最小单元,作为一个合格的开发者,不仅要会简单的使用,更要对它的内部结构做到心中有数,这样我们才能用的更好!
一、isa和元类
在前面对于对象的探究中,我们了解到isa是类中很重要的东西,那么接下来,我们将用一个例子去探索说明isa和元类之间的关系。
-
1、新建一个Person类,然后在main.m中,打印如下:
注:object_getClass 是runtime用于获取类方法的一个函数
通过添加断点的方式,我们可以在控制台,分别打印:p1,p2,p3,p4,p5的地址值。
其中:p1为实例对象,p2为LGPerson对象,p3为一个未知类,p4和p5地址一样,打印结果为NSObject。
-
2、那么,p3是一个什么类呢?
我们把编译的二进制machO文件,使用MachOView分析查看。
通过我们对符号表的查看,发现,除了常规的__OBJC_CLASS_RO__LGPerson类之外,系统又创建了一个__OBJC_METACLASS_RO__LGPerson。
由此,我们猜想,未知类就是就是上面的p3的类型。
-
3、如果 p3可以通过runtime的函数找到,那么,我们就顺藤摸瓜,通过runtime源码分析,类的内部机制。
-
4、原来,实例对象去查找类,是通过isa找到的,那么接着往下找内部实现。
看到这里,查找的核心逻辑就找到了,接下来我们进行分析: if (fastpath(!isTaggedPointer())) return ISA();
① 先判断快速查找,如果存在isa,就返回。如果没有isa,就进行慢速查找.
② 然后slot = ptr & 0xf,然后根据得到的地址,取出objc_tag_classes中的类。
③ 最后判断取出的类,是不是Class类型,并且是objc_class类型,如果符合就对slot操作,先向右位移4个字节,然后对_OBJC_TAG_EXT_SLOT_MASK进行与预算。
④ 根据位移后的地址,取出对应的类。
-
5、我们对isa进行MASK的与运算,如下:
(lldb) p/x p1
(LGPerson *) $0 = 0x000000010044c700
(lldb) p/x p2
(Class) $1 = 0x00000001000081f8 LGPerson
(lldb) p/x p3
(Class) $2 = 0x00000001000081d0
(lldb) p/x p4
(Class) $3 = 0x00007fff908200f0
(lldb) p/x p5
(Class) $4 = 0x00007fff908200f0
(lldb) x/4gx p1
0x10044c700: 0x001d8001000081f9 0x0000000000000000
0x10044c710: 0x50626154534e5b2d 0x65695672656b6369
(lldb) p/x 0x001d8001000081f9 & 0x00007ffffffffff8
(long) $6 = 0x00000001000081f8
(lldb) x/4gx $6
0x1000081f8: 0x00000001000081d0 0x00007fff90820118
0x100008208: 0x00007fff690cb140 0x0000801000000000
(lldb) p/x 0x00000001000081d0 & 0x00007ffffffffff8
(long) $7 = 0x00000001000081d0
(lldb) x/4gx $7
0x1000081d0: 0x00007fff908200f0 0x00007fff908200f0
0x1000081e0: 0x0000000100455dc0 0x0001e03100000003
(lldb) p/x 0x00007fff908200f0 & 0x00007ffffffffff8
(long) $8 = 0x00007fff908200f0
(lldb) x/4gx $8
0x7fff908200f0: 0x00007fff908200f0 0x00007fff90820118
0x7fff90820100: 0x00000001004077c0 0x0001e03100000003
(lldb) p/x 0x00007fff908200f0 & 0x00007ffffffffff8
(long) $9 = 0x00007fff908200f0
通过分析上述,可以得出以下结论:
p1 = 6 = 0x00000001000081f8 = p1 (InstanceOfClass)
p2 = 7 = 0x00000001000081d0 = p2 (LGPerson)
p3 = 8 = 0x00007fff908200f0 = p3 (LGPerson_MetaClass)
p4 = 9 = 0x00007fff908200f0 = p5 (NSObject)
上述的运算,都通过isa和mask进行运算,找到p1,p2,p3,p4之间的关系。
由此我们可以推理,p1(实例对象)通过isa可以获取p2(类对象),
p2(类对象)通过isa可以获取到p3(MetaClass元类对象),
p3(元类对象)通过isa又获取到p4(根元类对象),
p4通过isa获取到本身。
-
6、于是,我们可以得出,isa与实例对象、类、元类、根类之间有一个链式走位的关系!接下来我们就继续探索!
二、isa的走位图和继承链
-
1、我们通过分析上述结论,可以得出以下isa的关系图。
-
2、了解了isa的走位图之后,我们发现还有一种类继承的关系。
通过API层面的调试,我们可以看出superclass的继承链关系,对照苹果官方给出的继承走位链,我们可以进一步认识实例化对象、类、元类、根元类、根根元类之间的联系。
分析:
① 图中的关系是由superclass与isa一起关联起来的
② superclass表示了继承的关系,isa表示了各个对象间的关联关系
-
3、这里我们就简单分析到这里,关于更多isa和superclass的内容,将在后面专题分析。认识了isa之后,接下来我们就来了解下类的结构。
三、类的结构分析
1、源码分析
-
1.1、不同代码环境的命名
OC 环境 objc环境 NSObject objc_object Class objc_class ... ... -
1.2、查看objc_class的结构
我们提取出关键部分的代码,便于我们接下来分析类的结构( objc_class的结构 )
#pragma mark - objc_class的结构
struct objc_class : objc_object {
// Class ISA;
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //8bit
Class superclass; //8bit
cache_t cache; //8bit
class_data_bits_t bits; //8bit 关键数据
...
class_rw_t *data() const {
return bits.data(); //关键数据
}
...
}
经分析,isa为结构体指针,占用8字节;superclass也是结构体指针8字节,cache_t需要分析下结构,如下:其中
#pragma mark - cache_t
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
-
1.3、查看class_data_bits_t中的数据
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
class_data_bits_t中的一些关键数据格式为class_rw_t,接下来分析class_rw_t。
#pragma mark - class_rw_t
struct class_rw_t {
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
const class_ro_t *ro() const {...} //ro信息
const method_array_t methods() const {...} //方法数组
const property_array_t properties() const {...} //属性数组
const protocol_array_t protocols() const {...} //协议数组
}
接下来分别查看class_ro_t、method_array_t、property_array_t、protocol_array_t的结构:
#pragma mark - class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
#pragma mark - method_array_t
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{}
#pragma mark - 方法体method_t的结构
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
}
#pragma mark - property_array_t
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>{}
#pragma mark - property_t
struct property_t {
const char *name;
const char *attributes;
};
#pragma mark - protocol_array_t
class protocol_array_t :
public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>{}
#pragma mark - protocol_list_t
struct protocol_list_t {
// count is pointer-sized by accident.
uintptr_t count;
protocol_ref_t list[0]; // variable-size
size_t byteSize() const {
return sizeof(*this) + count*sizeof(list[0]);
}
...
}
通过class_rw_t的结构,我们可以知道,内部存储的信息,主要有方法、属性、协议。然后我们再看另外一个成员变量class_ro_t *ro。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList; //方法列表
protocol_list_t * baseProtocols; //协议列表
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout; //成员变量布局
property_list_t *baseProperties; //属性列表
-
1.4、既然类的基本布局信息,已经明白,那么我们通过实际项目案例去验证下。下面将通过LLDB调试,分析方法、属性、成员变量的分布情况。
四、指针和内存平移
-
1、新建Person类,提供方法、属性、成员变量,然后再main.m中调用,如下:
-
2、下面就开始LLDB,分步分析过程:
① 通过 x/4gx 获取Person类的内存结构信息
(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x0000000100357140
0x100008418: 0x0000000101236020 0x0002802800000003
② 获取到首地址,根据上面objc_class结构,通过32位地址偏移,可以得到bits数据,class_data_bits_t类型。
(lldb) p/x 0x100008428 // = 0x100008408 + 0x20
(long) $1 = 0x0000000100008428
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100008428
③ 拿到bits之后,通过内部成员变量data,获取内部结构为class_rw_t的值
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101235fe0
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000456
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
然后,通过 class_rw_t 的结构,我们可以获取其中的方法、属性、协议信息。
//获取类内部存储的方法信息
(lldb) p $4.methods()
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000081d0
}
arrayAndFlag = 4295000528
}
}
}
(lldb) p $5.list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
ptr = 0x00000001000081d0
}
(lldb) p $6.ptr
(method_list_t *const) $7 = 0x00000001000081d0
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
(lldb) p $8.get(0).big()
(method_t::big) $9 = {
name = "saySomething"
types = 0x0000000100003f69 "v16@0:8"
imp = 0x0000000100003c60 (KCObjcBuild`-[LGPerson saySomething])
}
...
上面是获取的类的方法信息,以下是获取属性信息
(lldb) p $4.properties()
(const property_array_t) $12 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x00000001000082d0
}
arrayAndFlag = 4295000784
}
}
}
(lldb) p $12.list
(const RawPtr<property_list_t>) $13 = {
ptr = 0x00000001000082d0
}
(lldb) p $13.ptr
(property_list_t *const) $14 = 0x00000001000082d0
(lldb) p *$14
(property_list_t) $15 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
(lldb) p $15.get(0)
(property_t) $16 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $15.get(1)
(property_t) $17 = (name = "age", attributes = "Ti,N,V_age")
...
通过本案例的LLDB,我们可以查找到类中包含的属性和实例方法。
类中存储的成员变量和对象方法,又存储在哪里呢?接下来让我们继续探索。
-
3、LLDB分析类中的成员变量和对象方法
为了节省篇幅长度,这里就不分段介绍,直接展示打印结果:
(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x0000000100357140
0x100008418: 0x000000010067d9b0 0x0002802800000003
(lldb) p/x (class_data_bits_t *)0x100008428
(class_data_bits_t *) $1 = 0x0000000100008428
(lldb) p/x $1->data()
(class_rw_t *) $2 = 0x000000010067d970
(lldb) p/x $2->ro()
(const class_ro_t *) $3 = 0x0000000100008188
(lldb) p/x *$3
(const class_ro_t) $4 = {
...
baseMethodList = 0x00000001000081d0
baseProtocols = 0x0000000000000000
ivars = 0x0000000100008268
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000082d0
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p/x $4.ivars
(const ivar_list_t *const) $5 = 0x0000000100008268
(lldb) p/x *$5
(const ivar_list_t) $6 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 0x00000020, count = 0x00000003)
}
(lldb) p/x $6.get(0) // 拿到成员变量 hobby 调试结束
(ivar_t) $7 = {
offset = 0x00000001000083a0
name = 0x0000000100003e18 "hobby"
type = 0x0000000100003f55 "@\"NSString\""
alignment_raw = 0x00000003
size = 0x00000008
}
经过同样的方式,我们查看baseMethodList中的值,发现依然找不到对象方法的信息,猜想对象方法可能不存储在类中,我们知道有对象、类、元类,那么对象方法会不会存在元类中?
我们可以通过isa去找到元类,然后用相同的方式进行调试,获取到,由于步骤相同和方法相同,这里就不做演示。
五、总结
本篇文章,主要分析了类的结构,并通过LLDB调试的方式,分析了类的成员变量、属性、方法、协议等存储的位置,从底层的角度认识了类的存储过程。
写在最后,类的分析是一个系统的过程,先从基本的结构去分析,把握类的存储位置,为后面学习和认识类的相关操作做好扎实的基础!