原文链接:https://blog.csdn.net/qq_27909209/article/details/82150856
本文链接:https://www.jianshu.com/p/627815e1996b
一:__NSCFConstantString __NSCFString NSTaggedPointerString
二:weak修饰,字符串内存管理
三:NSTaggedPointerString讲解
四:面试题
五:__NSCFString:Toll-free bridgin桥接机制
OC中的NSString不论是在编译时还是在运行时都做了很多的优化,并不同于普通的对象,它是一个非常复杂的存在。
一:__NSCFConstantString __NSCFString NSTaggedPointerString
这个证明需要再mrc环境下。
我们需要研究的NSString创建出来的实际的类型有:
__NSCFConstantString
__NSCFString
NSTaggedPointerString
那这三种类型分别在什么时候创建出来,又是什么意思呢?先看定义产生这三种类型分别是什么创建方式下产生的。看代码
//宏定义
#define HXLog(_var) ({ NSString *name = @#_var; NSLog(@"变量名=%@,类型=%@, 地址=%p,引用计数=%d,值=%@", name, [_var class], _var,(int)[_var retainCount], _var); })
NSString *a0 = [[NSString alloc] init];
NSString *a1 = @"12345678910";
NSString *a2 = [NSString stringWithString:@"12345678910"];
NSString *a3 = [[NSString alloc] initWithString:@"12345678910"];
NSString *a4 = [[NSString alloc] initWithString:a0];
NSString *a5 = [[NSString alloc] initWithFormat:@"1123456789"];
NSString *a6 = [NSString stringWithFormat:@"123456789"];
NSString *a7 = [NSString stringWithFormat:a5];
HXLog(a0);
HXLog(a1);
HXLog(a2);
HXLog(a3);
HXLog(a4);
HXLog(a5);
HXLog(a6);
HXLog(a7);
可以看出来,a0,a1,a2, a3, a4类型都为__NSCFConstantString类型,引用计数值为-1。
a5和a7都为__NSCFString类型,引用计数值为1。
a6为NSTaggedPointerString类型,引用计数值为-1。
小结:
创建的字符串有三种类型:造成这种情况是由于 OC 对 NSString 的内存优化产生的。
__NSCFConstantString
从字面就可以看出,这是一个常量字符串,该类型的字符串是以字面量创建的,是在编译期创建的,保存在常量区。通过a0,a1,a2, a3, a4的打印结果看出,当创建的字符串变量值在常量区存在时,变量会指向那个字符串,这是编译期做的优化。
对于 initWithString 实例方法以及 stringWithString 类方法,编译器会给出redundant警告,原因是该方法创建字符串等同于直接复制字符串字面量
那 retainCount为-1是什么情况
首先retainCount是NSUInteger的类型,其实上面的打印是将它作为int类型打印。所以它其实不是-1,它的实际值是4294967295。在objc的retainCount中.如果对象的retainCount为这个值,就意味着“无限的retainCount”,这个对象是不能被释放的。
所有的 __NSCFConstantString对象的retainCount都为-1,这就意味着 __NSCFConstantString不会被释放,使用第一种方法创建的NSString,如果值一样,无论写多少遍,都是同一个对象。而且这种对象可以直接用 == 来比较,
文字常量区存放常量字符串,程序结束后由系统释放,也就是说指向常量表的指针不受引用计数管理。所以对于NSCFConstantString类型的变量,OC 的内存管理策略对其无效。
__NSCFString (>=10位是__NSCFString类型)
表示这是一个对象类型的字符串,在运行时创建,存储在堆区,服从OC 的对象内存管理策略。该类型的字符串由 Format创建,无论是实例方法还是类方法且其长度不能太小(内容若包含中文字符,不论长度大小,都是NSCFString),否则创建的是NSTaggedPointerString类型,例如上例的变量 a5 与 a6。
NSTaggedPointerString (从上面也可以看出来,0-9位是taggedpointer类型)
对于64位程序,为了节省内存和提高运行速度,苹果引入了 Tagged Point 技术。
NSTaggedPointerString是对NSCFString优化后的存在,在运行时创建时对字符串的内容和长度做出判断,若字符串内容是由ASCII字符构成且长度较小(大概十个字符以内),这时候创建的字符串就是NSTaggedPointerString类型,字符串直接存储在指针里,引用计数同样为-1,不适用对象的内存管理策略。
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,OC 对象的内存管理方式对其无效。
对上述a0-a7进行copy,所得到的类型还都是原来的类型,并不会改变,并且地址都不会改变,因为原来就是一个不可变的,因为copy的还是不可变的,所以就没有开辟新的对象。
但是进行mutablecopy,会发现
__NSCFConstantString->__NSCFString; NSTaggedPointerString->__NSCFString; __NSCFString 依旧是__NSCFString类型
同时理解了上面三个类型,也就知道了一些关于string的内存管理的些许知识,比方说,看下面,用weak来修饰,看释放的时间。
二:weak修饰,字符串内存管理
在arc环境下,
__weak id weaka1 = nil;
__weak id weaka2 = nil;
__weak id weaka3 = nil;
__weak id weaka4 = nil;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"viewWillAppear:\n weaka1:%@ \n weaka2:%@ \n weaka3:%@ \n weaka4:%@",[weaka1 class],[weaka2 class],[weaka3 class], [weaka4 class]);
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
NSLog(@"viewWillDisappear:\n weaka1:%@ \n weaka2:%@ \n weaka3:%@\n weaka4:%@",[weaka1 class],[weaka2 class],[weaka3 class], [weaka4 class]);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSString *a1 = @"string";
NSString *a2 = [NSString stringWithFormat:@"stirng"];
NSString *a3 = [NSString stringWithFormat:@"stirng strings"];
NSString *a4 = [a1 mutableCopy];
weaka1 = a1;
weaka2 = a2;
weaka3 = a3;
weaka4 = a4;
}
打印结果如下:
2018-09-03 15:03:42.779454+0800 newstestt[5026:514384] viewWillAppear:
weaka1:__NSCFConstantString
weaka2:NSTaggedPointerString
weaka3:__NSCFString
weaka4:(null)
2018-09-03 15:03:44.432565+0800 newstestt[5026:514384] viewWillDisappear:
weaka1:__NSCFConstantString
weaka2:NSTaggedPointerString
weaka3:(null)
weaka4:(null)
这个结果说明:
a1是__NSCFConstantString,字符串常量,放在常量区,对其retain或者release不影响它的引用计数,程序结束后释放。用字面量语法创建出来的string就是这种,所以在出了viewDidLoad方法以后在其他地方也能打印出值,根本没有释放。不由我们控制
a2是NSTaggedPointerString,Tagged Point,标签指针,苹果在64位环境下对NSString和NSNumber做的一些优化,简单来说就是把对象的内容存放在了指针里,这样就不需要在堆内存里在开辟一块空间存放对象了,一般用来优化长度较小的内容。这个根本不是对象,所以也不受引用计数的计算。所以也无所谓释放和不释放之说。
a3 和a4 都是__NSCFString 类型, 这种string就和普通的对象很像了,储存在堆上,有正常的引用计数,需要程序员分配释放。所以weaka3 = a3时,会打印出null,cstr出了方法作用域在runloop结束时就被autoreleasepool释放了。只是这里有一点需要说明,就是系统创建的stringWithFormat类型和我们创建的以 alloc, copy, init,mutableCopy和new这些方法打头的方法,返回的都是 retained return value,例如[[NSString alloc] initWithFormat:],而其他的则是unretained return value,例如 [NSString stringWithFormat:]。对于前者调用者是要负责释放的,对于后者(系统穿件的那些)就不需要了。而且对于后者ARC会把对象的生命周期延长,确保调用者能拿到并且使用这个返回值,但是并不一定会使用 autorelease,在worst case 的情况下才可能会使用,因此调用者不能假设返回值真的就在 autorelease pool中。从性能的角度,这种做法也是可以理解的。如果我们能够知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接使用 release 就可以。如果很多对象都使用 autorelease 的话,也会导致整个 pool 在 drain 的时候性能下降。
所以可以看到,a3刚开始没多久就释放了,但是a4过了好一会才释放。当然这个在mrc下,自己释放a3即可,因为是自己创建的。也会得到同样的效果。
三:NSTaggedPointerString讲解
为什么字面量常量苹果不使用NSTaggedPointerString呢?
【译】采用Tagged Pointer的字符串中文版的 翻译有些晦涩,看了下英文版的描述比较易懂些:
although a string like @"a" could be stored as a tagged pointer, constant strings are never tagged pointers. Constant strings must remain binary compatible across OS releases, but the internal details of tagged pointers are not guaranteed.
原因是常量字符串需要在跨系统上保持二进制兼容,而 tagged pointers在技术上并不能保证这个。因此对于这种短的字符串字面量还是使用\ __NSCFConstantString类型。
下面一个问题,tagged pointers在内存上分配在哪个区?
其实如果我们仔细在XCode中多点两下,就可以看到其实tagged pointers是没有isa指针的,说明它根本不是一个对象。究其原因这个要说到tagged pointers是为什么被创造出来。
一般来说,对象所占内存是和CPU位数相关的。在32位的时候,比如一个NSNumber对象占用的空间是4(对象指针)+4(对象的值)=8字节,升级到64位的时候,逻辑不变的话,占用的空间直接翻倍,变成8+8=16字节,这样会产生十分严重的效率问题:为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
在查找资料的过程中也发现了苹果官方的明确说法(摘自深入理解Tagged Pointer):
我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
在内存读取上有着3倍的效率,创建时比以前快106倍。
由此看来,NSTaggedPointerString根本不是对象,是分配在栈区的
四:面试题
4.1:写一个NSString类的实现
+ (id)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding;
实现如下:
+ (id) stringWithCString: (const char*)nullTerminatedCString encoding:(NSStringEncoding)encoding{
NSString *obj;
obj = [self allocWithZone: NSDefaultMallocZone()];
obj = [obj initWithCString: nullTerminatedCString encoding: encoding];
return AUTORELEASE(obj);
}
4.2:判断两个NSString的字面量是否相同,为什么要用isEqualToString来判断,而不能用==来判断呢?
关于字符串有非常多的问题,比方说:判断两个NSString的字面量是否相同,为什么要用isEqualToString来判断,而不能用==来判断呢? 可能大多数人会回答:因为==判断的是两个指针是否相等,而NSString是分配到堆上的,每次创建的时候,指针指向的地址的不同的,所以不能用==来判断。但是这样的回答不完整。这个题感觉有点毛病,字面量本身就只有一种方式,@""; 直接创建等于赋值,这种方式创建的类型都是__NSCFConstantString,本身也不会有引用计数,所以它就是一个对象,这个是可以用==来判断的,我想题的意思应该是NSString创建的字符串,是否相等,要用isEqualToString来判断,因为字符串的创建方式不同,类型不同,地址不同,单纯从==来判断的话,不准确。
深入理解Tagged Pointer、字符串深度剖析
五:__NSCFString:Toll-free bridgin桥接机制
Toll-free bridging,简称为TFB,是一种允许某些ObjC类与其对应的CoreFoundation类(Core Foundation框架 (CoreFoundation.framework) 是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能)之间可以互换使用的机制。比如 NSString与CFString是桥接(bridged)的, 这意味着可以将任意NSString当做CFString使用,也可以将任意的CFString当做NSString使用。
官网也有相关描述:
There are a number of data types in the Core Foundation framework and the Foundation framework that can be used interchangeably. This capability, called toll-free bridging, means that you can use the same data type as the parameter to a Core Foundation function call or as the receiver of an Objective-C message。
原理(拿NSString举例)大概是:NSString是一个抽象类,每当你创建一个NSString实例,实际上是创建的NSString的一个私有子类实例。其中一个私有子类就是NSCFString,其是CFString类的在ObjC中的对应类。NSCFString实现了作为NSString需要的所有方法。
我的理解:总之,你知道有Toll-Free Bridging桥接机制,然后NSCFString是NSString的私有子类,实现了它的所有方法。详细解释看官网。
而为什么要有CFString呢?
官网解释
CFString provides a suite of efficient string-manipulation and string-conversion functions. It offers seamless Unicode support and facilitates the sharing of data between Cocoa and C-based programs
Toll-Free Bridging的桥接机制、 Toll Free Bridging的内部原理