深入理解isa指针

实例对象的isa指针指对象的类,类对象的isa指针指向对象的元类,这种说法对吗?

对,但也不完全对。因为在arm64架构之前,isa确实只存储着Class、Meta-Class对象的内存地址,但是在之后苹果对isa进行了优化,变成了一个共用体结构,使其能用位域存储更多的信息。而指向Class的指针需要按位与个ISA_MASK才可以获取到。

上图是一张很经典的指针指向图,虽然已经不完全正确,但是还是可以作为参考的。

上一篇文章提到,isa指针被优化成union isa_t类型,源码中搜索可以在objc-private.h文件中找到定义,去除方法简化后的代码如下

union isa_t {
    uintptr_t bits;
private:
    Class cls;
public:
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
};

然后再isa.h文件里面找到ISA_BITFIELD的定义,忽略x86和模拟器,把定义替换到以上代码中,可以变成如下

union isa_t {
    uintptr_t bits;
private:
    Class cls;
public:
    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 unused            : 1;
        uintptr_t has_sidetable_rc  : 1; 
        uintptr_t extra_rc          : 19
    };
};

那这些结构体里面的东西都表示什么意思呢?可以在源码里面搜索找下答案,我的源码版本是objc4-818.2,不同版本可能不太一样。

搜索isa.nonpointer; 会找到一个 hasNonpointerIsa()方法里面使用了,然后继续搜索hasNonpointerIsa(),会发现在objc-object.h文件里面有两个地方定义了这个函数,为什么会有两个呢,不会冲突吗?所以一定有条件编译指令区分编译。继续查找代码,会发现以下内容(简化代码)

#if SUPPORT_NONPOINTER_ISA  //line 172
inline bool 
objc_object::hasNonpointerIsa()
{
    return isa.nonpointer;
}
#else //line 952
inline bool 
objc_object::hasNonpointerIsa()
{
    return false;
}
#endif //1217

对比下很明显可以看出,在SUPPORT_NONPOINTER_ISA部分,返回值应该为true,也就是当nonpointer为1时,表示优化过的指针。

搜索isa.has_assoc; 找到个hasAssociatedObjects()方法被调用,然后搜索has_assoc = 找到setHasAssociatedObjects()方法中被设置为true,但是并没有设置为false的地方。所以当这个值为1的时候,表示被设置过关联对象。

搜索isa.has_cxx_dtor; 找到hasCxxDtor(),然后搜索hasCxxDtor()方法在objc-private.h文件中找到以下注释

// object may have -.cxx_destruct implementation?
bool hasCxxDtor();

可以看出,这个值表示对象是否有C++的析构函数。苹果标识这些的作用是什么呢?

搜索hasCxxDtor();方法,发现objc-runtime-new.mm文件中如下代码

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

可以看出,在对象释放的时候,用于判断是否有关联对象和C++的析构函数,如果没有的话,可以更快的释放对象。

搜索shiftcls,并没有找到有用的信息。不过看名字猜测一定和类有关系,如果获取类指针的话,需要在现有的isa指针上按位与ISA_MASK,查看下ISA_MASK的值为0x0000000ffffffff8ULL,复制粘贴到计算器如图

看到值为1的位的长度正好和shiftcls的长度对应上,按照按位与的逻辑,保留值为1对应部分的值。可以确定shiftcls 存储的Class、Meta-Class对象的内存地址信息。

源码中搜索magic,并没有找到太多有用的信息,通过查询资料,得知这个值是为了在调试的时候分辨对象是否未完成初始化。

weakly_referenced 从字面意思就能看出,这个标识是否有弱引用。相同方法搜索得如下代码

// object may be weakly referenced?
bool isWeaklyReferenced();

搜索extra_rc; 会找到如下代码

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    if (bits.nonpointer) {
        uintptr_t rc = bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

可以看到,extra_rc存储值为对象的引用计数,同时可以看到当has_sidetable_rc为1的时候,还需要加上另一个值才能得到准确的引用计数。

什么时候才会把引用计数写到sidetable中呢。搜索has_sidetable_rc = true 得到如下代码

    if (slowpath(carry)) {
        // newisa.extra_rc++ overflowed
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRetain_overflow(tryRetain);
        }
        // Leave half of the retain counts inline and
        // prepare to copy the other half to the side table.
        if (!tryRetain && !sideTableLocked) sidetable_lock();
        sideTableLocked = true;
        transcribeToSideTable = true;
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
    }

发现newisa.extra_rc++ overflowed,当引用计数过大无法存入到extra_rc,会把引用计数写入到sidetable里面,并把has_sidetable_rc设置为true,extra_rc的值设置为RC_HALF,查看宏定义的值define RC_HALF  (1ULL<<18),可得extra_rc最大存储引用计数为2^18 - 1 = 262143。

通过以上内容可以看出,优化后的指针可以存储很多内容,并且可以更有效率的获取存储的内容。