由NSString什么时候释放说起

起因

最开始只是想试一试写在方法内部的局部变量释放时经不经过autoreleasepool。
例如,下图这样的代码。
nsobject 实验
为了不影响对象本身的引用计数影响它的销毁过程,使用一个weak指针,不出所料的,打印出来了如下结果

1
2
3
2017-03-01 18:25:05.117 Hello[72930:2179247] <NSObject: 0x61000000a6e0>
2017-03-01 18:25:05.118 Hello[72930:2179247] (null)
2017-03-01 18:25:05.150 Hello[72930:2179247] (null)

但是这个实验如果换成NSString得到的则是完全不一样的结果。
如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"123456";
reference = str;
NSLog(@"%@", reference);
// str是一个autorelease对象,设置一个weak的引用来观察它
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference);
}

打印出来的结果却是:

1
2
3
2017-03-02 10:31:41.475 Hello[75799:2289076] 123456
2017-03-02 10:31:41.475 Hello[75799:2289076] 123456
2017-03-02 10:31:41.492 Hello[75799:2289076] 123456

这个看上去也很好似乎也很好理解,NSString初始化的时候是存放在常量区的,所以没有释放嘛。

深入研究

为了观察对象的释放过程,我们在str赋值的地方加一个断点
break point
走到该断点的时候通过lldb命令watchpoint set variable str来观察,可以看到str由0x0000000000000000变成0x00000001056b3250。
赋值过程
然后一路点击Continue program execution,发现str会变成0x0000000000000000,控制台只打印了一次str的值,也就是说viewwillappear还没有执行,这点跟雷大博客(Objective-C Autorelease Pool 的实现原理)中的略不一样,我猜是apple改了。
释放1
然后看左侧的方法调用栈,会发现这个过程经过了objc_store,AutoreleasePoolPage::pop(void *)等函数通过autoreleasepool释放了。现在修改一下log语句。

1
#define Log(_var) ({ NSString *name = @#_var; NSLog(@"%@: %@ -> %p : %@ ", name, [_var class], _var, _var); })

看了几个大神之前的博客,大都还打印了retainCount,但是今天这里研究的是arc,就不打印retainCount了。
执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];

NSString *str = @"123456789";
NSString *bstr = [NSString stringWithFormat:@"123456789"];
NSString *cstr = [str mutableCopy];
NSNumber *xnum = [NSNumber numberWithInteger:8];
references = cstr;
self.num = xnum;
Log(str);
Log(bstr);
Log(cstr);
}

得到打印结果:

1
2
3
2017-03-02 14:25:33.478 Hello[78655:2389611] str: __NSCFConstantString -> 0x10b52c308 : 123456789 
2017-03-02 14:25:33.478 Hello[78655:2389611] bstr: NSTaggedPointerString -> 0xa1ea1f72bb30ab19 : 123456789
2017-03-02 14:25:33.478 Hello[78655:2389611] cstr: __NSCFString -> 0x608000074d00 : 123456789

可以看到这里其实是有三种String的,而references指向了cstr,此时在viewWillAppear和viewDidAppear里打印references得到的则是null。

三种String

  • NSCFConstantString: 字符串常量,放在常量区,对其retain或者release不影响它的引用计数,程序结束后释放。用字面量语法创建出来的string就是这种,所以在出了viewDidLoad方法以后在其他地方也能打印出值,压根就没释放。
1
2
Tip:
NSString *str = @“xxx”;其实创建了两个对象,指针str也是一个对象储存在栈内存中,由系统负责释放
  • NSTaggedPointerString: Tagged Point,标签指针,苹果在64位环境下对NSString和NSNumber做的一些优化,简单来说就是把对象的内容存放在了指针里,这样就不需要在堆内存里在开辟一块空间存放对象了,一般用来优化长度较小的内容。关于标签指针的内容可以参考唐巧的介绍:深入理解Tagged Pointer

    对于NSString,当非字面量的数字,英文字母字符串的长度小于等于9的时候会自动成为NSTaggedPointerString类型。代码中的bstr如果再加一位或者有中文在里面就是变成NSCFString。而NSTaggedPointerString也是不会释放的,它的内容就在本身的指针里,又没有对象释放个啥啊。所以如果把references的赋值代码改为

    1
    references = bstr;

    在viewWillAppear和viewDidAppear中也是能打印出值来的。

  • NSCFString: 这种string就和普通的对象很像了,储存在堆上,有正常的引用计数,需要程序员分配释放。所以references = cstr时,会打印出null,cstr出了方法作用域在runloop结束时就被autoreleasepool释放了。

