该篇博客主要讨论在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对应的实例。
总结
- 当使用字符串常量生成NSString对象,例如字面量、+ stringWithString:、- initWithString:方法时,生成的NSString对象为__NSCFConstantString类型,且计数为整数最大值,并一直存在于内存中。
- 当使用格式化字符且字符中包含非ASCII字符生成NSString对象,例如+ stringWithFormat:、- initWithFormat:时。生成的NSString为__NSCFString类型,且遵循引用计数规则。
当使用格式化字符且只包含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类型,且遵循引用计数规则。