在开始阅读本篇博客之前,建议先去看看我的上一篇博客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请看下图:
由上面得知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)
具体操作请看下面的图
🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