iOS 内存管理(四)-Tagged Pointer
Tagged Pointer 对象一般用于 NSNumber
、NSDate
、NSString
等小对象的存储。通常来说,普通对象对象需要动态分配内存、维护引用计数等,对象指针存储的是堆中的对象的地址值。而 Tagged Pointer 对象呢,其指针里面不是地址,而是它的值。所以 Tagged Pointer 实际上已经不能算是对象了,只是一个对象皮的普通变量。它的内存并不存在堆中,也不需要 malloc
和 free
。Tagged Pointer 对象不仅节省内存,在内存读取和对象创建上效率大大提高。
1.什么样的对象算小对象?
所有的 NSNumber
、NSDate
、NSString
都是小对象吗?
小对象往往指的是占内存较小的对象,小道什么程度呢?小到它的值可以存储在对象的指针里面
。在 iOS 中对象的指针是 8 位,8*8=64bit,由于对象指针本身还要存储地址,对很多占用内存比较小的对象,比如 NSNumber
、NSDate
、NSString
,它们有时候内存很小,可以直接和地址存储在对象指针里面,不需要开辟堆空间来存储。但是当它们的内存比较大时,指针存不下时,也会开辟堆空间来存储。下面我们以字符串为例子来演示一下:
首先创建一个字符串属性,用 strong
修饰:
@property (nonatomic, strong) NSString *aString;
接着多线程给 aString 进行赋值操作,如 demo1:
dispatch_queue_t queue = dispatch_queue_create("com.djx.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(queue, ^{
self.aString = [NSString stringWithFormat:@"123456789"];
NSLog(@"%@",self.aString);
});
}
这段代码运行时正常的。但是如果我们把字符串长度在增加一下,如 demo2:
dispatch_queue_t queue = dispatch_queue_create("com.djx.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(queue, ^{
self.aString = [NSString stringWithFormat:@"1234567890"];
NSLog(@"%@",self.aString);
});
}
这段代码就会崩溃,为什么?这里 demo2 和 demo1 的不同仅仅是因为 demo1 的字符串长度为 9 位,而 demo2 字符串长度为 10 位,就多了 1 位,就会崩溃。补充:字符串的在内存中比较特殊,字符串主要有三种存储形式:
- Tagged Pointer对象,就像上面介绍的那样;
- 存储在常量区,比如
aString = @"ureryueyr"
等,这种在编译期就能确定的就存在常量区,不存在引用计数管理的问题; - 普通的字符串对象,通过
stringWithFormat
创建较长的字符串,比如上面的 demo2,此时跟其他 OC 对象没有区别。
![](https://img.haomeiwen.com/i3704739/50b546b3fa6f75c0.png)
-
__NSCFConstantString
显然是常量字符串,自然就是存储在常量区。 -
__NSCFString
表示为 oc 对象,NSString
就是封装的CFString
字符串对象存储在堆中。 -
NSTaggedPointerString
这个类表示这是字符串的一种指针Tagged Pointer
分析:
首先因为属性 aString 是用 strong
修饰的,demo2 崩溃的原因是因为 set
方法里进行了 retain
和 release
操作,在 release
操作的时候,由于多线程的原因,有可能变量刚 release
,又被其他线程 release
导致过度释放的问题。但是 demo1 为什么就不会崩溃的?原因在于 demo1 中的对象是个 Tagged Pointer对象。
2.内存管理
它们内存管理是没有通过引用计数来管理,自然就会出现像普通对象的过度释放的崩溃信息:
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (obj->isTaggedPointerOrNil()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}
__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
if (obj->isTaggedPointerOrNil()) return obj;
return obj->autorelease();
}
通过源码可以看到,如果是 Tagged Pointer 对象,就不会进行 retain
和 release
操作。
3.
在苹果的 64 位 OC 实现中,若对象指针的二进制第一位是1,则该指针为 Tagged Pointer。
例如 0xa000000000000311 其中 a 的二进制为 1010,第一位 1 表示这是 Tagged Pointer,010 表示这是一个 NSTaggedPointerString
类;这个地址最后一位表示字符串的数目,这里是 0001 表示有 1 位字符串;其中真正用来存储的位数只有中间的 14 位 16 进制。这个地址本身其实就存储了字符串的值,可以说是存储在 &str
内存中值,只是伪装成了地址,它不需要存储在数据区,也不需要申请堆空间。
NSTaggedPointerString
的存储有三种编码方式:ASCII码,六位编码,五位编码。
(1)ASCII码
NSTaggedPointerString
存储内容除去第一位和最后一位,其实只有中间的 14 位 16 进制字符,再看 ascll 码由 8 位二进制组成,所以这里 (14*4) / 8=7,用 8 位的 ascll 码的话最多可以存储 7 个字符。字符串数目 0~7 之间
[NSString stringWithFormat:@"1"]
输出的地址 0xa000000000000311,其中 31 的二进制是 0011 0001,在 ascll 码表里查找发现正是对应着“1”;
[NSString stringWithUTF8String:"abcdabc"]
这里输出的地址是0xa636261646362617 可以发现在使用 ascll 编码时,字符串对应的编码是从右向左存储的。
(2)六位编码:
NSTaggedPointerString
采用六位二进制编码,(14*4)/6=9.333…,可以看出最多存储 9 位字符。字符数目 8~9。
(3)五位编码:
采用五位二进制编码,(14*4)/5 = 11.2,可以看出这种编码最多存储 11 位字符。字符数目在 10~11。
NSTaggedPointerString
存储编码中的六位和五位编码都是根据通常代码中字母使用频率来排序的,但并不是一成不变的,Apple 会持续更新并统计字母使用频率,系统每次升级都可能不一样,当前第一位是字母 e,之后是 i,l,o,t…;这两种编码是从左向右的;根据编码位数我们显然也能推测出并不是所有字符都可以进行 ascll 或者六位五位编码的,当出现这样不能编码的时候,系统也就不会使用 NSTaggedPointerString
类。