Clang
我们知道iOS中对象对应到底层都是结构体。那么我们怎么才能分析底层的对象源码呢。所以就引入了Clang。
那么什么是Clang呢?
Clang是一个C语言、C++、Objective-C语言的轻量级编译器。它采用了LLVM作为其后端,而且由LLVM2.6开始,一起发布新版本。源代码发布于BSD协议下。
Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C的编译器。
Clang的目标是提供一个GUN编译器套装(GCC)的替代品,支持了GUN编译器大多数的编译设置以及非官方语言的扩展。(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异,并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。
常用的Clang指令
// 把目标文件编译成c++文件
clang -rewrite-objc main.m -o main.cpp
通过编译后,我们可以看出,Person对象,其实对应到底层中其实就是一个结构体,那么我们加些属性,来验证是否真的是这样的。
通过对比,我们可以看出:
- Person类中的属性其实就是结构体中的数据成员。
- Person类中属性的方法,对应到c++中都是对应的静态方法。
- Person类中的setter方法其实都是统一调用了runtime中的objc_setProperty方法。
那么Person对象对应结构体中的第一个成员(struct NSObject_IMPL NSObject_IVARS;)是什么呢?其实它就是我们今天的重点isa了。
isa 结构
isa初始化流程
我们在iOS对象原理探究:alloc & init & new中,曾提到过一个方法initInstanceIsa;
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 = 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;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
initInstanceIsa方法中直接调用了initIsa。
isa_t
通过initIsa的源码可以知道,无论是那个条件中,都是isa_t的结构体。
- !nonpointer 条件下:isa = isa_t((uintptr_t)cls);
- nonpointer 条件下:isa_t newisa(0);
同时根据isa_t的结构我们也可以知道其实isa_t中的 cls 和 bits 其实是互斥的。当我们的类型是nonpointer时,bits会有值,当不是nonpointer时,会直接返回cls。
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 是什么呢?ISA_BITFIELD 其实就是我们NONPOINTER_ISA结构
NONPOINTER_ISA 结构(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)
- nonpointer:表示是否对 isa 指针开启指针优化(0:纯 isa 指针;1:不止是类对象地址,isa中包含了类信息、对象的引用计数等)
- has_assoc:关联对象标志位;0没有,1存在
- has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器(dealloc等),如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
- shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
- magic:用于调试器判断当前对象是真的还是没有初始化的空间
- weakly_referenced:指对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快的释放。
- deallocating:标志对象是否正在释放内存。
- has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位。(是否有外挂的散列表)
- extra_rc:额外的引用计数;表示该对象的引用计数值,实际上是引用计数值减1。(eg:如果对象的引用计数为10,那么 extra_rc 为9。如果应用计数大于 10,则需要使用到 has_sidetable_rc)。
当使用 ISA_MAGIC_VALUE 设置 isa_t 结构体之后,实际上只是设置了 nonpointer 和 magic 这两部分的值。
shiftcls
在设置了 nonpointer、magic、和has_cxx_dtor 之后,我们要将对象对应的类指针存入到 isa 结构体中。
newisa.shiftcls = (uintptr_t)cls >> 3;
将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。
在 _class_createInstanceFromZone
方法中打印了调用这个方法传入的类指针:
可以看到,这里打印出来的所有类指针十六进制地址的最后一位都为 8 或者 0。也就是说,类指针的后三位都为 0,所以,我们在上面存储 Class 指针时右移三位是没有问题的。
而在此结构中,我们可以知道shiftcls其实就是存储类指针的值,那么我们来验证下到底是否是这样呢?
方式一:验证shiftcls
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);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_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;
}
}
通过initIsa的源码分析,我们可以知道当是nonpointer指针是,我们会先给bits赋值(ISA_MAGIC_VALUE),通过注释其实我们可以知道,magic和nonpointer都是ISA_MAGIC_VALUE的一部分,最后我们将cls右移三位就得到了shiftcls。
接下来我们尝试将 NSObject 的类指针和对象的 isa 打印出来,具体分析一下:
代码中打印出来的 isa 结构体中的内容如下:
其中红色部分为类指针,与上面的 [NSObject class] 指针右移三位的结果是完全相同的,这也就验证了我们之前对于初始化 isa 时对 initIsa 方法的分析是正确的。它设置了 indexed、magic 以及 shiftcls。
方式二:验证shiftcls
通过图中的lldb调试,其实我们可以知道通过二进制的偏移,可以得出对象的指针值其实就是shiftcls中存储的值。(x86_64的架构)
那么这只是我们的反推结果,Apple是怎么实现的呢?
源码获取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
}
通过源码可以知道,主要是通过 isa.bits 和 ISA_MASK 进行了位与。那么 ISA_MASK 是什么呢,通过NONPOINTER_ISA 结构(ISA_BITFIELD)中我们可以看出 ISA_MASK 的定义。
arm64分析
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
那么0x0000000ffffffff8ULL是什么呢?为何这个算法这么牛皮呢?
来来来,分析一波!!!
0x0000000ffffffff8ULL 其实就是一个转换成二进制是
111111111111111111111111111111111000
其中前三位是0,中间的33位是1,前面都是0了
对应到我们的isa结构中,进行位与,其实就是将前三位和后17位进行了抹零留下了中间的33位。那么这33位其实就是我们的shiftcls的值了,通过上面的分析,其实我们知道类指针默认后三位都是0,所以不需要我们在右移三位就已经是我们得到的类指针了。