iOS中对于NSString变量引用计数的分析

这篇博客深入探讨了Objective-C中NSString变量的引用计数,涉及字面量初始化、便捷初始化如stringWithString:和stringWithFormat:,以及init方法。分析了不同初始化方式下,对象类型如__NSCFConstantString、NSTaggedPointerString和__NSCFString的引用计数行为,特别是对NSTaggedPointerString的特性和长度限制进行了详细说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

该篇博客主要讨论在Objective-C中对于NSString变量的引用计数的不同处理。

为了在测试中打印方便,我们可以简单的定义一个宏:

#define MDMLog(str) ({NSString *name = @#str; NSLog(@"%@-->%@ %p, %zd", name, [str class], str, [str retainCount]);})

以下内容的测试环境为:Xcode9.4,SDK:iOS11.4,MRC

要想获取一个NSString对象,需要进行初始化NSString,所以我们就从NSString的初始化方法入手,进行NSString变量引用计数的分析。

字面量初始化

在对NSString初始化的方式中,字面量初始化是最直接的方法,我们来对字面量初始化的NSString对象进行引用计数查看:

NSString *str1 = @"123456789";
MDMLog(str1);

此时的打印结果为:

str1-->__NSCFConstantString 0x10beab060, -1

我们可以看到,此时生成的对象是NSString类簇中的__NSCFConstantString类的对象,同时由对象所处位置可以看出,此时生成的对象位于程序的数据区域,为可以一直存在的对象,所以该对象的计数为-1(即整数最大值),至于为何要在数据区域保存字符串常量,猜测是因为为了避免使用相同字符串常量造成的多次分配内存。该猜测可以由以下代码证实:

NSString *str1 = @"123456789";
MDMLog(str1);

NSString *str2 = @"123456789";
MDMLog(str2);

NSString *str3 = @"123456789";
MDMLog(str3);

打印结果如下:

str1-->__NSCFConstantString 0x107075060, -1
str2-->__NSCFConstantString 0x107075060, -1
str3-->__NSCFConstantString 0x107075060, -1

由此可见,在使用字符串变量进行初始化的NSString变量,它会指向数据区域的一块地址,并且该地址的计数为整数最大值。

便捷初始化

NSString的便捷初始化方法分为以下两种:

+ stringWithString:

该方法也是使用一个字符串常量来进行初始化的,按照字面量初始化的设计思路,该处生成的对象也应该是__NSCFConstantString类型的对象,我们来验证一下:

NSString *str1 = [NSString stringWithString:@"123456789"];
MDMLog(str1);

NSString *str2 = @"123456789";
MDMLog(str2);

NSString *str3 = @"123";
MDMLog(str3);

NSString *str4 = [NSString stringWithString:@"123"];
MDMLog(str4);

打印结果如下:

str1-->__NSCFConstantString 0x10b091060, -1
str2-->__NSCFConstantString 0x10b091060, -1
str3-->__NSCFConstantString 0x10b0910e0, -1
str4-->__NSCFConstantString 0x10b0910e0, -1

由上述测试可见,+ stringWithString:方法创建的对象与字面量创建的对象一样,是存放在数据区的计数为整数最大值的对象。

+ stringWithFormat:

该方法为使用格式化字符串来创建字符串对象的方法,对于该方法生成的对象是什么类型,我们可以测试一下:

NSString *str1 = [NSString stringWithFormat:@"123"];
MDMLog(str1);

结果为:

str1-->NSTaggedPointerString 0xa000000003332313, -1

生成的对象引用计数为整数最大值,但是它所属的对象为NSTaggedPointerString,同时所处位置也不是数据区域。

对于NSTaggedPointerString,我们稍后再谈论它。

init初始化方法

NSString的init初始化方法分为以下两种:

- initWithString:

该方法同样是使用一个字符串常量来进行初始化的,按照字面量初始化以及+ stringWithString:方法的设计思路,该处生成的对象应该也是__NSCFConstantString类型的对象,我们来验证以下:

