1、前言概述
- 本篇文章首先讲述
isa
的作用 , 实际数据结构 , 其中不同二进制位存储内容说明 , 包括isa
优化 , 是否为TaggedPoint
. - 然后以引用计数为例实际探索 .
- 最后讲述
isa
的指向 , 以及SuperClass
的指向探索 . - 其中穿插了一些面试题以及涉及到的知识点 .
isa
是我们能把底层知识点串联起来最为关键的一条引线 . 通过本篇文章探索 , 对于对象的本质有更深层次的理解 .
2、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
其实就是一个指针 , 起到指向的作用 , 将对象 , 类 , 以及元类连接起来 , 后来苹果针对其进行了优化 , 采用 联合体 + 位域 的方式来节省内存 与存储更多内容 .
② 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
.
TaggedPointer
- 在开始使用
64
位机器也就是iPhone 5S
时 , 指针对象占用8
字节内存 . - 也就是说当我们存储基础数据类型 , 底层封装成
NSNumber
对象 , 也会占用8
字节内存 , 而32
位机器下占用4
字节 . - 因此如果没有额外处理 , 在迁移到
64
位机器下时 , 会造成很大空间浪费 .
因此 , 为了节省内存和提高执行效率,苹果提出了
Tagged Pointer
的概念。对于64
位程序,引入Tagged Pointer
后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。
未引入 Tagged Pointer
为了存储和访问一个 NSNumber
对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。
引入 Tagged Pointer
由于
NSNumber
、NSDate
一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿 (注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。
因此苹果将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer
对象之后 , 对象的指针其实不再是传统意义上的指针 .
Tagged Pointer 作用
Tagged Pointer
特点:
- 1️⃣ :
Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
- 2️⃣ :
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
- 3️⃣ : 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍 .
(
objc_msgSend
能识别Tagged Pointer
,比如NSNumber
的intValue
方法,直接从指针提取数据 ) - 4️⃣ : 使用 Tagged Pointer 后,指针内存储的数据变成了
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
.
NONPOINTER_ISA
我们已经知道对象的 isa
指针,是用来表明对象所属的类类型。
但是如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。
于是,就像
Tagged Pointer
一样,对于isa
指针,苹果同样进行了优化。isa
指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了引用计数extra_rc
,是否有被weak
引用标志位weakly_referenced
,是否有附加对象标志位has_assoc
等信息 , 使用的就是我们刚刚提到的 联合体 + 位域 的数据结构 .
而 nonpointer
就是是否进行优化的标识 , 优化之后的联合体中存储了是否优化的标识在其中的 struct
的第一个二进制位中 . ( 下面我们会仔细讲述 ) .
那么接下来 , 终于进入到 isa_t
的结构了 .
isa 内存结构
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_t
从上源码得知 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 实战演练 - 引用计数探索 - 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 .- iOS 小端模式 , 读取二进制位时从右往左读 .
@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
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 指向探索
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
优化 .
lldb 调试探索
调试结果如下 :
结论 1️⃣ : 实例对象的 isa
指向类对象 .
结论 2️⃣ : 类对象的 isa
指向元类对象 .
( 注意 , 元类对象的地址与类对象不同 )
结论 3️⃣ : 根元类 ( NSObject 的元类 ) 指向自己 .
结论 4️⃣ : 元类的 isa
指向根源类 .
⑤ superClass 指向探索
提示 :
对象中第 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
流程指示图 .
3、总结
isa
是连接实例对象 , 类与元类的重要桥梁 .isa
在 64 位机器开始引入TaggedPointer
, 与isa
的优化 (NONPOINTER_ISA
) , 使用联合体 + 位域的模式 , 来存储更多内容 , 取值方式也变成使用掩码 mask 位运算获取真实cls
.isa
指向 :
- 实例对象的
isa
指向类对象 .- 类对象的
isa
指向元类对象 . ( 注意 , 元类对象的地址与类对象不同 , 名称相同 ) .- 根元类 (
NSObject
的元类 ) 指向自己 .- 元类的
isa
指向根源类 .superClass
指向 :
- 子类
superClass
指向父类 .- 根源类
superClass
指向NSObject
类 .
至此 , isa
的相关知识点我们已经探索完毕了 . 后续继续更新类的结构探索 , KVC
, KVO
, RunLoop
等底层探索 , 敬请关注 .