iOS底层探索之对象的本质和类的关联特性initIsa(下)
在开始阅读本篇博客之前,建议先去看看我的上一篇博客iOS底层探索之对象的本质和类的关联特性initIsa(上)
本篇内容主要讲下
OC
当中类的关联相关的initIsa
1.initIsa结构
从苹果开源的objc
底层源码可以看到OC底层是通过initIsa
和cls
类进行关联的
//关联对象
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
command+鼠标左键
进入obj->initIsa(cls)
inline void
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}
在进入initIsa(cls, false, false)
isa_t
进入isa_t
我们发现
isa_t
是一个联合体
(union),为了能更好的深入的探索下去,我们得先来了解下联合体
相关的知识。
2.联合体
我们在C语言中经常看到
union
,这就是联合体
,也叫共用体
是一种特殊的数据类型,允许你在相同的内存位置存储不同的数据类型。你可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
定义联合体
为了定义共用体,必须使用union
语句,方式与定义结构类似。union
语句定义了一个新的数据类型,带有多个成员。union
语句的格式如下:
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
union tag
是可选的,每个member definition
是标准的变量定义,比如int i
;或者 float f
;或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。
联合体举例
下面定义一个名为Data
的共用体类型,有三个成员i、f 和 str
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
代码运行输出结果
我们可以看到
共用体
的i
和f
成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str
成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
代码运行结果
代码运行结果
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
对比结构体
struct Student{
NSString *name;
int age;
} Student;
int main( )
{
struct Student stu;
stu.name = @"RENO";
stu.age = 18;
return 0;
}
代码运行结果
代码运行结果
从代码运行的结果我们看到,结构体的所有的成员赋值都能正常输出
小结
-
结构体(struct)中所有变量是“共存”的
- 优点:是海纳百川
“有容乃⼤”
- 缺点:是内存空间的分配是
粗放
的,不管你⽤不⽤,我系统都全给你分配。
- 优点:是海纳百川
-
联合体(union)中是各变量是
“互斥”
的,有你没我,有我没你- 缺点:就是不够
“包容”
- 优点:是内存使⽤更为
精细灵活
,也节省了内存空间
- 缺点:就是不够
3.位域
上面👆介绍了联合体
,现在我们再来了解下位域
的知识,嘿嘿😋提前透露下,待会探索底层需要用到位域
。
什么是位域
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为
“位段”
或称“位域
”(bit field
) 。利用位段能够用较少的位数存储数据。
光看这文字概念,也不太能理解啊!能不能给举个栗子🌰啊!好,那么接下来我就举个栗子🌰
举个🌰
位域举例
/* 定义简单的结构体 */
struct status1{
unsigned int widthValidated;
unsigned int heightValidated;
};
/* 定义位域结构 */
struct status2{
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
};
printf( "Memory size occupied by status1 : %lu\n", sizeof(status1));
printf( "Memory size occupied by status2 : %lu\n", sizeof(status2));
代码运行结果当上面的代码被编译和执行时,它会产生下列结果:
从运行结果我们可以得知:
status1
结构体占用了8 字节
的内存空间,而使用位域
的status 2
只占用4 个字节
的内存空间(冒号:后面加数字表示使用多少位
),但是只有2 位
被用来存储值。如果用了32
个变量,每一个变量宽度为1
位,那么status 2
结构将使用4 个字节
,但只要再多用一个变量,使用了33
个变量的话,那么它将分配内存的下一段来存储第33 个变量
,这个时候就开始使用8 个字节
了。
上面的举例我们只是为了记录width
和height
是否生效,其实只有TRUE
/FALSE
两种情况,使用结构体
的话就需要8字节
,64位
来存储,这就大大浪费了,有点大材小用了。而使用位域
,只需要2位
,4字节
存储。
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有
0
和1
两种状态,用1
位二进位
即可。为了节省存储空间,并使处理简便,所以C 语言
提供了"位域"
或"位段"
这种数据结构。
"位域"
是把一个字节
中的二进位划分为几个不同的区域
,并说明每个区域的位数。每个域有一个域名
,允许在程序中按域名
进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
对于位域的定义尚有以下几点说明:
- 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs{
unsigned a:4;
unsigned :4; /* 空域 */
unsigned b:4; /* 从下一单元开始存放 */
unsigned c:4
}
在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
-
位域
的宽度不能超过它所依附的数据类型
的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度
,:
后面的数字不能超过
这个长度。 - 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k{
int a:1;
int :2; /* 该 2 位不能使用 */
int b:3;
int c:2;
};
从以上分析可以看出,
位域
在本质
上就是一种结构体类型
,不过其成员是按二进位分配的。
位域允许用各种格式输出。
int main(){
struct bs{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit,*pbit;
bit.a=1; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.b=7; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
bit.c=15; /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
printf("%d,%d,%d\n",bit.a,bit.b,bit.c); /* 以整型量格式输出三个域的内容 */
pbit=&bit; /* 把位域变量 bit 的地址送给指针变量 pbit */
pbit->a=0; /* 用指针方式给位域 a 重新赋值,赋为 0 */
pbit->b&=3; /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
pbit->c|=1; /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c); /* 用指针方式输出了这三个域的值 */
}
上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示
位域
也是可以使用指针
的。
4.initIsa分析
在上面我们已经知道了,isa_t
是一个联合体
,里面除了有isa_t()
构造方法,还有uintptr_t
类型的bits
,还有我们的对象cls
,最最重要的是还有一个结构体成员变量ISA_BITFIELD
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
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
这个ISA_BITFIELD
不就是isa位域
吗?我们在深入进去看看,到底是个什么东东???
ISA_BITFIELD
command+鼠标左键
点击,进去找到了ISA_BITFIELD
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
好家伙?直呼“好家伙”啊!
我的天呢?这内有乾坤啊!这是一个
宏定义
,分为__arm64__
和__x86_64__
两种,其中__arm64__
的情况包括模拟器。完整定义如下
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
进都进来了,你就给我看这个啊?这个uintptr_t nonpointer
,uintptr_t has_assoc
奇奇怪怪的是个什么玩意啊???
那就耐着性子,听我一一道来!
NONPOINTER_ISA
-
nonpointer
在0
位,表示是否对isa
指针开启指针优化
。0:
纯isa
指针,1:
不止是类对象地址,isa
包含了类信息
、对象的引用计数
等。 -
has_assoc
在1
位,表示关联对象标志位,0:
没有,1:
有。 -
has_cxx_dtor
在2
位,表示该对象是否有C++
或者Objc
的析构器,如果有析构函数,则需要做析构
逻辑,如果没有,则可以更快的释放
对象。 -
shiftcls
在x86
架构中占用3~46
位,表示存储类指针的值。开启指针优化的情况下,在arm64
架构中占用3~35
位。 -
magic
在x86
架构中占用47~52
位,在arm64
架构中占用36~41
位,用于调式器
判断当前对象是真的对象还是没有初始化的空间。 -
weakly_referenced
在x86
架构中占用53
位,在arm64
架构中占用42
位,标志对象是否被指向或者曾经指向一个ARC
的弱变量,没有弱引用
的对象可以更快释放。 -
unused
在x86
架构中占用54
位,在arm64
架构中占用43
位,标志对象是否正在释放内存。 -
has_sidetable_rc
在x86
架构中占用55
位,在arm64
架构中占用44
位,表示当对象引用计数
大于10
时,则需要借用该变量存储进位
。 -
extra_rc
在x86
架构中占用56~63
位,在arm64
架构中占用45~63
位,当表示该对象的引用计数
值时,实际上是引用计数值减1
,例如:如果对象的引用计数为10
,那么extra_rc
为9
,如果引用计数大于10
,则需要使用到has_sidetable_rc
。
ISA_BITFIELD内存分布为了更直观的理解
ISA_BITFIELD
请看下图:
由上面得知OC底层通过运用
联合体
和位域
,大大优化了ISA
,这就是著名的NONPOINTER_ISA
,这样就充分利用了内存的使用。
isa关联对象过程还原
先看下面这个代码,在NSLog
处打上断点
JPStudent *stu = [JPStudent alloc];
NSLog(@"%@",stu);
当代码执行到NSLog
,控制台lldb
调试
(lldb) x/4gx stu
0x10070d140: 0x011d8001000080e9 0x0000000000000000
0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1
我们看到stu
对象的指针地址是0x011d8001000080e9
(isa),最高位是0
,说明64
位并没有使用满。
我们都知道对象是通过指针地址关联到类的,那么我们看看类的地址
(lldb) p/x JPStudent.class
(Class) $1 = 0x00000001000080e8 JPStudent
从控制台看到,类的地址占用位数更少了,那到底对象和类是怎么关联的呢???
ISA_MASK
在上面的源码中我们看到有一个ISA_MASK
的宏定义,ISA_MASK = 0x007ffffffffffff8ULL
# define ISA_MASK 0x007ffffffffffff8ULL
这个
ISA_MASK
,就是ISA
的面具
,和网络中的子网掩码
差不多,单独存在没有什么实际的意义。
比如我想露出眼睛👀或者鼻子👃,那么我就需要遮住脸部的其他地方,把需要露出来的地方露出来,那我戴上一个面具🎭就可以达到这种目的。
那么我们现在知道了ISA
是0x011d8001000080e9
,ISA_MASK
为0x007ffffffffffff8ULL
让它两个作与(&)操作,看看结果
还有谁???看到没有?得到的结果是一模模一样样!
对象stu
通过ISA
得到类
(cls),因为ISA
是不纯的,里面还包含了其他信息,所以必须通过掩码(ISA_MASK),得到类的信息。
ISA位运算
除了上面的通过
掩码
得到类
,也可以通过位
运算得到,因为我们只要找到shiftcls
就可以,在前面的介绍中我们已经知道shiftcls
是占中间的44
位。
具体操作如下
(lldb) x/4gx stu
0x10070d140: 0x011d8001000080e9 0x0000000000000000
0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1
(lldb) p/x 0x011d8001000080e9 >> 3
(long) $6 = 0x0023b0002000101d
(lldb) p/x 0x0023b0002000101d << 20
(long) $7 = 0x0002000101d00000
(lldb) p/x 0x0002000101d00000 >> 17
(long) $8 = 0x00000001000080e8
(lldb) p/x JPStudent.class
(Class) $9 = 0x00000001000080e8 JPStudent
(lldb)
具体操作请看下面的图
isa位运算过程
🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