NSString *str1 = [[NSString alloc] initWithString:@"123"];
MDMLog(str1);

NSString *str2 = @"123";
MDMLog(str2);

NSString *str3 = @"1234";
MDMLog(str3);

NSString *str4 = [[NSString alloc] initWithString:@"1234"];
MDMLog(str4);

打印结果如下:

str1-->__NSCFConstantString 0x105f49060, -1
str2-->__NSCFConstantString 0x105f49060, -1
str3-->__NSCFConstantString 0x105f490e0, -1
str4-->__NSCFConstantString 0x105f490e0, -1

由上述测试可见,- initWithString:方法创建的对象与字面量创建的对象、+ stringWithString:创建的对象一样,是存放在数据区的计数为整数最大值的对象。

- initWithFormat:

由于在Objective-C中,便捷初始化方法一般都会调用到对应init方法中,所以对于该方法所产生的对象,我们可以根据+ stringWithString:方法生成的对象进行猜测:- initWithFormat:方法生成的对象,应当与+ stringWithString:方法一样,为NSTaggedPointerString类型的引用计数为整数最大值的对象。

我们来验证一下:

NSString *str1 = [[NSString alloc] initWithFormat:@"123"];
MDMLog(str1);

输出为:

str1-->NSTaggedPointerString 0xa000000003332313, -1

可见我们的猜想是正确的。

NSTaggedPointerString

到此为止,我们无法解释的东西就是NSTaggedPointerString类了,关于该类,有一篇研究很深的博文:Tagged Pointer Strings

该博文的大致内容可以归为以下几点:

  • Tagged Pointer利用了因为使用内存对齐导致的某些高位一直为0的情况。
  • Tagged Pointer的设计实现大大减少了不必要的字符串的创建。
  • Tagged Pointer对于字符串的处理,仅局限于字符串中只包含ASCII字符的字符串。
  • Tagged Pointer采用4位表示类型,4位表示字符串长度,56位表示只含有ASCII字符的字符串内容。
  • Tagged Pointer在表示0-7位只包含ASCII字符的字符串内容时,直接使用字符的ASCII码进行保存。
  • Tagged Pointer在表示8-9位只包含ASCII字符的字符串内容时,使用6位编码对应于编码表eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX进行编码,如果存在无法编码的字符,那么生成NSString对应的真实类。
  • Tagged Pointer在表示10-11位只包含ASCII字符的字符串内容时,使用5位编码对应于编码表eilotrm.apdnsIc ufkMShjTRxgC4013进行编码,如果存在无法编码的字符,那么生成NSString对应的真实类。
  • 在表示11位以上只包含ASCII字符的字符串内容时,由于此时在使用5位编码的情况下,56位已经不能满足位数要求。同时如果采用4位编码,则可以表示编码的字符就很少,所以这种情况就生成NSString对应的真实类。

针对NSTaggedPointerString的实现思路,我们进行一些测试:

包含ASCII码之外的字符

NSString *str1 = [[NSString alloc] initWithFormat:@"马"];
MDMLog(str1);

输出为:

str1-->__NSCFString 0x60400003f4c0, 1

由此可见,此时生成的对象为NSString对应的真实的对象,且引用计数开始遵循Objective-C中引用计数规则。

只包含ASCII码中的字符

字符串长度在0-7之间

NSString *str1 = [[NSString alloc] initWithFormat:@"abc"];
MDMLog(str1);

NSString *str2 = [[NSString alloc] initWithFormat:@"abcdefg"];
MDMLog(str2);

NSString *str3 = [[NSString alloc] initWithFormat:@"1234567"];
MDMLog(str3);

输出为:

str1-->NSTaggedPointerString 0xa000000006362613, -1
str2-->NSTaggedPointerString 0xa676665646362617, -1
str3-->NSTaggedPointerString 0xa373635343332317, -1

