都知道isa指针是指向类对象,除了类对象,还存储了哪些信息呢?
联合体&位域
在了解联合体之前,首先我们要了解一下联合体与位域。因为isa的数据结构就是由这俩组成的。runtime的有很多数据结构都是基于联合体。
联合体
联合体(共用体)的所有成员占用同一段内存,修改一个成员会影响其余所有成员。 说到联合体(union) 的意义就不得不提结构体(struct), 因为这两者定义形式完全相同。
两者差别:结构体的各个成员会占用不同的内存块,互相之间没有影响;而联合体的所有成员占用同一段内存块,修改其中一个成员会对整个内存块保存的值产生影响。
union test_a {
long a;
int b;
char c[9];
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union test_a test = {0};
NSLog(@"\ntest_a size is %ld", sizeof(test));
test.a = 97;
NSLog(@"\na is : %ld\nb is %d \nc is %s", test.a, test.b, test.c);
test.b = 122;
NSLog(@"\na is : %ld\nb is %d \nc is %s", test.a, test.b, test.c);
}
return 0;
}
//输出:
2021-09-02 22:30:33.201485+0800 objc[73997:948863]
test_a size is 16
2021-09-02 22:30:34.658421+0800 objc[73997:948863]
a is : 97
b is 97
c is a
2021-09-02 22:30:34.658615+0800 objc[73997:948863]
a is : 122
b is 122
c is z
Program ended with exit code: 0
上面定义了一个test_a
的联合体,有三个成员变量。先看一下联合体的大小,共用一块内存,最大的就是成员变量c
,占用9个字节,又遵循对齐原则,大小需要能被其他成员变量整除,所以当前test_a
需要16字节。当赋值a之后,可以看到a、b、c的值都发生了变化,都变成了97(c的值变成了a,可查看ASCII)。
大小端序
使用联合体可查看当前系统的大小端序,代码如下:
union data{
int a; //4 bytes
char b; //1 byte
} ;
data.a = 1; //占4 bytes,十六进制可表示为 0x 00 00 00 01
//b因为是char型只占1Byte,a因为是int型占4Byte
//所以,在联合体data所占内存中,b所占内存等于a所占内存的低地址部分
if(1 == data.b)
{
//走该case说明a的低字节,被取给到了b,即a的低字节存在了联合体所占内存的(起始)低地址,符合小端模式特征
printf("Little_Endian\n");
}
else
{
printf("Big_Endian\n");
}
iOS就是小端模式。
位域
有些信息在存储时,并不需要占用一个完整的字节,而只需要一个或几个二进制位即可;比如:在存放一个开关变量时,只有0和1两种状态,只需要使用一个二进制位即可存储;为了节省存储空间,C语言提供了一种数据结构,称为"位域"或"位段";所谓"位域"就是把一个字节中的8个二进制位划分为几个不同的区域,并说明每个区域的二进制位数;每一个位域都有一个位域名,允许程序员在程序中按照位域名进行访问;这样就可以把几个不同的对象用一个字节的二进制位域来表示;
定义如下:
struct 位域结构名
{
类型说明符1 位域名1:位域长度1; //最低位;
类型说明符2 位域名2:位域长度2; //次低位;
类型说明符3 位域名3:位域长度3;
......
类型说明符N 位域名N:位域长度M; //最高位;
};
可以看到位域的第一个成员变量是处于内存中低位的, 最后一个是处于高位的。
ISA
在objc4源码中可以找到以下代码
struct objc_object {
private:
isa_t isa;
...... //剩下的就是函数了
};
结构体objc_object
就是用来表示OC对象的,isa_t
就是用来存储isa
信息的。
isa_t
isa_t是一个联合体(共用体),
isa_t
数据结构如下:
union isa_t {
// 构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
...... //剩下的就是函数了
};
既然isa_t
是一个联合体,数据存在bits
、cls
、struct
里都是一样的。在非nonpointer
情况下,isa的数据是存在cls
中的,反之,isa的数据会存储在bits
中。
struct
展开信息如下(这里只拿来了arm64真机下的):
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
};
这个结构体就是位域。
- nonpointer:是否是优化过的isa指针,0:未优化,1:优化过,可以存储更多信息;
- has_assoc:是否有关联对象,如果有,会影响释放速度变慢;
- has_cxx_dtor:是否有C++的析构函数,如果有,会影响释放速度变慢;
- shiftcls:类、元类的内存地址信息;
- magic:用于在调试时分辨对象是否未完成初始化;
- weakly_referenced:是否被弱引用(
weak
)指向过,如果有,会影响释放速度变慢; - unused:
- has_sidetable_rc:引用计数是否过大而存储到了sidetable中;
- extra_rc:引用计数;
shiftcls
上面的配图就是isa_t
在内存中的布局,尝试一下取出其中的shiftcls
,shiftcls
前28位存储了一些信息,后3位存储了一些信息,中间的33位数据即是shiftcls
,可以通过位移的方式把它取出来。
- 先右移3位:将后3位移除,前3位用0补齐;
- 在左移31位:将前31位移除,后31位用0补齐;
- 最后在右移28位:将后28位移除,前28位用0补齐,这样
shiftcls
就回到了最初的位置。 注:配置中以灰色为底的块中都是位移操作之后移除的数据,以白色为底的块中都是用0补齐的数据。
下面是LLDB调试过程:
最后$3
和$4
的值是一摸一样的。
ISA_MASK
ISA_MASK的定义
在objc4源码的isa.h
中可以找到ISA_MASK
的定义
# if __arm64__
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR /* 模拟器 */
# define ISA_MASK 0x007ffffffffffff8ULL
# ......
# else /* 真机 与 M1 芯片机器 */
# define ISA_MASK 0x0000000ffffffff8ULL
# ......
# endif
# elif __x86_64__ /* intel芯片机器 */
# define ISA_MASK 0x00007ffffffffff8ULL
# ......
# endif
ISA_MASK是如何算出来的?
从上面isa_t
在内存中的布局图可以看出,如果想要从中取出shiftcls
,也可以通过进行按位与运算,需要与上:前面为28个0,中间为33个1,后面为3个0,具体值:0000000000000000000000000000111111111111111111111111111111111000
,换算成16进制就是ffffffff8
,在按照8字节补齐就是0x0000000ffffffff8
,正好与0x0000000ffffffff8ULL
是一样的,ULL
是表示当前数据的类型unsigned long long
.这里进行了arm64下真机的运算,其他平台可自行尝试。
ISA_MASK的使用
如何使用,isa是存在当前对象第一个8字节内,只要取出对象的第一个8字节,在进行&ISA_MASK
与运算即可得到类对象的内存地址。
x/4gx p
查看当前指针p在内存中所存储的数据;po 0x01000001000081b9 & 0x0000000ffffffff8ULL
,取出对象在内存中存储的第一个8字节,进行&ISA_MASK
与运算得到Person
类;p/x 0x01000001000081b9 & 0x0000000ffffffff8ULL
以16进制的方式查看与运算的值,也就是当前Person
类的内存地址;- 查看真实的
Person
类的内存地址;
可以看到,在isa
为nonpointer
的情况下,可以存储更多关于对象的数据信息,但是isa需要进行运算才能获取到类对象的内存地址。而在非nonpointer
是直接指向类对象的内存地址的。
在Environment Variables
中添加OBJC_DISABLE_NONPOINTER_ISA
为YES即可禁用nonpointer
。
ISA 绑定流程
ISA的绑定是在_class_createInstanceFromZone
中开辟内存空间之后进行的处理
obj->initInstanceIsa(cls, hasCxxDtor);
initIsa(cls, true, hasCxxDtor);
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
前两部就是正常调用加一些特处理,主要看第三部:(中间省略了一些代码)
inline void
objc_object::initIsa(Class cls, bool nonpointer,
UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0); // 创建isa结构体
if (!nonpointer) { // 如果不是nonpointer,直接赋值cls
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
//
newisa.bits = ISA\_MAGIC\_VALUE;
#if ISA\_HAS\_CXX\_DTOR\_BIT
newisa.has_cxx_dtor = hasCxxDtor;
#endif
// 赋值cls
newisa.setClass(cls, this);
#endif
// retain count
newisa.extra_rc = 1;
}
isa = newisa;
}
首先创建创建isa结构体,如果是非nonpointer,直接复制cls,否则先赋值bits,此时会将isa的nonpointer赋值为1,在赋值magic。在赋值isa的has_cxx_dtor,接下来会赋值cls。
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
......
shiftcls = (uintptr_t)newCls >> 3;
......
}
将类的指针地址右移3位存入到shiftcls
,在此就完成了isa
的绑定。
类
类的介绍
类其实也可以认为是一个对象,本质是一个继承制自objc_object
的结构体,类对象在内存中只存储一份,里面存储着类实例对象的属性、协议、方法等数据,对象的isa指向的是类,可以直接调用类对象里存储的实例方法,每个对象不用分别存储其方法,可以减少内存开销。下面是类对象存储的数据:
struct objc_class : objc_object {
......
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
- 和对象一样,类对象也有一个
isa
指针,指向的是元类
; super_class
,指向的是父类
;cache
,方法缓存,是一个哈希表;bits
,方法数据,存储着class_rw_t
,rw
代表着readwrite,可进行读写操作,该结构体内存储着属性列表property_array_t
,方法列表method_array_t
,协议列表protocol_array_t
。里面还有class_ro_t,存储着类原始数据,ro
代表着readonly
。
LLDB调试探索类存储信息
class_rw_t
上图是苹果介绍存储类的数据结构,class_ro_t
是只读的,里面存储着类原始的属性、方法、协议,class_rw_t
是可读写的,可以对当前类动态添加属性、方法、协议,但是大部分类用不到动态添加,并且class_ro_t
中有这些数据,所以将class_rw_t
中的这些数据放在class_rw_ext_t
中,如果当前类没有动态添加属性、方法、协议,是可以减少内存开销的。 Advancements in the Objective-C runtime
获取class_rw_t
下面是objc_class
结构体获取class_rw_t
的方法
class_rw_t *data() const {
return bits.data();
}
可通过data()
方法获取到class_rw_t
,通过LLBD调试的方式是无法直接将class
转成(objc_class *)
,也就无法直接使用objc_class
的data()
方法(如果知道为什么不能这样操作的,请在评论留言,共同学习),只能通过bits
取出class_rw_t
,通过上面的图可以看到,bits
与isa
内存相差32位,只要在isa
的内存地址加上32,即可获得bits
,在通过data()
方法,即可拿到class_rw_t
,
entsize_list_tt&list_array_tt
在了解property_array_t
、method_array_t
、protocol_array_t
之前,先要了解entsize_list_tt
与list_array_tt
list_array_tt
template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
......
union {
Ptr<List> list;
uintptr_t arrayAndFlag;
};
......
}
list_array_tt
封装了一个数组的类,即可以是普通数组,也可以是一个二维数组,可指定数组里的元素类型,property_array_t
、method_array_t
、protocol_array_t
都是继承自list_array_tt,
一个 list_array_tt 的值可能有三种情况:
- 空的
- 一个指针指向一个单独的列表
- 一个数组,数组中都是指针,每个指针分别指向一个列表
尤其要关注里面的
void attachLists(List* const * addedLists, uint32_t addedCount)
方法,对理解类、分类加载有一定帮助。下面是详细代码:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
代码执行步骤:
- 首次添加列表:会将
list
指向该列表的首地址,对应着代码中// 0 lists -> 1 list
下面的操作 - 第二次添加列表:创建一个
array_t
赋值给arrayAndFlag
(list
与arrayAndFlag
是在一个匿名联合提,两个数据共用一块内存。也可以通过arrayAndFlag
取出list
指针),并将老的list
存储的数据放在最后一位,新增的数据放在数组前方;对应着代码中// 1 list -> many lists
下面的操作。 - 在两次之后的继续添加:继续创建一个
array_t
赋值给arrayAndFlag
,此时也是将新来的列表放在前面,老的列表放在最后面。 通过上面的执行步骤可得知,在添加类别的时候,类别方法肯定会放在原始方法前面,方法查找时,也是使用此方法列表,这就说明了类别方法为什么会覆盖类原来的方法。还有一个就是类别越多,启动速度相对较慢。(这里需要了解一点类加载的原理)
entsize_list_tt
entsize_list_tt其实就是一个列表,用来存储编译完成后类的属性。property_array_t
->property_list_t
、method_array_t
->method_list_t
的一维数组都是继承自entsize_list_tt
,详细内容可阅读链接的资料。里面有一个Element& get(uint32_t i)
方法,下面调试时会用到,就是根据索引获取元素。(protocol_list_t
为什么没有继承entsize_list_tt
?)
通过class_rw_t探索属性在类中的存储:property_array_t
property_array_t的数据结构
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_array_t
继承自list_array_tt
,其中存储的元素是property_t
,一维数组数据类形是property_list_t
。
property_t
数据结构如下:
struct property_t {
const char *name;
const char *attributes;
};
调试方式
- 通过
class_rw_t
获取property_array_t
,class_rw_t
中有properties()
方法,可以直接获取到property_array_t
; - 通过
property_array_t
获取property_list_t
,property_array_t
里面有一个Ptr
指针存储着property_list_t
,只要通过地址取值就可以取到一维数组property_list_t
; - 通过
property_list_t
的get()
方法即可获取property_t
;
Person
类里面有一个name
属性。
通过class_rw_t探索协议列表protocol_array_t
protocol_array_t的数据结构
class protocol_array_t :
public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;
public:
protocol_array_t() : Super() { }
protocol_array_t(protocol_list_t *l) : Super(l) { }
};
继承自list_array_tt
,其中存储的元素是protocol_ref_t
,一维数组数据类形是protocol_list_t
。
调试方式
- 通过
class_rw_t
获取protocol_array_t
,class_rw_t
中有protocols()
方法,可以直接获取到protocol_array_t
; - 通过
protocol_array_t
获取protocol_list_t
,protocol_array_t
里面有一个Ptr
指针存储着protocol_list_t
,只要通过地址取值就可以取到一维数组protocol_list_t
; - 通过
protocol_list_t
获取protocol_ref_t
,protocol_list_t
与属性、方法列表不同,不能直接通过get()
方法获取,需要使用protocol_list_t
结构体里的begin()
方法拿到存储protocol_ref_t
的list
,是一个数组。 - 通过
protocol_ref_t
获取protocol_t
,protocol_ref_t
其实是一个指针,指向的是protocol_t
,只需要做一个强转即可以,在通过指针取值,就可以看到协议相关的数据了;
KKPerson
类定义如下:
获取第一个协议NSCopying
,调试方法如下:
获取第二个协议KKPersonInterface
,调试方法如下:
protocol_t数据结构
struct protocol_t : objc_object {
const char *mangledName;
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;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
里面存储着协议名称、协议遵循的协议(继承?)、实例方法、类方法、可选实例方法、可选类方法、属性。其中的方法探索方式可参考下面method_array_t
的方法,属性可参考上面的探索属性方法。
通过class_rw_t探索协议在类中的存储:method_array_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;
};
继承自list_array_tt
,其中存储的元素是method_t
,一维数组数据类形是method_list_t
。
调试方式
- 通过
class_rw_t
获取method_array_t
,class_rw_t
中有methods()
方法,可以直接获取到method_array_t
; - 通过
method_array_t
获取method_list_t
,method_array_t
里面有一个Ptr
指针存储着method_list_t
,只要通过地址取值就可以取到一维数组method_list_t
; - 通过
method_list_t
的get()
方法即可获取method_t
; - 通过
method_t
获取SEL
,method_t
与property_t
数据结构还不太一样,无法直接打印出内部数据,method_t
中有一个name()
方法可直接获取SEL
。
通过调试可以看到,类里面存储的都是实例方法,没有类方法。类的方法都存储在了元类中。
元类
类的isa指向元类,可以推倒,其实类的方法是存储在元类中的。元类在我们自己写的代码里是找不到的,可以通过构建产物查看到。
也可以通过object_getClass
获取到元类信息,可以看到类名都是KKPerson
,但他俩的内存地址确不同;
类的继承关系
类的继承关系很好理解,子类继承父类,父类继承父父类,一直到NSObject
类,而NSObject
的父类是nil。
元类的继承关系
子类的元类继承自父类,父类元类继承父父类元类,一直到NSObject
元类,而NSObject
的父类元类是nil。
isa指向与superclass继承关系
下面是苹果官方isa指向与superclass继承关系,其中实线是类的继承关系,包括元类,虚线是isa指向关系,包括元类;
isa指向
- 实例(Instance)的isa指向类(Class);
- 类(Class)的isa指向元类(Meta Class);
- 元类(Meta Class)的isa指向根元类(Meta Class),即NSobject的元类;
- 跟元类(Meta Class)的isa指向它自己;
类的继承
- 子类继承父类,一直继承到根类,跟类继承为nil;
- 子类元类继承父类元类,一直继承到根类元类,跟类元类继承自跟类(NSObject);