对象在alloc的时候有重要三步。
计算需要开辟的内存空间大小
开辟指定大小的空间
将对象与isa指针关联起来
本文重点分析,如何将对象与isa指针关联起来
。
OC对象的本质
在探索对象与isa关联之前,需要了解到底什么是OC对象。那如何探索OC对象呢?
探索前准备
在探索OC对象的本质之前,先了解一个编译器:Clang
。
Clang
是一个由Apple主导编写
的,基于LLVM
的C/C++/OC的编译器
。Clang
主要用于底层编译,可以将OC文件
转化为C++文件
,这使得我们可以更好的观察底层的结构
及实现逻辑
。常用的Clang编译指令
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
开始探索
- 自定义一个类LGPerson,为了方便观察,在自定义类中增加一个属性name。
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
- 在main函数中定义一个LGPerson对象。
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
NSLog(@"%@",person);
}
return 0;
}
- 使用clang将main函数编译成c++文件
clang -rewrite-objc main.m -o main.cpp
- 打开编译好的main.cpp,找到LGPerson的定义
//NSObject的定义
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底层编译
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
由上面的代码可以看到,LGPerson
在底层会被编译成结构体
。
-
LGPerson_IMPL
中的第一个成员是一个嵌套的结构体NSObject_IMPL
,这个结构体的成员只有一个,那就是isa
。 -
LGPerson_IMPL
中的第二个成员就是定义的属性name。
总结
对象的本质
是一个含有isa的结构体
。isa
是Class类型
。Class
是一个objc_class结构体
类型的指针
。
探索isa
object_class结构在Object.mm中。定义如下:
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
}
struct objc_object {
private:
isa_t isa;
}
由源码可知,objc_class
结构体继承于objc_object
结构体,,而objc_object
中只有一个私有成员isa_t isa
。
接下来我们一项一项分析。
isa_t isa
在arm64之前
,isa
就是一个普通的指针
,只存储类对象
、元类对象
的指针
,但arm64之后isa做了优化
,采用了联合体
的形式,这使得8字节
的内存可以存储更多的内容。
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
对8字节内存的位域进行了宏定义,方便不同架构下代码统一。
接下来以arm64架构对isa的位域进行说明
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
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(
bit0
):是否对isa指针开启指针优化
。- 0 - 纯isa指针;
- 1 - 不止是类对象地址,还包含了类信息、对象引用计数等。
-
has_assoc(
bit1
):关联对象标志位
;- 0 - 没有关联对象;
- 1 - 存在关联对象;
-
has_cxx_dtor(
bit2
):该对象是否有C++或者Objc的析构器,如果有则做析构逻辑,如果没有则可以更快的释放对象。- 0 - 没有C++或者Objc的析构器;
- 1 - 有C++或者Objc的的析构器;
shiftcls(
bit3 - bit35
):存储类的指针
,开启指针优化的情况下在arm64架构
下有33位
来存储类的指针
。magic(
bit36 - bit41
):判断当前对象是否初始化完成
。调试器用来判断当前对象是真的对象还是没有初始化的空间。weakly_referenced(
bit42
):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)deallocating(
bit43
):标志对象是否正在释放内存
。has_sidetable_rc(
bit44
):判断该对象的引用计数
是否过大
,如果过大则需要其他散列表
来进行存储。extra_rc(
bit45 - bit63
):存放该对象的引用计数值减1后的结果
。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为 9。
initIsa
了解了isa的结构之后,接下来看一下是如何将isa与类关联起来的。
inline void
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);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
这个函数主要做三件事:
定义一个newisa结构体,并初始化为全0
向newisa结构体中的各位赋值
把newisa赋值给objc_object的成员变量
这个过程最重要的就是第二步,向newisa结构体中的各位赋值
。
-
newisa.bits = ISA_MAGIC_VALUE;
将newisa.bits
初始化为ISA_MAGIC_VALUE
。
而ISA_MAGIC_VALUE
被宏定义为0x000001a000000001ULL
。转换为二进制就是
从这个对应着isa_t的64个位域,可以看到这是对nonpointer
和magic
赋值。
newisa.has_cxx_dtor = hasCxxDtor;
对has_cxx_dtor赋值newisa.shiftcls = (uintptr_t)cls >> 3
将cls右移3位
,然后赋值给newisa.shiftcls
。
重点来了,这里为什么要右移3位。
shiftcls为什么是(uintptr_t)cls >> 3
cls是Class类型,定义如下:
typedef struct objc_class *Class;
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
// 省略 ...
}
再继续看cache_t
、class_data_bits_t
发现都是结构体,然后看里面的成员变量,大部分都是uintptr_t
类型的,查看定义
typedef unsigned long uintptr_t;
根据内存对齐原则
,可知Class
肯定是8字节对齐
的,同样的,cls
的指向地址(也既开始地址)肯定是8
的倍数,转换成二进制后
,低三位
肯定是000
。
再联想联合体的说明,共用内存
,可见苹果设计优化节省内存
的良苦用心。
赋值shiftcls
的时候既没有改变cls
的值,也最大的优化了内存使用
。
至此,isa与类就关联起来,接下来就来验证了。
验证之前分析
定义一个LGPerson对象
,然后再进入到initIsa函数
中,断点停下。
inline void
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);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
- 当执行
isa_t newisa(0);
时,通过lldb调试输出newisa的值。
(lldb) p newisa
(isa_t) $1 = {
cls = nil
bits = 0
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 0
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
此时newisa
内容为全为0;
- 当执行完位域赋值后再查看
newisa
的值
(lldb) p newisa
(isa_t) $2 = {
cls = LGPerson
bits = 8303516107965037
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 1
shiftcls = 536875085
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
此时可以看到,isa
的shiftcls
已经与类关联起来了。
当isa
的shiftcls
已经与类关联起来之后,回到obj->initInstanceIsa(cls, hasCxxDtor);
这里。
通过lldb输出obj的内容
(lldb) x/4gx obj
0x101233f30: 0x001d80010000826d 0x0000000000000000
0x101233f40: 0x0000000000000000 0x0000000000000000
根据我们的分析,对象是结构体,且第一个成员为isa
。此时0x001d80010000826d
就是isa
。
如何验证该isa就是LGPerson类
呢?
我们前面分析isa的shiftcls就是类
,那如何取出shiftcls
的值呢?
这里仍然是前面的分析,shiftcls
是isa
的bit3 - bit35
。
接下来通过lldb取出isa
的bit3 - bit35
。
//两种方式取出bit3 - bit35
//1. 通过移位的方式
(lldb) p 0x001d80010000826d >> 3
(long) $14 = 1037939513495629
(lldb) p 1037939513495629 << 30
(long) $15 = 576465233028055040
(lldb) p 576465233028055040 >> 27
(long) $16 = 4295000680
(lldb) po 4295000680
LGPerson
//2. 通过掩码的方式
(lldb) po 0x001d80010000826d & 0x0000000ffffffff8ULL
LGPerson
果然,此时isa
的bit3 - bit35
存的就是LGPerson类信息。这也验证了我们之前的分析。
疑问
前面提到newisa.shiftcls = (uintptr_t)cls >> 3;
,即将cls右移了3位
之后再赋值给shiftcls
。那为何取出来的时候直接是Class
而不需要左移3位
呢?
解答
前面有讲到,为什么
右移3位
?
答:因为Class是8字节对齐
,cls
的指向地址
(也既开始地址)肯定是8的倍数
,转换为二进制
后后3位就是0
,出于优化的考虑
,在赋值的时候无需将无用的后3位也赋值过去。赋值之后如何取?
我们来看苹果是如何取出Class
的。
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
这里直接将isa.bits
与掩码ISA_MASK
进行与操作
后,强转成Class
类型。
为什么这样就可以取出Class
呢?
答:因为进行与操作
可以直接将isa.bits
除bit3 - bit35
保留外,其余的位都置0
,此时就相当于bit3 - bit35
为Class
右移3位的值,而bit0 - bit2
为Class
的后三位。这样就与Class
结构对应了。因此可以通过强转获得Class
信息。