isa
的作用 , 实际数据结构 , 其中不同二进制位存储内容说明 , 包括 isa
优化 , 是否为 TaggedPoint
.isa
的指向 , 以及 SuperClass
的指向探索 .
isa
是我们能把底层知识点串联起来最为关键的一条引线 . 通过本篇文章探索 , 对于对象的本质有更深层次的理解 .
Objective-C
是一门面向对象的编程语言。每个对象都是其 类 的实例 , 被称为实例对象 . 每一个对象都有一个名为 isa
的指针,指向该对象的类。
新建 Command Line
工程 , 新建一个类 LBObject
, 写一个属性一个成员变量 , clang
编译.
clang -rewrite-objc main.m -o main.cpp 复制代码
打开 main.cpp
. 我们看到如下 :
typedef struct objc_object LBPerson; typedef struct {} _objc_exc_LBPerson; #endif extern "C" unsigned long OBJC_IVAR_$_LBPerson$_name; struct LBPerson_IMPL { struct NSObject_IMPL NSObject_IVARS; NSString *name; NSString *_name; }; struct NSObject_IMPL { Class isa; }; 复制代码
可以看到 , 类其实就是一个包含 isa
指针的结构体 .
来看下 NSObject
源码 :
@interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isa OBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop } 复制代码
从此可以得知 , 类也是一个对象 , 我们称之为类对象 .
源码如下 :
typedef struct objc_class *Class; /// A pointer to an instance of a class. typedef struct objc_object *id; 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 /**/ } /// Represents an instance of a class. struct objc_object { private: isa_t isa; public: // ISA() assumes this is NOT a tagged pointer object Class ISA(); // getIsa() allows this to be a tagged pointer object Class getIsa(); /*...*/ } 复制代码
是不是有点傻傻分不清了 . 梳理一下 :
其指示关系如下图 .
图片引用自 : iOS 内存管理在最初的时候 , isa
其实就是一个指针 , 起到指向的作用 , 将对象 , 类 , 以及元类连接起来 , 后来苹果针对其进行了优化 , 采用 联合体 + 位域 的方式来节省内存 与存储更多内容 .
先来看下 对象的 getIsa
方法 :
#if SUPPORT_TAGGED_POINTERS 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]; } } 复制代码
这里引出一个概念 , TaggedPointer
.
64
位机器也就是 iPhone 5S
时 , 指针对象占用 8
字节内存 .NSNumber
对象 , 也会占用 8
字节内存 , 而 32
位机器下占用 4
字节 .64
位机器下时 , 会造成很大空间浪费 .因此 , 为了节省内存和提高执行效率,苹果提出了
Tagged Pointer
的概念。对于64
位程序,引入Tagged Pointer
后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。
为了存储和访问一个 NSNumber
对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。
由于
NSNumber
、NSDate
一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿 (注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。
因此苹果将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer
对象之后 , 对象的指针其实不再是传统意义上的指针 .
Tagged Pointer
特点:
Tagged Pointer
专门用来存储小的对象,例如 NSNumber
和 NSDate
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc
和 free
objc_msgSend
能识别 Tagged Pointer
,比如 NSNumber
的 intValue
方法,直接从指针提取数据 )Tag
+ Data
,也就是将数据直接存储在了指针中 .那么回到 getIsa
方法中 , 当为对象类型时 , 很明显是非 isTaggedPointer
. 直接来到 ISA() ;
#if SUPPORT_NONPOINTER_ISA 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 return (Class)(isa.bits & ISA_MASK); #endif } 复制代码
这里又看到一个 NONPOINTER_ISA
与 INDEXED_ISA
. INDEXED_ISA
源码如下 :
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__) # define SUPPORT_INDEXED_ISA 1 #else # define SUPPORT_INDEXED_ISA 0 #endif 复制代码
也就是说 64 位机器下为 1 . 那么我们来说一说 NONPOINTER_ISA
.
我们已经知道对象的 isa
指针,是用来表明对象所属的类类型。
但是如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。
于是,就像
Tagged Pointer
一样,对于isa
指针,苹果同样进行了优化。isa
指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了引用计数extra_rc
,是否有被weak
引用标志位weakly_referenced
,是否有附加对象标志位has_assoc
等信息 , 使用的就是我们刚刚提到的 联合体 + 位域 的数据结构 .
而 nonpointer
就是是否进行优化的标识 , 优化之后的联合体中存储了是否优化的标识在其中的 struct
的第一个二进制位中 . ( 下面我们会仔细讲述 ) .
那么接下来 , 终于进入到 isa_t
的结构了 .
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
数据结构其实为 isa_t
, 是一个联合体 ( 或者叫共用体 ,union
) .
其中 ISA_BITFIELD
宏定义在不同架构下表示如下 :
# if __arm64__ # 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 # elif __x86_64__ # 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 复制代码
首先看到 isa_t
是一个联合体的数据结构 , 联合体意味着公用内存 , 也就是说 isa
其实总共还是占用 8 个字节内存 , 共 64 个二进制位 .
而上述不同架构的宏定义中定义的位域就是 64 个二进制位中 , 每个位置存储的是什么内容 .
- 由于联合体的特性 ,
cls
,bits
以及struct
都是 8 字节内存 , 也就是说他们在内存中是完全重叠的 .- 实际上在
runtime
中,任何对struct
的操作和获取某些值,如extra_rc
,实际上都是通过对bits
做位运算实现的。bits
和struct
的关系可以看做 :bits
向外提供了操作struct
的接口,而struct
本身则说明了bits
中各个二进制位的定义。
以获取有无关联对象来举例 :
可以直接使用
isa.has_assoc
, 也就是点语法直接访问bits
中第二个二进制位中的数据 . ( arm 64 架构中 )
因此 , bits
与 struct
的关系理解清楚以后 , 我们 isa
其实就有两种情况 , cls
或者是 bits
, 也就是我们刚刚所提到的 nonPointer_isa
与否 , 两种情况完美验证 . 如下图 :
arm64
架构下 , ISA_BITFIELD
. 我们来看看每个字段都存储了什么内容 , 以便更深刻的理解对象的本质 .
# 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 复制代码
-成员- | 位 | 含义 |
---|---|---|
nonpointer | 1bit | 标志位 - 1 ( 奇数 )表示开启了isa优化,0 ( 偶数 ) 表示没有启用isa优化 |
has_assoc | 1bit | 标志位 - 表明对象是否有关联对象。没有关联对象的对象释放的更快 , 关联对象可以参考 Category底层原理 |
has_cxx_dtor | 1bit | 标志位 - 表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快 , 参考 OC 对象的创建流程 中有详细叙述对象释放完整流程 |
shiftcls | 33bit | 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。 |
magic | 6bit | 用于调试器判断当前对象是真的对象还是没有初始化的空间 , 固定为 0x1a |
weakly_referenced | 1bit | 标志位 - 用于表示该对象是否被别ARC对象弱引用或者引用过。没有被弱引用的对象释放的更快 |
deallocating | 1bit | 标志位 - 用于表示该对象是否正在被释放 |
has_sidetable_rc | 1bit | 标志位 - 用于标识是否当前的引用计数过大 ( 大于 10 ) ,无法在 isa 中存储,则需要借用sidetable来存储 |
extra_rc | 19bit | 实际上是对象的引用计数减 1 . 比如,一个 object 对象的引用计数为7,则此时 extra_rc 的值为 6 |
以上就是 arm64
架构下 isa 每一个位置所存储的内容 , x86
架构下存储数据不变 , 只是占据位有所不同 , 就不重复讲述了 .
那我们接下来以 extra_rc
为例来探索一下其存储和获取的过程 .
首先再来看一下这张图 , 我们如何能拿到 isa_t
NSObject * obj = [[NSOibect alloc] init]; 复制代码
分析过程 :
- 1?? : 那么
obj
就是一个NSObject *
类型 . 而NSObject
是一个Class isa
, 也就是objc_class *
- 2?? : 换句话说 ,
obj == objc_class **
. 而objc_class
继承于objc_object
. 也就是说objc_class
的首地址其实就是objc_object
- 3?? : 由于
objc_object
内部就是一个isa_t
. 因此obj == objc_class **
可以替换成obj == objc_class **
- 4?? : 当
isa
开启优化时 , 也就是说isa
不再是一个指针 , 而是我们之前讲的联合体 . 因此改为obj == isa_t *
综上 , 我们得到结论 , obj
就是一个 指向 isa_t
的指针 .
接下来我们来实际演练一遍 .
注意:
arm64
下 isa_t
中后 19 位为 extra_rc
.extra_rc
实际存储为引用计数减 1 .@interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSObject *obj =[NSObject alloc]; NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj); } 复制代码
注意使用真机 , 使用 arm64
架构下的 bits
.
实际上 obj
对象此时引用计数为 1 , 预期一致 .
接下来修改代码如下 :
@interface ViewController () @property(nonatomic, strong) NSObject *obj1; @property(nonatomic, strong) NSObject *obj2; @property(nonatomic, weak) NSObject *weakRefObj; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSObject *obj =[NSObject alloc]; _obj1 = obj; NSObject *tempObj = obj; NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj); } 复制代码
引用计数显示为 2 , 实际为 3 - 1 = 2
. 符合预期 .
最后添加代码如下 :
- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1); _obj2 = _obj1; NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1); _weakRefObj = _obj1; NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1); NSObject *attachObj = [[NSObject alloc] init]; objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1); } 复制代码
分别在 NSLog
断点打印并查看 isa_t
结果如下 .
引用计数 , 弱引用标识 , 关联对象标识 均符合预期 .
最后 , 来看下实际 isa
指向内容 ( 在 isa_t
3 - 36 位中存储为 shiftcls
指针 , 相当于 isa
未优化时的 NONPOINTER_ISA
指针 )
isa 在 lldb 中 , 获取 isa_t 中某一段位置的数据 , 直接通过 宏定义中提供好的 mask 可以快速获取指定位置存储的值 .
# define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL 复制代码
通过以上探索 , 我们很清楚 runtime
中 isa
的具体结构 . 那么接下来 , 我们来探索一下 isa
的指向 , 这个问题也已经是面试常客的地位了 .
在探索 isa
指向之前 , 我们需要知道这个问题 :
class
、objc_getClass
、object_getclass
方法有什么区别 ?
( 这个问题来自 阿里、字节:一套高效的iOS面试题 这篇文章 , 刚好在写探索 isa
文章 , 就一起来解答一下 ) .
先上源码 .
+ (Class)class { return self; } - (Class)class { return object_getClass(self); } 复制代码
Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } Class objc_getClass(const char *aClassName) { if (!aClassName) return Nil; // NO unconnected, YES class handler return look_up_class(aClassName, NO, YES); } 复制代码
getIsa
方法上面粘贴过 , 我们就不沾了 . 其实就是获取 isa
指向 .
1??、 class
方法 .
isa
指向也就是类对象 ( 也就是 - class
) .+ class
) .2??、object_getClass
方法.
object_getClass
其实就是获取 isa
指向 .
也就是说 当需要获取元类时 , 则需要使用类对象调用 object_getClass
.
写法如下 :
void test(){ NSObject *obj =[NSObject alloc]; // NSObject类 Class class = object_getClass(obj); // NSObject元类 Class metaClass = object_getClass(class); } 复制代码
3??、objc_getClass
方法
写法如下 :
Class objcClass = objc_getClass("NSObject"); 复制代码
了解了这几个函数以后 , 我们就开始探索 isa
的指向了 .
提示 : ( 在探索前 , 请对 OC类对象/实例对象/元类 有详细了解 )
isa 走位流程图 , 图片引用自官方文档
新建一个 LBSuperClass
类继承于 NSObject
, 一个 LBSubClass
继承于 LBSuperClass
, 创建代码如下 .
NSObject * object = [NSObject alloc]; LBSuperClass * superClass = [LBSuperClass alloc]; LBSubClass * subClass = [LBSubClass alloc]; 复制代码
三个对象创建完加断点 , 运行代码.
提示 :
lldb
命令
x/4g
代表打印对象首地址开始连续 4 个 8 字节内存地址内容 .X/5g
,x/6g
以此类推 , 就免去因为小端模式x
打印必须从右往左读的问题 .
p/t
、p/o
、p/d
、p/x
分别代表二进制、八进制、十进制和十六进制打印 .通过前面探索 , 我们知道对象首地址前 8 个字节 , 其实就是
isa
.要使用真机跑 , 模拟器不开启
isa
优化 .
调试结果如下 :
结论 1?? : 实例对象的 isa
指向类对象 .
结论 2?? : 类对象的 isa
指向元类对象 .
( 注意 , 元类对象的地址与类对象不同 )
结论 3?? : 根元类 ( NSObject 的元类 ) 指向自己 .
结论 4?? : 元类的 isa
指向根源类 .
提示 :
对象中第 8 - 16 位置存储的是 superClass
指针地址 .
struct objc_class : objc_object { // Class ISA; 继承与父类 objc_object Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; } 复制代码
结论 1?? : 子类继承父类 ( 好像等于没说 , 忽略.. ) .
结论 2?? : NSObject
的元类 ( 根源类 ) 的父类指向 NSObject
类对象 .
完美验证了上述 isa
流程指示图 .
isa
是连接实例对象 , 类与元类的重要桥梁 .isa
在 64 位机器开始引入TaggedPointer
, 与isa
的优化 (NONPOINTER_ISA
) , 使用联合体 + 位域的模式 , 来存储更多内容 , 取值方式也变成使用掩码 mask 位运算获取真实cls
.isa
指向 :
- 实例对象的
isa
指向类对象 .- 类对象的
isa
指向元类对象 . ( 注意 , 元类对象的地址与类对象不同 , 名称相同 ) .- 根元类 (
NSObject
的元类 ) 指向自己 .- 元类的
isa
指向根源类 .superClass
指向 :
- 子类
superClass
指向父类 .- 根源类
superClass
指向NSObject
类 .
至此 , isa
的相关知识点我们已经探索完毕了 . 后续继续更新类的结构探索 , KVC
, KVO
, RunLoop
等底层探索 , 敬请关注 .