NSTaggedPointer和NONPOINTER_ISA

2021-12-04  本文已影响0人  可可先生_3083

内存管理之Tagged pointer

iOS开发者对引用计数这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念,当引用计数为0时,对象就会被释放。但是,真的是所有对象都是这样吗?

内存分配

iOS将虚拟内存按照地址由低到高划分为如下五个区:

[图片上传失败...(image-b07219-1625042343370)]

在程序运行时,代码区,常量区以及全局静态区的大小是固定的,会变化的只有栈和堆的大小。而栈的内存是有操作系统自动释放的,我们平常说所的iOS内存引用计数,其实是就堆上的对象来说的。

如何引入tagged pointer

自2013年苹果推出iphone5s之后,iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63 ,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @100,实际上内存中浪费了很多的内存空间。

当然苹果肯定也认识到了这个问题,于是就引入了tagged pointer,tagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。

我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

image

NSTaggedPointer

我们先看下下面这段代码:

NSMutableString *mutableStr = [NSMutableString string];
    NSString *immutable = nil;
    #define _OBJC_TAG_MASK (1UL<<63)
    char c = 'a';
    do {
        [mutableStr appendFormat:@"%c", c++];
        immutable = [mutableStr copy];
        NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
    }while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

运行结果:

2020-08-08 14:15:54.480862+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e86e57c a NSTaggedPointerString
2020-08-08 14:15:54.481719+0800 TaggedPointerDemo[55468:2078125] 0xdc5050684e80c57f ab NSTaggedPointerString
2020-08-08 14:15:54.482480+0800 TaggedPointerDemo[55468:2078125] 0xdc50506848b0c57e abc NSTaggedPointerString
2020-08-08 14:15:54.483342+0800 TaggedPointerDemo[55468:2078125] 0xdc50506e08b0c579 abcd NSTaggedPointerString
2020-08-08 14:15:54.483950+0800 TaggedPointerDemo[55468:2078125] 0xdc50563e08b0c578 abcde NSTaggedPointerString
2020-08-08 14:15:54.484246+0800 TaggedPointerDemo[55468:2078125] 0xdc56363e08b0c57b abcdef NSTaggedPointerString
2020-08-08 14:15:54.484800+0800 TaggedPointerDemo[55468:2078125] 0xda26363e08b0c57a abcdefg NSTaggedPointerString
2020-08-08 14:15:54.485200+0800 TaggedPointerDemo[55468:2078125] 0xdc527050ee978a35 abcdefgh NSTaggedPointerString
2020-08-08 14:15:54.485644+0800 TaggedPointerDemo[55468:2078125] 0xdcd85e404adcb774 abcdefghi NSTaggedPointerString
2020-08-08 14:15:54.486003+0800 TaggedPointerDemo[55468:2078125] 0x28334c2c0 abcdefghij __NSCFString

上图我们可以看到,当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString

打印结果分析:

NSTaggedPointer标志位

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

上面这个方法我们看到,判断一个对象类型是否为NSTaggedPointerString类型实际上是讲对象的地址与_OBJC_TAG_MASK进行按位与操作,结果在跟_OBJC_TAG_MASK进行对比,我们在看下_OBJC_TAG_MASK的定义:

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

我们都知道一个对象地址为64位二进制,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。

那么我们在看下上面打印出的地址,所有NSTaggedPointerString地址都是0xd开头,d转换为二进制1110,根据上面的结论,我们看到首位为1表示为NSTaggedPointerString类型。在这里得到验证。

注意:TaggedPointer类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位

对象类型

正常情况下一个对象的类型,是通过这个对象的ISA指针来判断的,那么对于NSTaggedPointer类型我们如何通过地址判断对应数据是什么类型的呢?

objc4-723之前

在objc4-723之前,我们可以通过与判断TaggedPointer标志位一样根据地址来判断,而类型的标志位就是对象地址的61-63位,比如对象地址为0xa开头,那么转换成二进制位1010,那么去掉最高位标志位后,剩余为010,即10进制中的2。

接着我们看下runtime源码objc-internal.h中有关于标志位的定义如下:

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

那么我们知道2表示的OBJC_TAG_NSString即字符串类型。因为目前已经无法验证这种情况了 所以我们不做其他类型验证。

objc4-750之后

// Returns a pointer to the class's storage in the tagged class arrays.
// Assumes the tag is a valid basic tag.
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                >> _OBJC_TAG_INDEX_SHIFT)
                               & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;
    // Array index in objc_tag_classes includes the tagged bit itself
#if SUPPORT_MSB_TAGGED_POINTERS ////高位优先
    return &objc_tag_classes[0x8 | obfuscatedTag];
#else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}

classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组objc_tag_classes中获取类指针,而下标的计算方法发是根据外部传递的索引tag。比如字符串 tag = 2。当然这并不是简单的从数组中获取某条数据。

