[iOS开发]NSString的三种类型管理方式

文章目录

  • 参考博客
  • 问题发现:
  • __NSCFConstantString
  • __NSCFString
  • NSTaggedPointerString
    • 标签指针的概念
  • NSString的深浅复制问题
  • 三种类型字符串的copy/mutableCopy/retainCount情况
  • 9.24内存分布补充
    • __NSCFConstantString
    • __NSCFString
    • NSTaggedPointerString

参考博客

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(长度比上面的短)

看一下结果
在这里插入图片描述
[iOS开发]NSString的三种类型管理方式_第1张图片

一、三直接赋值不管长短,类型相同,地址相邻,都为__NSCFConstantString类型
二为__NSCFString类型,四为NSTaggedPointerString类型
来了解一下NSString的这三种类型

__NSCFConstantString

Constant->常量

  • 通俗理解其就是常量字符串,是一种编译时常量
  • 这种对象存储在字符串常量区。
  • 通过打印起retainCount的值,发现很大,2^64 - 1,测试证明对其进行release操作时,retainCount不会产生任何变化创建之后便无法释放掉的对象。
  • 当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同,这也就证明了常量字符串是一种单例。
  • 这种对象一般通过字面值 @"…"、CFSTR("…") (一种宏定义创建字符串的方法)或者 stringWithString: 方法(现在会报警告⚠️这个方法等同于字面值创建的方法)。

__NSCFString

  • 和 __NSCFConstantString 不同, __NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。
  • 这种对象被存储在堆上。
  • 通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。
  • 如果字符串长度大于9或者如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 __NSCFString 类型

NSTaggedPointerString


标签指针的概念

理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。

简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 字节足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。


  • 从其的引用计数可以看出,这也是一个释放不掉的单例常量对象。当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同。在运行时根据实际情况创建。
  • 对于 NSString 对象来讲,当非字面值常量的数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型。(小于等于9这个数据也是原博主进行猜测,经过测试在字符串含有q的时候是小于等于7)

NSString的深浅复制问题

想看看__NSCFString和NSTaggedPointerString的copy和mutableCopy的结果是否一致
在这里插入图片描述
__NSCFString的字符串和我们之前理解的一样
[iOS开发]NSString的三种类型管理方式_第2张图片

NSTaggedPointerString也一样

[iOS开发]NSString的三种类型管理方式_第3张图片
常量字符串也和我们之前的理解一样

三种类型字符串的copy/mutableCopy/retainCount情况

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

用lldb进行调试可以看到
[iOS开发]NSString的三种类型管理方式_第4张图片
经过测试

总结就是

无论原来的三个的类型是NSString还是NSMutableString类型

copy 会使原来的对象引用计数加一(当然仅有正常类型的字符串,而不是单例创建的,毕竟那两个引用计数是无限的),并拷贝对象地址给新的指针,所以类型与原类型一致。
mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.

9.24内存分布补充

了解一下内存的分布

__NSCFConstantString

NSString *str = @"1111111";
NSLog(@"%p",str);

输出结果
0x100004058

[iOS开发]NSString的三种类型管理方式_第5张图片

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看一下
[iOS开发]NSString的三种类型管理方式_第6张图片
很完美,31对应就是1的ASCII码。

25~32字节07 00 00 00 00 00 00 00存放的是字符串的长度。

所以对于ConstantString,我们想查看内存分布情况,直接打印str得到的其实是str这个指针的地址信息,前8位是isa指针,17到24位是对应常量字符串的地址,25~32位是字符串的长度。

__NSCFString

与__NSCFConstantString的存储常量的地址不同
[iOS开发]NSString的三种类型管理方式_第7张图片

__NSCFString直接将对应字符串的ASCII码存储在之前17~24字节存储对应字符串地址的地方,而不是通过再存一个地址来进行存储。

所以对于常量字符串的单例来说,仅仅存储地址,哪怕后面再创建新的字符串,但是只要内容相同,str对象里面存储的该字符串的地址都是一样的。而对于CFString来说,每个对象都是新的,每个对象都是由自己内部的地址来直接存储,省略了再次通过地址获取内容的步骤。大家哪怕内容相同,自己也是自己的。

其实存在于堆中的

NSTaggedPointerString

打印一个TaggedPointerString类型的字符串

0xd6e611f9069c4ec5为什么这么奇怪?
[iOS开发]NSString的三种类型管理方式_第8张图片

所以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));

[iOS开发]NSString的三种类型管理方式_第9张图片
不知道最后一位5的意义是什么,但是前面的61、62、63、64不就是对应a\b\c\d的16进制编码吗。5前面的那一位是字符串的个数,底层也可能是通过后面的这个个数的记录来判断是否过长,过长会依然会采用对象的形式保存。

同时我们也发现Tagged Pointer没有isa指针,它不是一个对象,只是一个伪装成对象的普通变量而已。

所以总体来说:
Tagged Pointer是一个特殊的指针,不指向任何实质地址。使用编码的方式产生一个假地址,在需要时,通过解码方式得到其内部存储的数据。TaggedPointer极大的提高了内存利用率和简化了查询步骤。它不单单是一个指针,还包括了其值+某些具体信息(比如个数等等),节省了对象的查询流程。

你可能感兴趣的:(ios,objective-c)