iOS开发笔记

objc 中的Tagged Pointer的应用

2019-02-15  本文已影响19人  brownfeng

objc使用TaggedPointer的原因

TaggedPointer是一个能够提升性能, 节省内存的技术. NSString, NSNumber中就采用了这个技术.

对象在内存中都是对齐的, 它们的地址总是指针大小的整数倍, 通常是16的倍数. 在64位系统中, 对象指针是一个64位的整数, 为了对齐, 对象指针的一些位永远是0.

举一个例子, 假设要存储一个NSNumber对象,其值是一个整数. 正常情况下, 如果这个整数只是一个NSInteger的普通变量, 那么它所占用的内存是与CPU的位数有关, 在32位CPU下占4个字节, 在64位CPU下是占8个字节的. 而指针类型的大小通常也是与CPU位数相关, 一个指针所占用的内存在32位CPU下为4个字节, 在64位CPU下也是8个字节. 所以一个普通的iOS程序, 如果没有TaggedPointer对象, 从32位机器迁移到64位机器中后, 虽然逻辑没有任何变化, 但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。

与此同时, 在没有使用TaggedPointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值.

为了改进上面提到的内存占用和效率问题, 苹果提出了TaggedPointer对象. 由于NSNumber, NSDate, NSString等类的变量本身的值需要占用的内存大小常常不需要8个字节, 拿整数来说, 4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位), 对于绝大多数情况都是可以处理的.

在使用TaggedPointer之后, NSNumber指针里面存储的数据变成了: Tag + Data.

因此可以说使用了TaggedPointer以后, 对象指针是一个伪指针, 不再指向对象在堆上的地址, 也就是将数据直接存储在了指针中, 当指针不够存储数据时, 才会使用动态分配内存的方式来存储数据.

如何判断一个对象指针是否是TaggedPointer

我们通过objc判断该对象指针是否是TaggedPointer, 通过查看对象指针的tag bit是否为1来判断, 源码如下:

//如果是iOS平台, 最高有效位是1(第64bit, 使用高位优先规则 MSB)
#define _OBJC_TAG_MASK (1UL<<63)
//如果是mac平台, 最低有效位是1(低位优先规则 LSB)
#define _OBJC_TAG_MASK 1UL

// objc-object.h 中
inline bool objc_object::isTaggedPointer() {
    return _objc_isTaggedPointer(this);
}

// objc-internal.h 中
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

结论: 使用对象指针的最高位bit或者最低位bit来标识该指针是否是Tagged Pointer.

为什么可以通过设定最高位或者最低位是否为 1 来标识呢?

因为在对象指针变量分配内存的时候, 都是按2的整数倍来分配的, 这样分配出来的正常内存地址末位不可能为1, 这样通过将最低标识为 1, 就可以和其他正常指针做出区分.

那么为什么最高位为 1, 也可以标识呢?

这是因为64位操作系统, 设备一般没有那么大的内存, 所以内存地址一般只有 48 个左右有效位, 也就是说高位的 16 位左右都为 0, 所以可以通过最高位标识为 1 来表示TaggedPointer. 那么既然一位就可以标识TaggedPointer了其他的信息是干嘛的呢? 我们可以想象, 要有一些bit位来表示这个指针对应的类型, 不然拿到一个TaggedPointer的时候我们不知道类型, 就无法解析成对应的值。

从源码中我们可以有如下注释, 我进行了翻译:

/***********************************************************************
* Tagged pointer objects. - 对于 TaggedPointer 对象的描述
*
* Tagged pointer objects store the class and the object value in the 
* object pointer; the "pointer" does not actually point to anything.
* TaggedPointer对象的内容, 是将对象指针(object pointer)用来保存类(class)和对象值(object value)!!! `pointer`指针并没有指向任何对象(obejct), 我们称这个为伪指针!!!
*
* Tagged pointer objects currently use this representation:
* (LSB) 低bit
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index  -- 用来表示 object的class
* 60 bits  payload -- 这个负载是 object的class定义的内容(NSString, NSNumber定义的内容不同)
* (MSB) 高bit
* The tag index defines the object's class. 
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an 
* "extended" representation, allowing more classes but with smaller payloads:
* 如果tagindex的内容是`ob111`, 这个`TaggedPointer`对象使用的是`extended`扩展描述法, 这个伪指针可以支持更多的类, 但是对应的用来描述object值的bits减少成52bits
* (LSB) -- 低bit
*  1 bit   set if tagged, clear if ordinary object pointer - 用来展示当前对象指针是taggedPointer还是普通对象指针!!!
*  3 bits  0b111 -- 描述是否是 extended tagged pointer
*  8 bits  extended tag index -- 扩展 tag index
* 52 bits  payload -- 具体的 objct值
* (MSB) -- 高bit
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

总而言之:

  1. 低地址中最低的1bit, 标志当前对象指针(object pointer)是否是TaggedPointer.
  2. 接着3bit位(转化成10进制是 0~7), 标志当前对象是哪个类的内容, 具体标志的内, 通过下面的枚举能看到, 常用的是OBJC_TAG_NSStringOBJC_TAG_NSNumber, 分别是2和3.
  3. 如果tagged index == 0b111, 标志extended tag index.
  4. 其余的60bit 或者 52bit 就用来存储具体的 object value.(如果tag index对应是NSString, object value 是经过编码的, 参考文章中有具体的编码方法)

这样, 通过TaggedPointer这种伪指针方法, 一次性将class - objectValue放在对象指针中, 即节省内存, 又提高了效率. 当然如果60bit/52bit还不能满足object value的范围, objc仍然会把它当做普通对象指针处理!!!

// 具体 tag index 与类关系如下
enum {
    // 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, 

    ...

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

    OBJC_TAG_RESERVED_264      = 264
};

TaggedPointer使用的实例

可以参考文献, 其中有TaggedPointerNSNumberNSString中的实践应用.

注意NSString到最后使用成为: NSTaggedPointerString__NSCFString

参考

1 [译]采用Tagged Pointer的字符串
2 深入理解Tagged Pointer
3 内存管理-Tagged Pointer

上一篇下一篇

猜你喜欢

热点阅读