uint16_t NSString_Tag = 2;
uint16_t NSNumber_Tag = 3;
// 3 = 0011
// _OBJC_TAG_INDEX_MASK = 0x7 = 0111
        uintptr_t string_tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                           >> _OBJC_TAG_INDEX_SHIFT)
                                          & _OBJC_TAG_INDEX_MASK);

        uintptr_t number_tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                           >> _OBJC_TAG_INDEX_SHIFT)
                                          & _OBJC_TAG_INDEX_MASK);

// 异或操作 相同返回0 不同返回1
// 2 ^ 3 = 0010 ^ 0011 = 0001
// 3^ 3 = 0011 ^ 0011 = 0000
        uintptr_t string_obfuscatedTag = NSString_Tag ^ string_tagObfuscator;
        uintptr_t number_obfuscatedTag = NSNumber_Tag ^ number_tagObfuscator;

// 按位或
// 1000 | 0001 = 1001 = 9
// 1000 | 0000 = 1000 = 8
        NSLog(@"%@", objc_tag_classes[0x8 | string_obfuscatedTag]);
        NSLog(@"%@", objc_tag_classes[0x8 | number_obfuscatedTag]);

控制台输出为:

TaggedPointer[89420:3027642] NSTaggedPointerString
TaggedPointer[89420:3027642] __NSCFNumber

当我们多次运行时,我们发现实际上每次获取到的string_tagObfuscatornumber_obfuscatedTag都不一样,但是每次从objc_tag_classes中取出的类型均是一致的,因此实际上每次运行objc_tag_classes中的内容也是不断变化的。

如果你想进一步的了解可以参考Objective-C中伪指针Tagged Pointer

NSCFNumber

下面我们在看下NSNumber类型

NSNumber *number1 = @(0x1);
    NSNumber *number2 = @(0x20);
    NSNumber *number3 = @(0x3F);
    NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE);
    NSNumber *maxNum = @(MAXFLOAT);
    NSLog(@"number1 pointer is %p class is %@", number1, number1.class);
    NSLog(@"number2 pointer is %p class is %@", number2, number2.class);
    NSLog(@"number3 pointer is %p class is %@", number3, number3.class);
    NSLog(@"numberffff pointer is %p class is %@", numberFFFF, numberFFFF.class);
    NSLog(@"maxNum pointer is %p class is %@", maxNum, maxNum.class);

我们在看下打印结果:

TaggedPointerDemo[59218:2167895] number1 pointer is 0xf7cb914ffb51479a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] number2 pointer is 0xf7cb914ffb51458a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] number3 pointer is 0xf7cb914ffb51447a class is __NSCFNumber
TaggedPointerDemo[59218:2167895] numberffff pointer is 0xf7346eb004aea86b class is __NSCFNumber
TaggedPointerDemo[59218:2167895] maxNum pointer is 0x28172a0c0 class is __NSCFNumber

我们发现对于NSNumber,我们打印出来的数据类型均为__NSCFNumber,但是我们发现对于MAXFLOAT打印出的地址显然与其他几项不符,上面几个NSNumber的地址以0xf开头,根据字符串地址的经验我们可以看出f = 1111,首位标记位为1,表示这个数据类型属于TaggedPointer。而MAXFLOAT不是。

获取TaggedPointer的值

objc4-723之前

字符串:

[图片上传失败...(image-e6401a-1625042343369)]

从上图的地址中我们就可以看出,从低位到高位分别表示的就是字符串的值(在ASCII码表中的值)

数字:

[图片上传失败...(image-1f04a-1625042343369)]

对于数字来说从地址中也是直接读出存储的值,如上图。

objc4-750之后

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

