objc runtime梳理(二):杂七杂八的笔记

0

本文是学习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,它有三个成员变量:

  1. superclass,指向父类,显然。
  2. cache用来缓存实例方法,提高执行效率。
  3. bits存放了所有的实例方法。

此外,它还从objc_object继承了isa,那么,classisa指向什么?指向的是metaclassmetaclass也是objc_class类型的变量,它主要用来存放一个类的类方法。

有以上基本了解后,我们来看这张经典的图:

img

这张图清晰地展现了objc对象的运行时结构:

  1. 对象实例的isa指向class
  2. class的isa指向meta class,class的superclass指向父类
  3. meta class的isa指向root meta class,meta class的superclass指向父类的meta class
  4. root class的isa指向root meta class,root class的superclass指向nil
  5. 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类型的bitsuintptr_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));
}

可以看到下层调用的是getBitsetBitsgetBit比较简单,一个基本的位运算。

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。这个函数返回写入成功/失败。

参考gcc 原子操作函数

到这里,前面的setBits就完全清楚了:

  1. 原子读当前bits
  2. FastAlloc逻辑处理
  3. 原子写,如果失败,重试。
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;
};

rwread-writeroread-onlyclass_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的回答 - 知乎

理解 C++ 的 Memory Order

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];
}

slowpathfastpath,可以看到这两个宏是:

#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的allocWithZonealloc

当没有自定义的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;
}

两个关键的变量,hasCxxCtorhasCxxDtor,其定义如下:

// 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 classgcc - -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。

小结

  1. 性能优化手段__builtin_expect,runtime中包装了fastpathslowpath
  2. hasCxxCtorhasCxxDtor,跟objc++和ARC有关,编译器插入的构造和析构函数
  3. raw isa和nonpointer isa

    • raw isa就是个class指针
    • nonpointer isa则赋值为结构体,其中class指针存在shiftcls,还存了别的信息
  4. 主要流程:

    1. 申请内存空间
    2. 初始化isa
    3. 执行构造函数

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...