一,isa 的作用
在iOS开发过程中,我们知道,任何一个对象都有一个isa,通过isa 指向,可以找到父类,以及根元类的相关的实例方法,在iOS开发过程中isa的作用可以说非常重要,不可替代,因为有了iOS,系统才能才是实现相关的runtime、objc_msgSend,以及IMP 的映射。
例如,我们在main.m 文件中简单声明一个类 YOPerson,还是通过我们最常用的查看底层代码的命令语句clang -rewrite-objc main.m -o main.cpp
,这样会生成一个相应的 .cpp
文件,我们进入到里边查看我们声明的类的整体居然是一个这样的构造。
#ifndef _REWRITER_typedef_YOPerson
#define _REWRITER_typedef_YOPerson
typedef struct objc_object YOPerson;
typedef struct {} _objc_exc_YOPerson;
#endif
struct YOPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
/* @end */
// @implementation YOPerson
// @end
我们在开发过程中知道,不管是C语言
、C++
,还是C#
,以及我们的iOS开发
结构体是可以进行继承的,只要把结构体的声明放为第一个成员变量就可以。所以我们声明的类在底层编译的时候,就是转换为相应的结构体类型。因为这样能使用 内存对齐 原则,当然在对象的存储中还使用联合体位域 的机制来优化内存。从而达到节省内存、快速定位的作用。
在我们声明的类实现的后边我们能看到有一个NSObject_IVARS;
的标识,NSObject_IVARS;
是什么呢,不难发现这就是我们声明的类的isa,我们全局的搜索这个关键字能看到不管是系统的类还是我们自己的声明的类都存在这样的一个标识。
- isa究竟是一个什么样的构造?
- isa 是如何关联一个类的?
带着这样的问题我们进入详细的探索~~~~~~~
二,isa的结构
part1:isa中的概念
在iOS开发中的 alloc 的执行流程学习总结 这篇文章中我们已经介绍了再alloc 的过程是通过isa来具体的关联一个累的。代码如下:objc-781
中的开源代码
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);
}
再次进入
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
最后到
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
上边是手机端的相关的结构,下边的这部分是mac。
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
这就是isa的具体的结构,接下来详细分析一下相关的每一个字段的具体作用
exp1:
ISA_MASK
---ISA_MASK
是一个固定值,在通过类对象的isa地址 和ISA_MASK
进行按位与
或者&
就能准确的定位到相应的类对象。或者通过另一种左移
和右移
也能得到相同的结果exp2:
ISA_MAGIC_MASK
--- 是一个isa 镜像标识,获取镜像文件使用的exp3:
ISA_MAGIC_VALUE
--- 是isa 的镜像值,此处不做过多的解释exp4:
has_assoc
---- 表示对象是否含有关联引用(associatedObject)exp5:
has_cxx_dtor
----has_cxx_dtor表示当前对象是否有C++的析构函数(destructor),如果没有,释放时会快速的释放内存。exp6:
magic
--- magic的值调试器会用到,调试器根据magci的值判断当前对象已经初始过了,还是尚未初始化的空间。exp7:
weakly_referenced
--- 表示对象是否含有弱引用对象exp8:
deallocating
--- 表示对象是否正在释放exp9:
has_sidetable_rc
--- 表示对象的引用计数是否太大,如果太大,则需要用其他的数据结构来存,一般是散列表。exp10:
extra_rc
--- 对象的引用计数大于1,则会将引用计数的个数存到extra_rc里面。比如对象的引用计数为5,则extra_rc的值为4。-
exp11:
nonpointer
--- 在文章开头也提到了,在Objective-C语言中,类也是对象,且每个对象都包含一个isa指针,现在改为了isa结构体。nonpointer作用就是区分这两者。- 1 如果nonpointer为1,代表不是isa指针,而是isa结构体。虽然不是isa指针,但是通过isa结 构体仍然能获得类指针(下面会分析)
- 2如果nonpointer为0,代表当前是isa指针,访问对象的isa会直接返回类指针。
-
exp12:
shiftcls
---shiftcls存储的就是当前对象类的指针。之所以右移三位是出于节省空间上的考虑- 在Objective-C中,类的指针是按照字节(8 bits)对齐的,也就是说类指针地址转化成十进制后,都是8的倍数,也就是说,类指针地址转化成二进制后,后三位都是0。既然是没有意义的0,那么在存储时就可以省略,用节省下来的空间存储一些其他信息。
part2:isa中的数据的变化;
前一部分我们已经介绍了 isa
中所有的字段的概念和相关的作用,接下来我来分析一下isa
它是如何工作的,内存数据变化是怎么样的。
-
1、首先我们过滤掉系统的方法,找到我们自己创建的类对象。
此时我们在控制台打印 po newisa
,打印出的结果如下
此时我们能看到,nonpointer = 0
, magic = 0'
,cls = nil
,bits = 0
等一切都是空的信息
-
2、给newisa.bit赋值 过后再次打印的结果是如下截图,
赋值过后我们能看到相应的地方都有值了,nonpointer = 1
, magic = 59
,cls = 0x001d800000000001
,bits = 8303511812964353
nonpointer = 1 这个是我们自己定义的类,值为1都能理解,那为什么magic 赋值后是59?
然后我们打开计算器,把相应的cls 地址输入计算器,得出的结果是
从图中我们看到的47位正好就是magic的地址,0011 1011 二进制正好就是59;也就是magic的值,但是为什么是47位呢?接下来我们来分析一下 isa位
的情况。
part3:isa 的 位
的分配
我们在开发过程中知道isa
指针占用8个字节。也就是64位
,在根据isa的分配
我们可以清楚的知道isa 中各个字段对应的起始位置和占用多少位的情况。
这就是isa中的位占据大致图。没有标准的画图工具,多多包涵。
part4:isa的shiftcls如果包涵一个类的信息
通过文章和代码示例,我们知道shiftcls
是isa
中最重要的一部分,占据内存最大,几乎包涵了整个类的所有信息,我们怎么去验证在 shiftcls
中能找到整个类的信息呢?
我们知道,shiftcls
是从#3到#46的位置,所以为了方便我们右移3位,然后在左移17位,就能完整的得到我们想要的结果。
- 给
shiftcls
赋值
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
打印结果是
- 执行
p (uintptr_t)cls >> 3
右移操作,将末尾的3位抹掉;结果是
(lldb) p (uintptr_t)cls >> 3;
(uintptr_t) $6 = 536872044
此处我们用。po
(lldb) po 0x001d800100002365 & 0x0000000ffffffff8ULL
结果是 YOPerson
然后我们用isa 指针进行左移右移,操作
(lldb) p/x 0x001d800100002365 >> 3
结果是 $3 = 0x0003b0002000046c
再将$3 右移 20位
(lldb) p/x 0x001d800100002365 << 20
结果是 $4 = 0x0002000046c00000
再将$4左移17位
(lldb) p/x 0x0002000046c00000 >> 17
结果是 $5 = 0x001d800100002365
就可以完整的得到ISA的指针地址,
这就是为什么shiftcls 是整个ISA的最核心的存在的原因,
总结
这就是整个ISA的内部结构过程,当然只是个人的理解,有什么问题请多多指教,