本文是学习runtime过程中的笔记,主要是对象初始化和对象结构这块的,比较细碎,emmm,基本上不太是给人看得。
对象基本结构
Class和Object本质上都是结构体。
其定义如下:
typedef struct objc_object *id;
typedef struct objc_class *Class;
struct objc_object {
private:
isa_t isa;
}
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
}
复制代码
对于一个Objc的类,运行时会有一个唯一的objc_class
与之对应,这个类的每个实例就是个objc_object
。
objc_object
有个成员变量isa
,可以先简单理解成指向它对应的Class
的指针,具体内容下面再讲。
objc_class
继承自objc_object
,它有三个成员变量:
superclass
,指向父类,显然。cache
用来缓存实例方法,提高执行效率。bits
存放了所有的实例方法。
此外,它还从objc_object
继承了isa
,那么,class
的isa
指向什么?指向的是metaclass
。metaclass
也是objc_class
类型的变量,它主要用来存放一个类的类方法。
有以上基本了解后,我们来看这张经典的图:
这张图清晰地展现了objc对象的运行时结构:
- 对象实例的isa指向class
- class的isa指向meta class,class的superclass指向父类
- meta class的isa指向root meta class,meta class的superclass指向父类的meta class
- root class的isa指向root meta class,root class的superclass指向nil
- root meta class的super class指向root class,root meta class的isa指向自己
具体内容
objc_object
首先来看objc_object
,它只显式声明了一个成员变量isa
,早年,isa
直接就是个Class
类型的变量,指向它的类,而64位机器出现后,由于虚拟地址并不需要64位这么多的空间,为了提高空间的使用率,使用了isa_t
这个union类型。
这里贴出其在arm64下的定义:
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;
};
};
复制代码
运行时,有一些底层的特殊的类,由于向前兼容的需要,使用了isa.cls
,这就跟32位时代的用法是一致的了,这种形式的isa
称为raw isa
。而通常情况下,isa
使用了下面这个结构体,其中shiftcls
字段存储了Class
指针,其它字段记录了一些额外信息。
objc_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
}
复制代码
1. superclass
没什么可说的,单纯地指向父类。
2.bits
cache先放一边,我们先看class_data_bits_t bits
。
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}
复制代码
bits
里面是个uintptr_t
类型的bits
,uintptr_t
其实就是unsigned long,我们知道unsigned long的长度是平台相关的,在32位下是32位,在64位下是64位。
注释中体贴地告诉我们,这个bits跟前面的FAST标记有关。
以arm64为例,来看一下bits里面都存了什么:
// 是否是Swift类
#define FAST_IS_SWIFT (1UL<<0)
// 是否有默认的Retain/Release等实现
#define FAST_HAS_DEFAULT_RR (1UL<<1)
// 是否需要raw isa
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
// 指向data部分的指针
#define FAST_DATA_MASK 0x00007ffffffffff8UL
复制代码
可以看到,FAST_DATA_MASK
存了个数据指针,其它的都是class相关的几个标记位。我们来逐一看看这几个字段是如何读写的。
2.1 标记位读写
以isSwift
这个位为例,来看一下其读写过程:
#define FAST_IS_SWIFT (1UL<<0)
bool isSwift() {
return getBit(FAST_IS_SWIFT);
}
void setIsSwift() {
setBits(FAST_IS_SWIFT);
}
bool getBit(uintptr_t bit)
{
return bits & bit;
}
void setBits(uintptr_t set)
{
uintptr_t oldBits;
uintptr_t newBits;
do {
oldBits = LoadExclusive(&bits);
newBits = updateFastAlloc(oldBits | set, set);
} while (!StoreReleaseExclusive(&bits, oldBits, newBits));
}
复制代码
可以看到下层调用的是getBit
和setBits
。getBit
比较简单,一个基本的位运算。
setBits
看起来就复杂多了。
LoadExclusive
是原子读操作,看代码:
#if __arm64__
static ALWAYS_INLINE
uintptr_t
LoadExclusive(uintptr_t *src)
{
uintptr_t result;
asm("ldxr %x0, [%x1]"
: "=r" (result)
: "r" (src), "m" (*src));
return result;
}
#elif __arm__
static ALWAYS_INLINE
uintptr_t
LoadExclusive(uintptr_t *src)
{
return *src;
}
#elif __x86_64__ || __i386__
static ALWAYS_INLINE
uintptr_t
LoadExclusive(uintptr_t *src)
{
return *src;
}
#else
# error unknown architecture
#endif
复制代码
可以看到,在arm64下,LoadExclusive
使用了汇编指令ldxr
保证原子性,在其它平台下都是直接读出对应的值。这是因为,在arm64下,默认的变量赋值用的是ldr
指令,不保证原子性。
参考:ARM Compiler armasm Reference Guide和对int变量赋值的操作是原子的吗?
然后看updateFastAlloc
,
#if FAST_ALLOC
static uintptr_t updateFastAlloc(uintptr_t oldBits, uintptr_t change)
{
if (change & FAST_ALLOC_MASK) {
if (((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) &&
((oldBits >> FAST_SHIFTED_SIZE_SHIFT) != 0))
{
oldBits |= FAST_ALLOC;
} else {
oldBits &= ~FAST_ALLOC;
}
}
return oldBits;
}
#else
static uintptr_t updateFastAlloc(uintptr_t oldBits, uintptr_t change) {
return oldBits;
}
#endif
复制代码
注意FAST_ALLOC
这个宏,其实是常关的。
当它关闭时updateFastAlloc
不做任何处理。
当它打开时,其实是判断是否是修改FAST_ALLOC_MASK
这个位,如果是的话,需要满足一定的条件才能改,否则不许改。
再看下面的StoreReleaseExclusive
static ALWAYS_INLINE
bool
StoreReleaseExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
{
return StoreExclusive(dst, oldvalue, value);
}
static ALWAYS_INLINE
bool
StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
{
return __sync_bool_compare_and_swap((void **)dst, (void *)oldvalue, (void *)value);
}
复制代码
这里使用的__sync_bool_compare_and_swap
,提供了原子的比较和交换,如果*dst == oldValue
,就将value
写入*dst
。这个函数返回写入成功/失败。
到这里,前面的setBits
就完全清楚了:
- 原子读当前
bits
FastAlloc
逻辑处理- 原子写,如果失败,重试。
2.2 data部分
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
复制代码
可以看到,从bits
中取出FAST_DATA_MASK
对应的部分,即[3, 47]
位。可以看到取出的是class_rw_t
类型的指针。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
复制代码
rw
是read-write
,ro
是read-only
。class_ro_t
存放的是一个类在编译阶段已经完全确定的信息,因此是只读的;class_rw_t
存放的则是在运行时仍可以修改的信息,因此是可读写的。
data部分的set很有意思
void setData(class_rw_t *newData)
{
assert(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
复制代码
参考:如何理解 C++11 的六种 memory order? - zlilegion的回答 - 知乎
ARM64: LDXR/STXR vs LDAXR/STLXR
简而言之,memory-order是一种保证线程间控制执行顺序的手段,弱于锁但消耗也更小。一般的应用开发中,比较少见。
这里似乎是为了保证get操作和set操作不被重排。(不是很确定)
3. cache
cache里其实是实例方法的缓存,我们来看一下cache_t
这个结构其实是个哈希表。
这里也算是个比较简单的性能优化手段。在class_rw_t
中,有存放方法列表,但那是个数组,我们知道数组的查询效率是O(n)的。因此,把部分常用方法放到一个比较小的哈希表中,就可以大大提高查询效率。
Objc对象初始化学习笔记
以下笔记基于objc-750版本。
首先看NSObject的初始化方法,alloc和new,最终都会走到callAlloc
这个函数中。
+ (id)alloc {
return _objc_rootAlloc(self);
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码
callAlloc
这个函数比较长,一点点来看。
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
复制代码
slowpath
和fastpath
,可以看到这两个宏是:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
复制代码
__builtin_expect
可以参考__builtin_expect 说明,简而言之,它通过预测其中的值进行非常底层的性能优化,不影响逻辑。
cls->ISA()->hasCustomAWZ()
,AWZ是"AllocWithZone"的缩写,可知这里是判断当前class是否有自定义的allocWithZone
方法。当然,通常没有人会去干预对象的内存分配。
如果有自定义的allocWithZone
,则调用class的allocWithZone
或alloc
。
当没有自定义的allocWithZone
时,cls->canAllocFast()
看起来是判断是否能够快速初始化的。点进去发现这个功能目前是关闭的。移除无关代码后逻辑如下:
#if !__LP64__
#elif 1
#else
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<2)
#endif
#if FAST_ALLOC
#else
bool canAllocFast() {
return false;
}
#endif
复制代码
那么,剩下的部分就很明了了:通过class_createInstance
创建obj并返回,如果创建失败就走callBadAllocHandler
。
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
复制代码
两个关键的变量,hasCxxCtor
和hasCxxDtor
,其定义如下:
// class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_CTOR (1<<18)
// class or superclass has .cxx_destruct implementation
#define RW_HAS_CXX_DTOR (1<<17)
复制代码
参考iOS : “.cxx_destruct” - a hidden selector in my class,gcc - -fobjc-call-cxx-cdtors这两个玩意儿一开始是objc++中用来处理c++成员变量的构造和析构的,后来.cxx_destruct
也用来处理ARC下的内存释放。
下一句,bool fast = cls->canAllocNonpointer();
isa
这个变量应该熟悉,它是objc_object
的成员,早些年,它是个单纯的Class
类型的变量,指向这个对象的Class
。后来为了节省64位机器上的空间,它被赋予了更多的内容,即isa_t
类型。
isa_t
是个union,其定义如下:(这里取了arm64下的定义)
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;
};
};
复制代码
可以看到,新的isa_t
,如果填充的是isa.cls
,就跟原来一样,如果填充的是其中的struct,则是新的方式了。
这里,旧的isa被称为raw isa,新的isa被称为nonpointer isa。
后面的逻辑比较清晰,根据cls中记录的size申请内存,然后调用initIsa初始化isa。注意这里的size其实是isa和成员变量所需空间的总和。
最后,如果存在c++构造函数,调用之。
这里看一下initIsa的过程
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = 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;
}
}
复制代码
有个SUPPORT_INDEXED_ISA
的宏,上面已经说过了raw isa和nonpointer isa,其中,nonpointer isa在不同平台的结构体是不太一样的,主要又分为indexed isa和packed isa,indexed isa是用在iWatch上的,iWatch情况比较特殊,为了节省内存,大体上类似在64位CPU上跑32位程序。
isa的初始化看起来也很简单,直接写死了一个Magic number进行初始化,可以参考一下对象是如何初始化的(iOS),对应到isa的struct上,其实就是给indexed和magic两个字段赋值。indexed上面已经讲了,magic则是用来标记当前的isa是否已经初始化了。
isa经过magic number初始化后,写入了两个变量:hasCxxDtor和shiftcls。
hasCxxDtor用于标记是否需要处理析构函数,而shiftcls则真正存储了class的地址。这里右移3位的原因是,这里指针是按照8bit对齐的,后3位必然是0。
小结
-
性能优化手段
__builtin_expect
,runtime中包装了fastpath
、slowpath
-
hasCxxCtor
和hasCxxDtor
,跟objc++和ARC有关,编译器插入的构造和析构函数 -
raw isa和nonpointer isa
- raw isa就是个class指针
- nonpointer isa则赋值为结构体,其中class指针存在shiftcls,还存了别的信息
-
主要流程:
- 申请内存空间
- 初始化isa
- 执行构造函数