iOS内存管理系列第一篇-初识id指针

iOS内存管理系列第一篇-初识id指针

IOS小彩虹2021-07-11 15:01:19100A+A-

id类型源码解析

iOS中任何变量都有明确定义属于哪种类型,对象指针也是如此,属于id类型。id其实是结构体struct objc_object类型的指针.

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

而结构体struct objc_object仅仅有一个Class类型的成员变量isa。Class又是struct objc_class结构体类型。 所以,id就是指向struct objc_object {objc_class *isa}

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

到此为止,我们无法再深入理解下面的具体结构,因为Xcode中仅仅开放了objc的9个头文件,并且代码太过老旧。我们需要从苹果的opensource下载一份比较新的来看,不幸的是苹果提供的源码因为缺少依赖和一些头文件,无法编译成功,这里有一份Github上可以编译和调试的源码OjbcDeubg。本文后续的objc源码全部基于objc750.1版本。

我们可以看到struct objc_object中isa已经变为isa_t类型,并且是私有成员不能从外部直接访问。

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    ......
}

isa_t的结构类型是一个联合体,联合体是把几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构。所以isa_t的成员变量可能是Class cls;,也可能是uintptr_t bits;,也可能是struct { ISA_BITFIELD; // defined in isa.h };

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

# 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

所以,从现在来看,struct objc_objectd中的isa并不一定指向的是Class,可能是一个无符号整数,也可能是一个结构体位域,关于结构体位域的相关知识可以参考这篇文章。 我们之前有听说过对象的指针指向类类型么?现在证明这个结论是错误的。对象指针可能是指向类类型,也可能仅仅是一个无符号整数,或是一个结构体位域。

如何不浪费64bit,用好isa的内存空间?

在2013年9月,苹果推出了iPhone5s配备了 64 位架构的处理器。64位CPU下,指针所占用的位数为8个字节64位。一个内存地址实际上用不了64位去存储,一般32位即可存储一个20亿的数(2^31=2147483648,另外1位作为符号位)。所以,苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。

TaggedPoint

对于NSDateNSNumber这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value值直接存储在isa_t中的uintptr_t bits;上,并且用一些特殊标识来标明此isa是TaggedPoint类型的。这样用isa就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。

NonpointerIsa

isa其实并不单单是一个指针,实际上只有33位用于存储对象地址。其余位用来存储一些特殊的值。

uintptr_t nonpointer        : 1;    //标识是否为nonpointer
uintptr_t has_assoc         : 1;    //是否有关联对象
uintptr_t has_cxx_dtor      : 1;    //是否有C++的一些操作
uintptr_t shiftcls          : 33;   //对象地址
uintptr_t magic             : 6;    //魔数
uintptr_t weakly_referenced : 1;    //是否有弱引用
uintptr_t deallocating      : 1;    //是否正在释放 
uintptr_t has_sidetable_rc  : 1;    //是否在sidetable中有存储引用计数
uintptr_t extra_rc          : 19    //在当前isa中存储的引用计数值

强调一点,很多教程讲MRC中引用计数是通过sidetable来存储,但其实isa_t中已经拿出了19位来存储对象的引用计数,这个引用计数的值已足够大,大多数情况19位已经够存储其引用计数值,所以并不需要使用sidetable来存储额外超过的引用计数值。

源码解读TaggedPoint

inline Class objc_object::getIsa()  
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

现在的iOS已经无法直接访问obj_object的isa,在获取isa的时候需要调用getIsa()方法。此方法中首先判断是否为taggedPonter,如果不是则调用ISA()方法,是的话则判断是否为ExtTaggedPointer,然后再按位与操作得到class类型。所有支持TaggedPointer的class被保存在全局数据变量OBJC_EXPORT Class _Nullable objc_debug_taggedpointer_ext_classes[]中。

在判断是否为ExtTaggedPointer时,调用了_objc_decodeTaggedPointer方法,在要获取slot时要先让ptrobjc_debug_taggedpointer_obfuscator进行按位异或操作,然后拿异或后的值与_OBJC_TAG_EXT_MASK进行与操作来判断是否是ExtTaggedPointer

_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

网上有很多教程是这样的

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        int a = 0;
        NSObject *obj = [[NSObject alloc] init];
        NSNumber *num0 = @0;
        NSNumber *num1 = @1;
        NSNumber *num2 = @2;
        NSNumber *num3 = @8;
        
        NSNumber *num4 = @0xFFFFFFFFFFFF;
        
        NSLog(@"%p, %p, %p, %p, %p", num0, num1, num2, num3, num4);
    }
    return 0;
}

因为num0、num1、num2、num3的值没有超过32位的存储空间,所以通过打印其地址可以看到0,1,2,3的值是直接保存到地址里的。但遗憾的是,大家试一下发现都是错误的。

0x59aa0169f2f5214b, 0x59aa0169f2f5204b, 0x59aa0169f2f5234b, 0x59aa0169f2f5294b, 0x5955fe960d0ade5b

从这段输出中,并不能发现什么规律。

就是因为上文提到了objc_debug_taggedpointer_obfuscator,苹果对TaggedPointer的值进行了按位异或混淆。在ios应用启动装载的过程中装载objc时会调用到initializeTaggedPointerObfuscator中,这里生成了一个随机数objc_debug_taggedpointer_obfuscator,所有的TaggedPointer都用此随机数进行了混淆,所以我们看不出规律来。

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation. DisableTaggedPointerObfuscation) { objc_debug_taggedpointer_obfuscator = 0; } else { // Pull random data into the variable, then shift away all non-payload bits. arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } } 

幸运的是,我们拿到了源代码。所以可以把此随机数改为0。

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation. DisableTaggedPointerObfuscation) { objc_debug_taggedpointer_obfuscator = 0; } else { // Pull random data into the variable, then shift away all non-payload bits. arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } #warning write by test objc_debug_taggedpointer_obfuscator = 0; objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } 

然后我们在run一次代码,执行结果如下

0x27, 0x127, 0x227, 0x827, 0xffffffffffff37

第一位的0x27等于0x027,这样我们才真切的感受到TaggedPointer真的是将value值直接保存到了isa中。

源码解读NonpointerIsa

MRC时代,我们是可以直接获取引用计数的。ARC下无法直接获取,但NSObject中还是对我们暴露了retainCount函数,我们去追踪retainCount做了哪些事情。

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

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

retainCount首先判断是否为taggeePointer,如果是则直接返回isa,不是的话判断是否为nonpointer,是的话,要先从bits中即我们上文提到的结构体位域中取出extra_rc中存储的引用计数,然后根据bits中的has_sidetable_rc来断定是否有sidetable中存储该对象的引用计数。如果有则加上sidetable中存储的引用计数,无则直接返回bits.extra_rc

至此,我们也分析出nonPointerISA中,isa并不是一个纯粹的指向class,而是还携带了一些附加信息。

总结

iOS的内存管理中,并不是仅仅用到散列表SideTables来存储引用计数,而是针对不同的对象及CPU位数做了特殊的事情。在64位CPU下,有TaggedPointer来存储小对象,有NonPonterISA这种非指针型isa来管理引用计数等,还有散列表来存储超过范围的引用计数和弱引用关系。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们