前言
本文的主要目的是探索 isa是如何与当前的类关联在一起的。
1、clang编译器
Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。- 主要是用于底层编译,将一些文件``输出成c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。
- 常用的方式如下:
//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/iPhoneSimulator13.7.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
2、探索对象的本质
- 在main中自定义一个类Person类,有一个name属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
- 通过终端,来到项目根目录下,利用clang将main.m编译成 main.cpp
- 打开生成的main.cpp文件,搜索我们创建的Person。
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
- 通过上面源码我们可以得出结论,对象在底层被编译成了
struct结构体。 - 但是
NSObject_IMPL又是什么呢?于是继续搜索,NSObject_IMPL。
struct NSObject_IMPL {
Class isa;
};
总结
通过上述的探索,我们可以得出结论:
- 1.对象的本质是个结构体。
- 2.
NSObject_IVARS本质是class类型的isa
3、setter方法底层原理探索
由于我们给Person类中有个name属性,我们在底层源码中看到了name的get和set方法。
- name
getter方法底层源码:
static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
- name
setter方法底层源码:
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
- 打开已经下载好的objc可编译源码,全局搜索
objc_setProperty,我们可以找到objc_setProperty的源码实现
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
- 进入
reallySetProperty方法,查看源码实现,底层的实现对旧值的release,对新值的retain。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
//对新值的retain
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
//对旧值的release
objc_release(oldValue);
}
总结
通过对objc_setProperty的底层源码探索,有以下几点说明:
- 1、
objc_setProperty方法本质相当于一个接口,用于关联上层的setter方法以及底层的setter方法。 - 2、如果上层的setter方法有很多,如果直接调用底层setter方法中,会产生很多的临时变量,当你想查找一个sel时,很难处理。
- 3、给上层setter方法提供了接口,通过
SEL _cmd,达到上下层接口隔离的目的。
4、结构体&联合体
1.结构体
结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。
- 优点:
存储容量较大,包容性强,且成员之间不会相互影响 - 缺点:struct内存空间的分配是粗放的,不管用不用,全分配。
2.联合体
联合体也是由不同的数据类型组成,但其变量是互斥的,所有的成员共占一段内存。而且共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖掉。
- 优点:内存使用更为精细灵活,也节省了内存空间。
- 缺点:不够“包容”。
5、isa探索
在iOS底层原理01:alloc&init&new源码分析文章中,我们知道了alloc的三个核心操作:
- 计算要开辟的内存大小:
size = cls->instanceSize(extraBytes) - 申请内存:
obj = (id)calloc(1, size); - 将当前的类和指针地址绑定在一起:
obj->initInstanceIsa(cls, hasCxxDtor); 现在我们来到obj->initInstanceIsa(cls, hasCxxDtor)的源码。
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
1、进入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);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_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;
}
}
2、isa是什么
进入isa的定义, isa = isa_t((uintptr_t)cls),从源码中可以看出isa是通过联合体定义的。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,两者是互斥关系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
-
从源码可以看出,联合体中定义了两个成员
cls和bits和一个结构体位域ISA_BITFIELD(用来存放类信息和其他信息)。初始化isa指针时,有两种初始化方式:
- 1.通过
cls初始化:即isa = isa_t((uintptr_t)cls)。 - 2.通过
bits初始化。
- 1.通过
3、ISA_BITFIELD位域
这里ISA_BITFIELD就是一个位域,它有两个版本,分别对应__arm64__和__x86_64__,即iOS和macOS:
# 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)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
(以 arm64 架构为例)
nonpointer(存储在第0字节):表示是否对isa 指针开启指针优化0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。has_assoc(存储在第1个字节):关联对象标志位,0没有,1存在。has_cxx_dtor(存储在第2个字节):该对象是否有C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。shiftcls(存储在第3-35字节):存储类指针的值。开启指针优化的情况下,在arm64 架构中有 33 位用来存储类指针。magic(存储在第36-41字节):用于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced(存储在第42字节):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。deallocating(存储在第43字节):标志对象是否正在释放内存。has_sidetable_rc(存储在第44字节):当对象引用技术大于 10 时,则需要借用该变量存储进位。extra_rc(存储在第45-63字节):当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
isa的存储分布如下图:
4、进入initIsa源码分析
-
首先通过main中的Person 断点 -->
initInstanceIsa-->initIsa--> 走到else中的isa_t newisa(0); -
执行lldb命令:
p newisa,打印newisa的信息 -
点击
step over继续往下执行,走到newisa.bits = ISA_MAGIC_VALUE;这里给isa的bits成员赋值,重新执行lldb命令p newisa,打印结果如下: -
对比前后打印的结果,发现
nonpointer被赋值1,magic被赋值59。那么magic为什么会被赋值59呢? -
打开计算器并输入0x001d800000000001,从47位开始读取6位,将59转二进制,发现都为111011,如下图:
5、isa指针与类的关联
-
initInstanceIsa的过程是将
calloc的指针和当前的类cls关联起来。 -
isa指针中的
shiftcls位域中存储了类信息。 -
关联流程
-
1、我们将代码运行至
newisa.shiftcls = (uintptr_t)cls >> 3的上一行代码 -
2、查看此时cls,是Person类
-
3、
shiftcls赋值的逻辑是将 Person进行编码后,右移3位 -
4、执行lldb命令
p (uintptr_t)cls,结果为(uintptr_t) $0 = 4294975864 -
5、将
(uintptr_t)cls,执行lldb命令p (uintptr_t)cls >> 3,得到结果uintptr_t) $1 = 536871983 -
6、继续执行代码到
isa = newisa;部分,执行lldb命令p newisa对比
bits赋值的结果: -
此时
cls由默认值,变成了Person,将isa与cls完美关联。 -
shiftcls由0变成了5536871983。 -
为什么需要右移3位?
主要是由于shiftcls处于isa指针地址的中间部分,前面还有3个位域,为了不影响前面的3个位域的数据,需要右移将其抹零。
-
isa指针与类关联的验证
-
方式1:通过 isa & ISA_MSAK
-
上述步骤继续执行,回到
_class_createInstanceFromZone方法,此时cls 与 isa已经关联完成,执行lldb命令po objc -
以16进制格式化打印4段内存情况,执行
x/4gx obj,得到isa的地址0x001d8001000021c9 -
将
isa指针地址和ISA_MASK做与运算,(处于macOS,使用x86_64中的宏定义),即po 0x001d8001000021c9 & 0x00007ffffffffff8,得出Personarm64中,ISA_MASK 宏定义的值为0x0000000ffffffff8ULLx86_64中,ISA_MASK 宏定义的值为0x00007ffffffffff8ULL
-
为什么要与运算?
-
因为isa是联合体,处于
macOS,x86_64中,第3-35位用来存储class信息,isa需要进行一次位运算,才能计算出存储class真实地址,相当于只读取 3-46位数据。 -
-
方式2:通过查看object_getClass的源码
-
在main中导入#import <objc/runtime.h>
-
通过
runtime的api,即object_getClass函数获取类信息 -
查看
object_getClass源码
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
- 查看
getIsa源码
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
- 查看
ISA()源码SUPPORT_INDEXED_ISA是indexed类型,SUPPORT_INDEXED_ISA= 1,将类存储在isa中,以字段作为类表的索引。
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
else 流程中返回了isa.bits & ISA_MASK的结果,与上述的验证一致,获得当前类的信息。
- 方式3:通过位运算
- 回到
class_createInstanceFromZone方法。通过x/4gx obj得到obj的存储信息,当前类的信息存储在isa指针中,且isa中的shiftcls此时占44位(处于macOS环境)
(lldb) x/4gx obj
0x102933830: 0x001d8001000021c9 0x0000000000000000
0x102933840: 0x67616d49534e5b2d 0x7765695674694b65
- 想要读取中间的44位 类信息,就需要经过位运算 ,将右移3位,和左边除去44位以外的部分都抹零,其相对位置是不变的。
- 将isa 右移三位,即低三位抹零
(lldb) p/x 0x001d8001000021c9 >> 3
(long) $1 = 0x0003b00020000439
- 将得到的结果
0x0003b00020000439左移20位,由于shiftcls在3-46位,需要将高17位抹零,上一步中已经右移三位,因此需要左移20位,才能将高17位抹零。
(lldb) p/x 0x0003b00020000439 << 20
(long) $2 = 0x0002000043900000
- 将结果右移17位,回到最初的位置。
(lldb) p/x 0x0002000043900000 >> 17
(long) $3 = 0x00000001000021c8
- 打印cls的信息,由此验证cls 和isa已经完美关联。
(lldb) p/x cls
(Class) $4 = 0x00000001000021c8 Person
###总结
通过对isa的底层原理探索,我们对isa有了新的认识,也清楚isa指针如何与类关联的,下一篇将继续探索isa的走位图。