iOS 底层探索: 学习大纲 OC篇
前言
- 这篇主要内容探索 类与isa是如何关联的。
- 在之前iOS底层探索 alloc&init这篇文章中,我们知道了_class_createInstanceFromZone方法的关键三步:
a. 获取实例的内存空间大小: cls->instanceSize()
b. 根据内存大小,分配内存空间,让实例指向内存开始地址: calloc
c. 关联isa,实例的isa指向类: obj->initInstanceIsa(cls, hasCxxDtor)
, 结合位运算、联合体、位域和结构体的内存对齐的知识,我们探索oc对象的本质从关联isa,实例的isa指向类
开始。
准备工作
先大概了解一个编译器:clang
:
-
clang
是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器,主要是用于底层编译,将一些文件``输出成c++文件,例如main.m 输出成main.cpp; - 其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。
一 、查看类的编译源码
1 .打开终端,cd到文件目录下,利用clang将main.m编译成 main.cpp .
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
其他举例:
//1、将 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/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//2、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//3、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
2 .打开 main.cpp, 找到HJPerson,发现HJPerson在底层会被编译成 struct 结构体
点击NSObject_IMPL
跳转可以得到 isa
struct NSObject_IMPL {
Class isa;
};
先看这里:
struct HJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
这里的知识点:
-
HJPerson中
的第一个属性NSObject_IVARS
等效于NSObject
中的isa
, 每个类中都有默认属性isa
;
二 、翻开objc源码 可以看到 NSObject
的isa
也是class
类型
@interface NSObject {
Class isa OBJC_ISA_AVAILABILITY;
}
现在我们通过clang 看到类被编译成结构体之后,找到isa ,那么isa是如何关联类的信息的呢?
我们可以在 _class_createInstanceFromZone
的核心方法的第三步:关联isa,实例的isa指向类: obj->initInstanceIsa(cls, hasCxxDtor)
中找到答案,这里做了一系列操作isa和类信息的操作,我们再去查看initInstanceIsa
内部源码
inline void
objc_object::initProtocolIsa(Class cls)
{
return initClassIsa(cls);
}
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 = isa_t((uintptr_t)cls);
} else {
//禁用非指针Isa
ASSERT(!DisableNonpointerIsa);
//实例需要原始Isa
ASSERT(!cls->instancesRequireRawIsa());
//关键代码: 初始化isa
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic是ISA_MAGIC_VALUE的一部分
// isa.nonpointer是ISA_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;
}
}
重要代码:isa_t: 指针的初始化
-
isa = isa_t((uintptr_t)cls)
; -
isa_t newisa(0)
;
关键单词:nonpointer
非指针
三 、继续探索isa_t
是怎么做的,我们再次进入源码跳转到isa_t
内部如下:
union isa_t { //联合体 isa_t
//两个初始化方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//一个Class
Class cls;
//一个 bits 。 uintptr_t :在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
// ISA_BITFIELD 这是一个宏
ISA_BITFIELD; // defined in isa.h
};
#endif
};
从union isa_t定义可以看出:
-
提供了两个成员,cls 和 bits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式
通过cls初始化,bits无默认值
通过bits初始化,cls有默认值
-
还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD,这是一个宏定义,有两个版本 arm64(对应ios 移动端) 和 x86_64(对应macOS),以下是它们的一些宏定义,如下图所示:
注:
nonpointer有两个值,表示自定义的类等,占1位
0:纯isa指针
1:不只是类对象地址,isa中包含了类信息、对象的引用计数等has_assoc表示关联对象标志位,占1位
0:没有关联对象
1:存在关联对象has_cxx_dtor 表示该对象是否有C++/OC的析构器(类似于dealloc),占1位如果有析构函数,则需要做析构逻辑
如果没有,则可以更快的释放对象, 析构函数 类似于 oc层面的 dealloc;shiftclx表示存储类的指针的值(类的地址), 即类信息arm64中占 33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针x86_64中占 44位;
magic 用于调试器判断当前对象是真的对象 还是 没有初始化的空间,占6位;
weakly_refrenced是 指对象是否被指向 或者 曾经指向一个ARC的弱变量, 没有弱引用的对象可以更快释放deallocating 标志对象是是否正在释放内存;
has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位;
extra_rc(额外的引用计数) --- 表示该对象的引用计数值,实际上是引用计数值减1, 如果对象的引用计数为10,那么extra_rc为9;
针对两种不同平台,其isa的存储情况如图所示
我们lldb 调试 可以看到经过一系列赋值 将HJPerson类信息存进了shiftcls中
注:
为什么在shiftcls赋值时(newisa.shiftcls = (uintptr_t)cls >> 3)
需要类型强转?因为内存的存储不能存储字符串,机器码只能识别 0 、1这两种数字,所以需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解, 其中uintptr_t是long。
至此,我们得出结论:在isa初始化obj->initInstanceIsa(cls, hasCxxDtor)
的时候,通过isa_t
联合体,在位域
运算中,将类信息cls
存进了存储类的指针的值shiftclx
, 最后isa = newisa
;isa
中既有HJPerson
的指针,又有HJPerson
的信息。就这样isa与类关联到一起了。
四 、拓展 验证 isa 与 类 的关联
简单点 我们在x86_64
中通过isa & ISA_MSAK
验证
流程:
1.在
_class_createInstanceFromZone
方法,此时cl
s 与isa
已经关联完成,执行po objc
2.执行
x/4gx obj
,得到isa指针
的地址0x001d8001000021fd
3.将
isa指针地址 & ISA_MASK
(处于macOS
,使用x86_64
中的宏定义),即po 0x001d8001000021fd & 0x00007ffffffffff8
,得出HJPerson
.x86_64
中,ISA_MASK
宏定义的值为0x00007ffffffffff8ULL
good~~~加油!