前言
了解完对象的底层,知道isa指向的是类对象。那么类(Class)的本质究竟是什么?本文顺序isa的指向,探索类的继承链,和类对象的结构,并且尝试获取方法和变量的存放位置。
类的继承链
实例对象存储的isa指针,占8字节,并指向所属的类。这里通过3中不同的方式打印一下isa内容:
可以看到都是同个对象,这说明类对象有且仅有一个。它是Class类型的。
isa的指向
在源码中搜索一下:实际上类对象就是objc_class
这么一个结构体。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
那么为什么说类也是个对象?你看他有isa指针,还继承了objc_object
,这结构体同时也是对象的本质。
这就说明和对象一样了,不同的是,类对象的isa指针指向哪里呢?元类对象。接下来是证明。
首先获得类对象:
接着用x/4gx
打印类对象的内存地址,以及它的isa地址并打印:发现还是原来那个类???
这打破了前面的认知,是类对象不唯一?其实这就是FFGoods
的元类。捋一下,实例对象的isa指向类对象,类对象的isa指向元类对象。
元类也是一个objc_class
,也有一个isa指针;继续打印看看:
这和我们认识的NSObeject
,是不是一回事呢?我再通过p/x NSObject.class
打印一下地址,发现并不一样:
继续通过 x/4gx
打印NSObject的类对象及其isa
指向的类对象:
最终这个和前面元类的地址一样,这就是对象根类NSObject
的元类,也叫根元类(它还是NSObject
)。
捋一捋:元类对象的isa指向根元类,根类NSObject的指针也指向根元类。
那么根元类有没有isa
,有的话指向哪里?没有什么是LLDB
不能打印的:
原来,根元类的isa是指向自身!isa
不是应该包含对象相关信息吗,这里怎么直接等于自身内存地址了?
isa是一个指针只是用来存储内存地址,根元类或者元类没有引用计数,或者是否被弱引用。所以他们两的isa不是上篇文章说到的
nonPointerIsa
。
这样一来,isa的指向形成了一个闭环。
那么每个对象都能通过4个步骤找到根元类吗?先卖个关子,往下看。
类的继承
如果创建一个类,不继承NSObject
?看个栗子:
发现对于子类FFToys
来说,元类的父类 = 父类的元类?也就是说元类之间也保持继承关系。
接着看一下根类和根元类有没有父类:
根类NSObject
没有父类,NSObject
元类的父类竟然是NSObject
的类对象。
再补充一下类对象之间的关系:子类的类对象的父类 = 父类的类对象
走位图
由此总结一下继承链,以自定义的父子类为例:
NSObject
是objc
的根类,所以它没有父类。根元类的父类都是NSObject
。
再总结一下isa
的走向:
类对象的结构
已知对象的本质是objc_object
结构体,那么看一下类对象:其结构体objc_class
也是继承objc_object
superclass
是指向父类对象的指针,cache
我打算之后的文章里单独讲。接下来打算读bits
这个内存空间,这里涉及到内存平移的概念。
内存平移
开发中用到的指针,在内存中也需要有个地址来存放指针;举个栗子:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 数组指针
int c[4] = {5,6,7,9};
int *d = c;
NSLog(@"数组头指针:%p", &c);
NSLog(@"通过下标获取元素的内存地址:%p - %p - %p - %p", &c[0], &c[1], &c[2], &c[3]);
NSLog(@"通过地址偏移获取元素的内存地址:%p - %p - %p - %p", d, d + 1, d + 2, d + 3);
for (int i = 0; i < 4; i++) {
int value = *(d + i);
NSLog(@"%d", value);
}
}
return 0;
}
一般将c[4]看做数组指针,c指向数组的首地址;
通过*(d+i)
就是一种内存平移。数组里存的是指针,取出第i个指针。
根据声明的int类型,每个指针在内存上相差4字节。
类信息的读写
再看类对象的首地址,通过内存平移32字节(isa + superclass +cache
的大小)就拿到bits
地址(指针类型class_data_bits_t *
)
看到$3的这一串数字,显然我不知道它是啥。回顾它的结构体:
friend
关键字难道是朋友关系?如果要访问一个类的私有成员,正常需要public
关键字。c++提供的friend
修饰符,和objc_class
成为朋友。使其可以访问里面的私有数据;
参考:C++友元函数和友元类(C++ friend关键字)。友元类中的所有成员函数都是另外一个类的友元函数。例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。此时,B是
objc_class
,那么可以访问class_data_bits_t
的所有成员。注意:友元的关系是单向的、不能传递的。
调用一下公开的data()
方法:
要看懂这些,还得翻一下class_rw_t
源码:看到methods()
应该就是方法列表:
一步步拿到方法列表的结构体,但是方法的数量对不上?属性name
的get
、set
,以及实例的init
方法加起来应该是3个。
跳转method_array_t
:
DECLARE_AUTHED_PTR_TEMPLATE(method_list_t)
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }
const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
return beginLists();
}
const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
跳转method_list_t
:
原来是通过entsize_list_tt
这个模板生成的:
在编程的世界里,是容器(container)都有迭代器(iterator),可以用来遍历这个容器。
根据上图的Element& get(uint32_t i)
,试试get方法,但是拿到的对象不显示具体内容:
找到method_t
,对于一个方法,应该有sel
方法名,还有imp
方法实现。
这里面的big
结构体正好就有我们需要的内容,接下来分别获取方法和属性列表。
大小端
这里看到读取的big的公开方法:
有个isSmall()
的判断是什么?iOS系统是分大小端,intel电脑是大端模式,arm架构的是小端模式。
**大端:**数据的高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
**小端:**数据的低位字节存储在内存的低地址端,高位字节存储在内存的高地址端。
举个例子,比如,我们要存储一个16进制数0x12345678
,从内存地址0x1001
开始存放。内存最低操作单元是字节,每个内存地址存放1字节的大小,而16进制数的2位等于1字节,所以总共要4个内存地址存放(地址低的是高位)。
// 1字节 = 8位(二进制),
// 而一位16进制转二进制是4位,例如 15 = 0xf = 1111 = 8 + 4 + 2 + 1
具体内容:
内存地址 | 大端模式存放 | 小端模式存放 |
---|---|---|
0x1001 | 0x12 | 0x78 |
0x1002 | 0x34 | 0x56 |
0x1003 | 0x56 | 0x34 |
0x1004 | 0x78 | 0x12 |
用大端系统下的计算器表示:
换算:
0001 0010 = 0x12;
0011 0100 = 0x34;
0101 0110 = 0x56;
0111 1000 = 0x78;
实例方法
(大端设备)逐个读取方法:
这多出来的.cxx_destruct
是什么?我搜到的是析构方法,在ARC模式下用于释放成员变量的。当类拥有成员变量的时候,就会自带这个方法。包括属性自动生成的成员变量也算。
类方法
类对象里只有实例方法,类方法应该是存放在元类里。
接着就通过获取元类的class_rw_t
结构中的方法列表来验证一下。
可以看到元类的方法列表里只有这个类方法。
图中涉及的指令:
// x/6gx meta
// p/x (class_data_bits_t *)0x100008220
// p *$1
// p $2.data()
// p *$3
// p $4.methods()
// p $5.list
// p $6.ptr
// p *$7
// p $8.get(0).big()
属性
同理可得属性列表。
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
// 跳转 property_list_t
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
调试如下:p/x $5.properties()
超过属性数量的时候就出现Assertion failed。
如果给类添加成员变量,会不会出现在属性列表里呢?修改,重试一遍:
属性列表里数量没变,也就是没有成员变量_privateProperty
。欲知后事如何,且听下回分解。iOS底层专栏
总结
isa指向
实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身。
类的继承链
对于NSObject
、父类、子类,他们的类对象、元类对象都保持继承关系。
根元类的父类就是NSObject
类对象。
NSObject
类对象是万类之祖,没有父类。
类对象
类对象本质为objc_class
结构体,有且只有一个。存储了isa
、superclass
(父类)、bits
(属性、实例方法、协议、成员变量)、cache(方法缓存)。
class_rw_t
初步认为类的信息存储在class_rw_t
结构体中,已经验证包含了属性和方法。
entsize_list_tt
entsize_list_tt
是个模板,可以实例化出method_list_t、ivar_list_t、property_list_t
三种类型。
// Element:表示元素类型 List:表示容器类型 FlagMask:标记位
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
.cxx_destruct
.cxx_destruct
方法是在ARC
模式下用于释放成员变量的。只有当前类拥有实例变量时这个方法才会出现,property
生成的实例变量也算,且父类的实例变量不会导致子类拥有这个方法。
大小端
大端的意思就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。小端就相反。