本文将分析 OC 对象的本质,对象的内存布局,已经如何为对象分配内存。分析的源码来自 objc-812
对象的本质
打开 objc-812 runtime 的源码可以找到对象的定义:
typedef struct objc_object *id;
struct objc_object {
private:
isa_t isa;
}
id
被类型定义为 objc_object *
,也就是说对象本质上一个 objc_object
结构体。其唯一的变量 isa
的类型为 isa_t
:
#define ISA_MAGIC_MASK 0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE (1ULL<<56)
#define RC_HALF (1ULL<<7)
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 : 44;
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
是一个联合体,可简单理解为 64 位二进制,每一位都代表特定的信息:
nonpointer
: 表示是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。has_assoc
:关联对象标志位,0没有,1存在has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。shiftcls
:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。magic
:用于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced
:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放。deallocating
:标志对象是否正在释放内存has_sidetable_rc
:当对象引用技术大于 10 时,则需要借用该变量存储进位extra_rc
:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
其中需重点理解 nonpointer
和 shiftcls
,举个例子:假如 isa
的值为
0x011d800100008b1d,转为二进制:
当 nonpinter
= 1 时,第 3-47 位为 shiftcls,即类的指针,这是什么呢?后面会分析。
为了方便取出 shiftcls
,可以使用 isa & ISA_MASK
。
define ISA_MASK 0x00007ffffffffff8ULL // 3-47 位为 1
对象的内存布局
在 OC 中,一切对象都是以 objc_object
为基础,那如果一个类声明了多个属性,它的对象在内存中布局是怎样的呢?
@interface MYObject : NSObject
@property(nonatomic, strong) NSString *property1;
@property(nonatomic, strong) NSString *property2;
@property(nonatomic, assign) BOOL bool1;
@property(nonatomic, assign) NSInteger int10;
@property(nonatomic, strong) NSString *property3;
- (void)instanceMethod1;
@end
@implementation MYObject
- (void)instanceMethod1 {
}
@end
int main(int argc, const char * argv[]) {
MYObject *myObject = [MYObject alloc];
myObject.property1 = @"property1";
myObject.property2 = @"property2";
myObject.bool1 = YES;
myObject.int10 = 10;
myObject.property3 = @"property3";
return 0;
}
在 return 0;
打个断点,运行程序,然后在 lldb 中输入 x/8gx myObject
将 myObject 对象内存打印出来。 我们已经知道对象的第一个变量为 isa,并且 isa 中的 3-47 位对应类的指针:
接着打印其他数据:
可以看到对象的内存布局不一定和变量声明的顺序是一样的。由于字节对齐和节省内存,在编译时编译器会进行重排。
对象的内存分配
上面我们已经知道了,内存的布局情况。那么在创建一个对象时,是如何为它分配内存的呢?
OC 的所有对象都是通过 alloc
方法来分配内存,研究 alloc
的内部实现,需要下载可以编译的 runtime 源码。在 [MYObject alloc];
打个断点,此时就可以跳进 alloc
源码里研究它的流程了。大致如下:
[MYObject alloc];
-> _objc_rootAlloc(self);
-> callAlloc(cls, false, true);
-> _objc_rootAllocWithZone(cls, nil);
-> _class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC);
最后的函数 _class_createInstanceFromZone
进行分配,看一下源码:
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
通过设置断点,可以忽略无效的条件判断,可以得到核心的过程为:
size = cls->instanceSize(extraBytes); // 计算对象内存大小
obj = (id)calloc(1, size); // 分配内存
obj->initInstanceIsa(cls, hasCxxDtor); // 初始化 isa,即把类指针关联到对象
所以分配对象内存过程经过了三个步骤:
- 计算内存大小
- 分配内存
- 对象关联类指针
计算内存大小
通过设置断点,可以忽略无效的条件判断,size_t instanceSize(size_t extraBytes)
过程为:
alignedInstanceSize()
-> cache.fastInstanceSize(extraBytes);
-> align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
-
fastInstanceSize
获取对象的内存大小:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
这里暂时不展开,后续写到类的缓存 cache 时,会补充。现在只需知道 这个函数获取对象大小。
-
align16
字节对齐
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
OC 的对象是以 16 位进行对齐。
打个断点,输出 MYObject 的对象大小为 48 个字节。
来验证一下,isa 占 8 个字节,property1/property2/property3 各占 8 个,bool1 占 1 个字节,int10 占 8 个字节,根据 C++ 结构体内存对齐原则,加起来占 48 个字节。
对象关联类指针
obj->initInstanceIsa(cls, hasCxxDtor);; 对象关联类指针
calloc
已经为对象分配好了内存,但此时这块内存还是空的,所以需要类信息关联到这个对象上,也就是为对象的 isa
赋值。
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.setClass(cls, this);
newisa.extra_rc = 1;
}
isa = newisa;
}
上面代码是删了无效信息后的核心代码,先判断 nonpointer
是否有效:
-
nonpointer
为false
,只为isa
设置类指针。 -
nonpointer
为true
,为isa
设置类指针,并且设置isa
的其他位。
再来看看是如何设置类指针的:
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
shiftcls = (uintptr_t)newCls >> 3;
}
将类指针向右移 3 位后赋值给 shiftcls
,这和在分析 isa_t
时讲的是一致的,但为什么要向右移 3 位呢,前面说到 OC 对象是以 16 进行内存对齐,而 OC 的类指针是以 8 字节进行对齐的,也就是地址后面 3 位都是 0,也就没必要进行存储了。
验证
在 obj->initInstanceIsa(cls, hasCxxDtor);
打个断点
输入 x/4gx
打印对象的内存上的内容:
此时,isa
为空的,往下运行一步,输入 x/4gx
:
此时,就找到对象的类信息了。
至此,已经为对象分配好了内存,并且关联了 isa。
小结
文中分析了 OC 对象本质都是 objc_object,每个对象都有一个 isa_t 类型的变量 isa,其存储了类的信息。并分析了对象的内存布局情况,以及对象内存分配和关联 isa 的过程。
那么在 OC 中,类以及属性、方法的本质又是什么呢?类是怎么存储属性和方法的呢?类的缓存又是什么呢?在下一篇文章,将为大家揭晓。