由此可见,满足该情况的NSString变量均为NSTaggedPointerString类的实例,同时在对应的内存位置中,最高位a表示NSTaggedPointerString类型,最低位数字表示字符串长度,中间位数用来存储字符串内容。

字符串长度在8-9之间

NSString *str1 = [[NSString alloc] initWithFormat:@"abcdefgh"];
MDMLog(str1);

NSString *str2 = [[NSString alloc] initWithFormat:@"abcdefghi"];
MDMLog(str2);

NSString *str3 = [[NSString alloc] initWithFormat:@"abcdefghiq"];
MDMLog(str3);

输出为:

str1-->NSTaggedPointerString 0xa0022038a0116958, -1
str2-->NSTaggedPointerString 0xa0880e28045a5419, -1
str3-->__NSCFString 0x600000220860, 1

我们可以看到,str1与str2为NSTaggedPointerString类的实例,同时在对应的内存位置中,最高位a表示NSTaggedPointerString类型,最低位数字表示字符串长度,中间位数用来存储经过6位编码的字符串内容。

对于str3来说,由于字符串中含有不在编码表中的q字符,所以无法生成对应的NSTaggedPointer类,最终生成遵循Objective-C引用计数规则的真正__NSCFString对应的实例。

字符串长度在10-11之间

NSString *str1 = [[NSString alloc] initWithFormat:@"acdefghijk"];
MDMLog(str1);

NSString *str2 = [[NSString alloc] initWithFormat:@"acdefghijkl"];
MDMLog(str2);

NSString *str3 = [[NSString alloc] initWithFormat:@"acdefghijklb"];
MDMLog(str3);

输出为:

str1-->NSTaggedPointerString 0xa010e5023aa86d2a, -1
str2-->NSTaggedPointerString 0xa21ca047550da42b, -1
str3-->__NSCFString 0x60400023ae40, 1

str1与str2为NSTaggedPointerString类的实例,同时在对应的内存位置中,最高位a表示NSTaggedPointerString类型,最低位数字表示字符串长度,中间位数用来存储经过5位编码的字符串内容。

对于str3来说,由于字符串中含有不在编码表中的b字符,所以无法生成对应的NSTaggedPointer类,最终生成遵循Objective-C引用计数规则的真正__NSCFString对应的实例。

字符串长度大于11

NSString *str1 = [[NSString alloc] initWithFormat:@"aaaaaaaaaaaa"];
MDMLog(str1);

输出为:

str1-->__NSCFString 0x60000042a340, 1

可见,当字符串长度大于11时,即使所含字符在5位编码表之内,由于56位不能满足位数要求,只能去生成遵循Objective-C引用计数的真正__NSCFString对应的实例。

总结

  1. 当使用字符串常量生成NSString对象,例如字面量、+ stringWithString:、- initWithString:方法时,生成的NSString对象为__NSCFConstantString类型,且计数为整数最大值,并一直存在于内存中。
  2. 当使用格式化字符且字符中包含非ASCII字符生成NSString对象,例如+ stringWithFormat:、- initWithFormat:时。生成的NSString为__NSCFString类型,且遵循引用计数规则。
  3. 当使用格式化字符且只包含ASCII字符生成NSString对象时:

    • 字符数在0-7之间,生成NSTaggedPointerString对象并计数为整数最大值且一直存在内存中。
    • 字符数在8-9时,字符全部在6位编码表中时,生成NSTaggedPointerString对象并计数为整数最大值且一直存在内存中。
    • 字符数在8-9时,字符存在不在6位编码表中时,生成的NSString为__NSCFString类型,且遵循引用计数规则。
    • 字符数在10-11时,字符全部在5位编码表中时,生成NSTaggedPointerString对象并计数为整数最大值且一直存在内存中。
    • 字符数在10-11时,字符存在不在5位编码表中时,生成的NSString为__NSCFString类型,且遵循引用计数规则。
    • 字符数大于11时,生成的NSString为__NSCFString类型,且遵循引用计数规则。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值