NSString的内存管理
iOS里的TaggedPointer[NSString篇]
NSString *firstString = @"helloworld";
NSString *secondString = [NSString stringWithFormat:@"helloworld"];
NSString *thirdString = @"hello";
NSString *fourthSting = [NSString stringWithFormat:@"hello"];
NSLog(@"%p %@\n%p %@\n%p %@\n%p %@\n",firstString,[firstString class],secondString,[secondString class],thirdString,[thirdString class],fourthSting,[fourthSting class]);
创建四个字符串
一、三直接赋值,二、四通过stringWithFormat进行赋值
一、二赋值helloworld
三、四赋值hello(长度比上面的短)
一、三直接赋值不管长短,类型相同,地址相邻,都为__NSCFConstantString类型
二为__NSCFString类型,四为NSTaggedPointerString类型
来了解一下NSString的这三种类型
Constant->常量
理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。
简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 字节足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。
想看看__NSCFString和NSTaggedPointerString的copy和mutableCopy的结果是否一致
__NSCFString的字符串和我们之前理解的一样
NSTaggedPointerString也一样
int main(int argc, const char * argv[]) {
NSString *firstString = @"你好";
NSString *secondString = [NSString stringWithFormat:@"hello"];
NSString *thirdString = [NSString stringWithFormat:@"helloWorld"];
NSString *test1 = [firstString copy];
NSString *test2 = [firstString mutableCopy];
NSString *test3 = [secondString copy];
NSString *test4 = [secondString mutableCopy];
NSString *test5 = [thirdString copy];
NSString *test6 = [thirdString mutableCopy];
return 0;
}
总结就是
无论原来的三个的类型是NSString还是NSMutableString类型
copy 会使原来的对象引用计数加一(当然仅有正常类型的字符串,而不是单例创建的,毕竟那两个引用计数是无限的),并拷贝对象地址给新的指针,所以类型与原类型一致。
mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.
了解一下内存的分布
NSString *str = @"1111111";
NSLog(@"%p",str);
输出结果
0x100004058
0~8字节–>48 65 09 80 FF 7F 00 00
这个是是啥呢
OK,前8个字节是isa指针
9~16字节–>C8 07 00 00 00 00 00 00
不知道,测试发现每个都有
17~24字节–>A8 3F 00 00 01 00 00 00
这个是啥呢?
我们直接通过NSLog(@"%p","1111111");
发现地址为0x100003fa8
刚好对应上
我们去0x100003fa8
看一下
很完美,31对应就是1的ASCII码。
25~32字节07 00 00 00 00 00 00 00
存放的是字符串的长度。
所以对于ConstantString,我们想查看内存分布情况,直接打印str得到的其实是str这个指针的地址信息,前8位是isa指针,17到24位是对应常量字符串的地址,25~32位是字符串的长度。
与__NSCFConstantString的存储常量的地址不同
__NSCFString直接将对应字符串的ASCII码存储在之前17~24字节存储对应字符串地址的地方,而不是通过再存一个地址来进行存储。
所以对于常量字符串的单例来说,仅仅存储地址,哪怕后面再创建新的字符串,但是只要内容相同,str对象里面存储的该字符串的地址都是一样的。而对于CFString来说,每个对象都是新的,每个对象都是由自己内部的地址来直接存储,省略了再次通过地址获取内容的步骤。大家哪怕内容相同,自己也是自己的。
其实存在于堆中的
打印一个TaggedPointerString类型的字符串
所以taggedPointer是进行了一个编码的过程,在Mac10.14和iOS12之前,对value做异或操作的objc_debug_taggedpointer_obfuscator
值为0,之后为objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK
。
所以我们想得到解码,重新声明全局变量objc_debug_taggedpointer_obfuscator
和内联函数_objc_decodeTaggedPointer
就好了
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline uintptr_t _objc_decodeTaggedPointer (id ptr) {
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
void(^myBlock)(void);
int main() {
NSString * str1 = [NSString stringWithFormat:@"a"];
NSString * str2 = [NSString stringWithFormat:@"bb"];
NSString * str3 = [NSString stringWithFormat:@"ccc"];
NSString * str4 = [NSString stringWithFormat:@"dddd"];
NSLog(@"str1 = %@ - %p - 0x%lx",object_getClass(str1),str1,_objc_decodeTaggedPointer(str1));
NSLog(@"str2 = %@ - %p - 0x%lx",object_getClass(str2),str2,_objc_decodeTaggedPointer(str2));
NSLog(@"str3 = %@ - %p - 0x%lx",object_getClass(str3),str3,_objc_decodeTaggedPointer(str3));
NSLog(@"str4 = %@ - %p - 0x%lx",object_getClass(str4),str4,_objc_decodeTaggedPointer(str4));
不知道最后一位5的意义是什么,但是前面的61、62、63、64不就是对应a\b\c\d的16进制编码吗。5前面的那一位是字符串的个数,底层也可能是通过后面的这个个数的记录来判断是否过长,过长会依然会采用对象的形式保存。
同时我们也发现Tagged Pointer没有isa指针,它不是一个对象,只是一个伪装成对象的普通变量而已。
所以总体来说:
Tagged Pointer是一个特殊的指针,不指向任何实质地址。使用编码的方式产生一个假地址,在需要时,通过解码方式得到其内部存储的数据。TaggedPointer极大的提高了内存利用率和简化了查询步骤。它不单单是一个指针,还包括了其值+某些具体信息(比如个数等等),节省了对象的查询流程。