iOS 探索系列相关文章 :
前言
在之前的研究中我们大概知道了对象创建和分配内存的过程, 但是在这中间有一个东西还没有去注意。那就是对象的 isa
属性, 只是知道在过程中有伴随着它的一系列初始化操作, 但是这个过程中为什么要创建 isa
呢 ?
isa 的初始化 和 结构分析
1. isa 初始化
在之前的 alloc流程分析 中我们从 objc
源码中看到过对 isa
的初始化过程, 下面我们回过去重新看一下当时的代码:
// callAlloc 方法
obj->initInstanceIsa(cls, dtor);
//
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
// 初始化具体实现
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
//
if (!nonpointer) {
isa.cls = 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;
// 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;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
从对象创建过程中方法的调用我们不难发现, nonpointer
被默认传的 true
。然后下面的一些判断操作可以理解为对 isa
的一系列的赋值操作。还有重要的一点, 可以看到 isa
的类型为 isa_t
类型, 下面我们来看一看这个类型到底是什么。
补充: 关于Tagged Pointer
可以在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
Tagged Pointer 专门用来存储小的对象,例如 NSNumber, NSDate, NSString。
Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free 。
在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
有兴趣的可以去看看这两篇文章 iOS Tagged Pointer , 深入理解 Tagged Pointer 。
2. isa 的结构分析
在分析结构之前, 我们首先了解两个概念, 联合体
和 位域
。
- 联合体 (
union
)
在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。(出自百度百科联合体)
1. 联合体是一个结构
2. 它的所有成员相对于首地址的偏移量都为 0
3. 长度取决于各成员中最长的长度
4. 各变量是“互斥”的 —— 共用一个内存首地址,联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值
- 位域
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。 (选自:百度百科)
// 联合体
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
// ISA_BITFIELD 的定义
// 位域
# 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)
# 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)
可以发现, isa_t
就是一个 联合体
类型, 至于 ISA_BITFIELD
从后面的宏定义可以看出是一个 位域
, 所以 isa
的实际类型是一个 联合体
, 并且在联合体内部包含一个 位域
, 用来存储不同的信息。
下面来展开分析一下:
-
首先是两个构造函数
isa_t(), isa_t(unitptr_value)
。 -
然后是三个数据成员
Class cls
,unitptr_t bits
,struct
。其中unitptr_t
被定义为typedef unsigned long uintptr_t
, 占64位。关于
uintptr_t bits
和struct
, 这两个其实可以看成一个成员, 首先他们都占据 64 位内存空间, 并且联合里面的空间是重叠的, 所以他们两个的内存空间是完全重叠的。即uintptr_t bits
和struct
是一个东西的两种表现形式。实际上在
runtime
中,任何对struct
的操作和获取某些值,如extra_rc
,实际上都是通过对uintptr_t bits
做位操作实现的。uintptr_t bits
和struct
的关系可以看做,uintptr_t bits
向外提供了操作struct
的接口,而struct
本身则说明了uintptr_t bits
中各个二进制位的定义。所以
isa_t
其实可以看做有两个可能的值,Class cls
或者struct
。 -
当作为
Class cls
使用时, 就符合了我们一惯的认知,isa
是一个指向对象所属类的指针。但是一个 64位 的指针很显然不划算。所以大多数情况下苹果采用的是优化的isa
策略, 即struct
。 -
下面来看一下
struct
里面各个成员所起到的作用
成员 | 位 | 作用 |
---|---|---|
nonpointer | 1bit | 表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中还包含了类信息、对象的引用计数等 |
has_assoc | 1bit | 关联对象标志位,0没有,1存在 |
has_cxx_dtor | 1bit | 该对象是否有 C++ 或者 Objc 的析构器, 如果有析构函数, 则需要做析构逻辑, 如果没有, 则可以更快的释放对象 |
shiftcls | 33bit | 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。 |
magic | 6bit | 固定为 0x1a, 用于调试器判断当前对象是真的对象还是没有初始化的空间 |
weakly_referenced | 1bit | 指对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放。 |
deallocating | 1bit | 标志对象是否正在释放内存 |
has_sidetable_rc | 1bit | 当对象引用计数大于 19 位时,则需要借用该变量存储进位 |
extra_rc | 19bit | 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 19 位, 则需要使用到上面的 has_sidetable_rc。 |
以上就是对 isa
结构的分析, 具体的 struct
内部成员分析可以去看一下上面分享的链接。
isa 的指向分析
1. class , objc_getClass () 与 object_getclass () 的区别 ?
开始分析 isa
指向之前, 我们首先了解一下上面这三个方法的区别是什么, 下面我们从 objc
源码里直接找到了上面方法的实现 :
// class ()
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
// objc_getClass ()
/***********************************************************************
* objc_getClass. Return the id of the named class. If the class does
* not exist, call _objc_classLoader and then objc_classHandler, either of
* which may create a new class.
* Warning: doesn't work if aClassName is the name of a posed-for class's isa!
**********************************************************************/
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}
// object_getclass ()
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
// ISA_MASK 掩码, 通过 & 运算来获取到 isa 的指向信息
// # define ISA_MASK 0x00007ffffffffff8ULL
return (Class)(isa.bits & ISA_MASK);
#endif
}
从以上的源码实现我们可以总结如下:
- class 方法
// 1. 当为实例方法时, 其返回与 object_getClass () 一致, 即当前对象的 isa 指向的类对象
// 2. 当为类方法时, 返回该类对象本身
- objc_getClass ( ) 方法
// 该方法的参数是一个字符串, 当我们传进去一个类名的字符串, 就会返回这个类的类对象
- object_getclass ( ) 方法
// 根据传入的参数获取 isa 指向, 需要说明的一点在于
// 当传入的是一个实例对象时, 获取到的是 类对象; 当传入的是一个类对象时, 获取到的是 元类
2. isa 指向分析
首先放上来自官方的指向图:
从标注可以看出, 实线箭头表示继承关系, 虚线箭头表示 isa 指向关系
, 然后从上图我们可以得出 isa
的指向顺序依次为: 对象 --> 类 --> 元类 --> 根元类 --> 根元类 。下面我们来一起验证一下这个过程 (注: 此过程需要用到 lldb 相关打印命令, 有兴趣的可以去自己查询一下。) :
// 定义两个类
// 父类 : YJPerson
// 子类 : YJTeacher
// YJPerson类
Class class = object_getClass(person);
// YJPerson元类
Class metaClass = object_getClass(class);
// YJPerson根元类
Class rootMetaClass = object_getClass(metaClass);
// YJPerson根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象 %@\n%p 类 %@\n%p 元类 %@\n%p 根元类 %@\n%p 根根元类 %@",person,person,class,class,metaClass,metaClass,rootMetaClass,rootMetaClass,rootRootMetaClass,rootRootMetaClass);
// 注意: 模拟器情况下是不会对 isa 进行优化的, 所以我们这里选择真机调试
说明: 通过上面的 object_getclass ( ) 方法的内部实现, 我们可以了解到 可以直接通过 isa & ISA_MASK(掩码)
来获取所指向的类, 所以我们下面的验证是通过采用这种方式进行的。
- 首先通过上面介绍的方法一次获取到
person
对象的 类, 元类, 根元类和根根元类并将结果打印出来 - 然后通过
lldb
命令去读取person
对象的内存情况, 我们知道内存中前8
位存储的就是我们的isa
, 所以通过isa & ISA_MASK
方式来获取到isa
指向的地址并将其打印 - 最后得出的结果如上图所示, 通过内存读取的结果与第1步打印的结果完全一致
- 还有最重要的一点, 可以发现我们的
根元类
与根根元类
的地址其实是完全一样的, 然后查看流程图右上角的箭头指向得出结论: 根元类以后的 isa 都是指向根元类本身 。
总结
通过此篇文章, 主要研究了 isa
相关的一些内容, 包括 isa
的结构组成和指向关系等, 期间也包含了一些内容的补充, 比如: Tagged Pointer、联合体、位域等。这次文章的内容到这里就结束了, 希望内容能对你有所帮助和启发, 如有不对的地方也请大佬们指正, 我们一起探讨共同进步。