static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr) 
{
    // assert(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}

static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr) 
{
    // assert(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}

示例代码:

NSString *str1 = [NSString stringWithFormat:@"1"];
        NSString *str11 = [NSString stringWithFormat:@"11"];
        NSString *str2 = [NSString stringWithFormat:@"2"];
        NSString *str22 = [NSString stringWithFormat:@"22"];

        // 0x31 1 0x32 1
        uintptr_t value1 = objc_getTaggedPointerValue((__bridge void *)str1);
        uintptr_t value2 = objc_getTaggedPointerValue((__bridge void *)str2);
        uintptr_t value11 = objc_getTaggedPointerValue((__bridge void *)str11);
        uintptr_t value22 = objc_getTaggedPointerValue((__bridge void *)str22);
        // 以16进制形式输出
        NSLog(@"%lx", value1);
        NSLog(@"%lx", value11);
        NSLog(@"%lx", value2);
        NSLog(@"%lx", value22);

控制台输出:

TaggedPointer[89535:3033433] 311
TaggedPointer[89535:3033433] 31312
TaggedPointer[89535:3033433] 321
TaggedPointer[89535:3033433] 32322

即 "1" = 0x31 1,最后一位表示长度,在ASCII码表中31表示的就是字符1。而且从字符串“11”的结果我们也可以验证上面的说法。

isa 指针(NONPOINTER_ISA)

上面我们说了,对于一个对象的存储,苹果做了优化,那么对于ISA指针呢?

对象的isa指针,用来表明对象所属的类类型。

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

同时结合下图,我们可以更清晰的了解isa指针的作用以及类对象的概念。

image

从图中可以看出,我们所谓的isa指针,最后实际上落脚于isa_t的联合类型。那么何为联合类型呢? 联合类型是C语言中的一种类型,是一种n选1的关系,联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的。比如isa_t 中包含有clsbitsstruct三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls,就不能当bits访问,你把它当bits,就不能用cls来访问。

对于isa_t联合类型,主要包含了两个构造函数isa_t(),isa_t(uintptr_t value)和三个变量cls,bits,struct,而uintptr_t的定义为typedef unsigned long

当isa_t作为Class cls使用时,这符合了我们之前一贯的认知:isa是一个指向对象所属Class类型的指针。然而,仅让一个64位的指针表示一个类型,显然不划算。

因此,绝大多数情况下,苹果采用了优化的isa策略,即,isa_t类型并不等同而Class cls, 而是struct

struct

下面我们先来看下struct的结构体

// ISA_BITFIELD定义如下
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

注意:成员后面的:表明了该成员占用几个bit 而每个成员的意义如下表

标志位说明

| 成员 | bit位 | 说明 | | --- | --- | --- | | nonpointer | 1bit | 标志位。1(奇数)表示开启了isa优化,0(偶数)表示没有启用isa优化。所以,我们可以通过判断isa是否为奇数来判断对象是否启用了isa优化 | | has_assoc | 1bit | 标志位。表明对象是否有关联对象。没有关联对象的对象释放的更快。 | | has_cxx_dtor | 1bit | 标志位。表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快| | shiftcls | 33bit | 类指针的非零位。 | | magic | 6bit | 固定为0x1a,用于在调试时区分对象是否已经初始化。 | | weakly_referenced | 1bit | 标志位。用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。 | | deallocating | 1bit | 标志位。用于表示该对象是否正在被释放。 | | has_sidetable_rc | 1bit | 标志位。用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。(这种情况大多不会发生) | | extra_rc | 19bit | 对象的引用计数减1。比如,一个object对象的引用计数为7,则此时extra_rc的值为6。 |

从上表我们发现,extra_rchas_sidetable_rc是和引用计数相关的标志位,当extra_rc 不够用时,还会借助sidetable来存储计数值,这时,has_sidetable_rc会被标志为1。

接下来我们来验证下,这些标志位是否真的如表中介绍那样。

引用计数

我们先来看下面这段代码

- (void)testisa {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"1\. obj isa_t = %p", *(void **)(__bridge void*)obj);
}

控制台输出结果

TaggedPointerDemo[59983:2185591] 1\. obj isa_t = 0x1a1f335beb1

我们将地址0x1a1f335beb1转换过后:

image

我们看到这时候 对象是nonpointer开启了isa优化,且当前的引用计数器为 extra_rc = 0 + 1 = 1;

下面我们接着测试

NSObject *obj = [[NSObject alloc] init];
    NSLog(@"1\. obj isa_t = %p", *(void **)(__bridge void*)obj);
    _obj1 = obj;
    NSObject *tmpObj = obj;
    NSLog(@"2\. obj isa_t = %p", *(void **)(__bridge void*)obj);

控制台输出为

TaggedPointerDemo[63235:2266690] 1\. obj isa_t = 0x1a1f335beb1
TaggedPointerDemo[63235:2266690] 2\. obj isa_t = 0x41a1f335beb1

我们将地址0x41a1f335beb1转换过后:

[图片上传失败...(image-262798-1625042343368)]

我们看到这时候,我们将obj强引用之后,又实用了一个局部变量对其进行引用,所以这时的引用计数应该为2,当然从图中我们也可以验证这一点。

weakly_referenced

我们这次添加一个弱引用来验证

_weakRefObj = _obj1;
NSLog(@"3\. obj isa_t = %p", *(void **)(__bridge void*)_obj1);

控制台输出为

TaggedPointerDemo[63235:2266690] 3\. obj isa_t = 0x45a1f335beb1

这时候我们仅仅通过地址进行判断 当添加了_obj2 = _obj1后,地址变为0x61a1f335beb1与之前地址0x41a1f335beb1对比

image

上图我们可以看到weakly_referenced标志位被置为1.表示这个对象有被弱引用。

has_assoc

然后我们在添加一个关联属性

NSObject *attachObj = [[NSObject alloc] init];
objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"4\. obj isa_t = %p", *(void **)(__bridge void*)_obj1);

控制台输出为:

TaggedPointerDemo[63235:2266690] 4\. obj isa_t = 0x45a1f335beb3
image

从上图中我们看到has_assoc标志位被置为1.

总结

截止到这里,我们通过观察NSTaggedPointer,相关标志位我们基本了解了NSTaggedPointer是如何存储数据以及标志位的作用。

上一篇下一篇

猜你喜欢

热点阅读