stringWithFormat

到这里还是有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];

NSString *str = @"12345678900";
NSString *bstr = [NSString stringWithFormat:@"12345678900"];
NSString *cstr = [str mutableCopy];
references = bstr; //1
// references = cstr;//2
Log(str);
Log(bstr);
Log(cstr);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", references);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%p", references);
}

根据以上说法,当string超过10位数时,bstrcstr都是NSCFString,可是两种情况viewWillAppear和viewDidAppear在打印的结果不一样。
bstr:

1
2
2017-03-02 17:27:12.637 Hello[81349:2493571] 12345678900
2017-03-02 17:27:13.187 Hello[81349:2493571] 0x0

cstr:

1
2
2017-03-02 17:28:40.768 Hello[81398:2494728] (null)
2017-03-02 17:28:41.311 Hello[81398:2494728] 0x0

根据太阳神黑幕背后的Autorelease中的说法,是因为viewWillAppear和viewDidLoad在一个runloop中导致bstr在willappear中能打印出来值。而cstr由于来自于mutablecopy方法,被自己持有,并不会加入到autoreleasepool当中,文章最开始用NSObject做实验时也是这个道理。

所以,stringWithFormat这个工厂方法在返回值时会先把对象加入到最近的autoreleasepool当中。
查资料得知以 alloc, copy, init,mutableCopynew这些方法打头的方法,返回的都是 retained return value,例如[[NSString alloc] initWithFormat:],而其他的则是unretained return value,例如 [NSString stringWithFormat:]。对于前者调用者是要负责释放的,对于后者就不需要了。而且对于后者ARC会把对象的生命周期延长,这里文档中没说延长方式,我猜加入自动释放池是手段之一,确保调用者能拿到并且使用这个返回值,但是并不一定会使用 autorelease,在worst case 的情况下才可能会使用,因此调用者不能假设返回值真的就在 autorelease pool中,有的时候为了优化会直接拿到返回值,狮子头一书中有讲。从性能的角度,这种做法也是可以理解的。如果我们能够知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接使用 release 就可以。如果很多对象都使用 autorelease 的话,也会导致整个 pool 在 drain 的时候性能下降。

1
2
3
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.

也就是说通过工程方法得到的string生命周期被延长了,所以才会在viewWillAppear里依然可以打印出来。为了证实这一点,我们换成array来做个实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *object = [NSObject new];
NSArray *arr = @[object]; //1
//NSArray *arr = [NSArray arrayWithObjects:object, nil]; //2
references = arr;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", references);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%p", references);
}

第一种情况通过字面量创建array,打印台输出:

1
2
2017-03-02 17:48:19.667 Hello[81612:2505809] (null)
2017-03-02 17:48:20.216 Hello[81612:2505809] 0x0

第二种情况通过工厂方法创建,打印台输出:

1
2
3
4
2017-03-02 17:50:04.964 Hello[81664:2507484] (
"<NSObject: 0x618000200480>"
)
2017-03-02 17:50:05.508 Hello[81664:2507484] 0x0

可以看到通过工厂方法创建的array生命周期确实被延长了。

总结

1.NSCFString跟普通对象一样是可以释放的
2.NSString和NSArray的工厂方法可以延长对象的生命周期,但不一定是通过autorelease,有可能优化(同理,NSDictionary也是一样的,有兴趣的可以试一下)
3.NSArray的释放做了很多优化,释放时机是个谜,有空总结下。

##参考资料
黑幕背后的Autorelease
Objective-C Autorelease Pool 的实现原理
Objective-C 内存管理——你需要知道的一切