最近把一個游戲內嵌到app里,選用了微信開源的Mars,結果遇到了記憶體峰值。解決的方法很容易,加上@autoreleasepool就可以了。但是做實驗的時候又有了好多疑惑,不停地往深處挖,最終瞭解了autoreleasepool的實現,Tagged Pointer,和NSString記憶體管理的特殊性 ...
最近把一個游戲內嵌到app里,選用了微信開源的Mars,結果遇到了記憶體峰值。解決的方法很容易,加上@autoreleasepool就可以了。但是做實驗的時候又有了好多疑惑,不停地往深處挖,最終瞭解了autoreleasepool的實現,Tagged Pointer,和NSString記憶體管理的特殊性。
Mars
我們做的小游戲需要實時傳輸數據,數據很小,就選用了Mars。結果記憶體一直漲,在這裡加個autoreleasepool就可以避免記憶體峰值。
void StnCallBack::OnPush(int32_t _cmdid, const AutoBuffer& _msgpayload) {
if (_msgpayload.Length() > 0) {
@autoreleasepool {
NSData *recvData = [NSData dataWithBytes:(const void *)_msgpayload.Ptr() length:_msgpayload.Length()];
[[TRSocketManager sharedInstance] OnPushWithCmd:_cmdid data:[[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding]];
}
}
}
autoreleasepool
Objective-C Autorelease Pool的實現原理
這篇博客很不錯,詳細介紹了autoreleasepool的實現,圖文並茂,很好理解。不過他提的3個場景,答案現在已經不適用了。
__weak NSString *string_weak_ = nil;
- (void)viewDidLoad {
[super viewDidLoad];
// 場景 1
NSString *string = [NSString stringWithFormat:@"leichunfeng"];
string_weak_ = string;
// 場景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"leichunfeng"];
// string_weak_ = string;
// }
// 場景 3
// NSString *string = nil;
// @autoreleasepool {
// string = [NSString stringWithFormat:@"leichunfeng"];
// string_weak_ = string;
// }
NSLog(@"string: %@", string_weak_);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"string: %@", string_weak_);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"string: %@", string_weak_);
}
結果令人大跌眼鏡,來看看輸出吧:
//3個場景全部都是這個答案
2017-04-09 22:33:47.362 ReleaseTest[3338:184169] string: leichunfeng
2017-04-09 22:33:47.362 ReleaseTest[3338:184169] string: leichunfeng
2017-04-09 22:33:47.396 ReleaseTest[3338:184169] string: leichunfeng
我的第一反應是這不科學,weak是不會增加引用計數的,怎麼可能不釋放呢?難道我所理解的都是不對的?
然後我改了改,把NSString改成NSDate
//場景一
2017-04-09 22:42:12.211 ReleaseTest[3453:189869] date: 2017-04-09 14:42:12 +0000
2017-04-09 22:42:12.211 ReleaseTest[3453:189869] date: (null)
2017-04-09 22:42:12.227 ReleaseTest[3453:189869] date: (null)
//場景二
2017-04-09 22:41:21.333 ReleaseTest[3428:188843] date: (null)
2017-04-09 22:41:21.333 ReleaseTest[3428:188843] date: (null)
2017-04-09 22:41:21.349 ReleaseTest[3428:188843] date: (null)
//場景三
2017-04-09 22:39:33.494 ReleaseTest[3395:187226] date: 2017-04-09 14:39:33 +0000
2017-04-09 22:39:33.494 ReleaseTest[3395:187226] date: (null)
2017-04-09 22:39:33.511 ReleaseTest[3395:187226] date: (null)
這個答案才對嘛,不過有兩個疑惑:
- NSString下,引用計數為0,可是沒有釋放記憶體
- stringWithFormat這個方法創建的對象,會被系統自動添加到了當前的 autoreleasepool中,賦個變數後,引用計數會為2(作者的想法,可是現在沒法驗證)
Tagged Pointer
第一個疑惑很好解決,這是Tagged Pointer的鍋。
Tagged Pointer是一個能夠提升性能、節省記憶體的有趣的技術。
他不是一個對象,不用在堆上分配空間,感覺和python變數的存儲方式很像,簡單點理解就是以變數值來定址,只要變數相同,就指向同一個地址,讀取速度非常快。
NSString *tempStrA = @"lu";
NSString *tempStrB = @"lu";
NSNumber *tempNumA = @(123);
NSNumber *tempNumB = @(123);
NSDate *tempDateA = [NSDate date];
NSDate *tempDateB = [NSDate date];
2017-04-11 23:33:46.312 NSStringTest[30025:411902] tempStrA:0x10bd39068
2017-04-11 23:33:46.313 NSStringTest[30025:411902] tempStrB:0x10bd39068
2017-04-11 23:33:46.313 NSStringTest[30025:411902] tempNumA:0xb0000000000007b2
2017-04-11 23:33:46.313 NSStringTest[30025:411902] tempNumB:0xb0000000000007b2
2017-04-11 23:33:46.313 NSStringTest[30025:411902] tempDateA:0x600000008b00
2017-04-11 23:33:46.313 NSStringTest[30025:411902] tempDateB:0x600000008b70
NSNumber對象緩存以及Tagged Pointer
這篇博客提到Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate。
NSNumber的地址位很高,明顯是棧上,符合Tagged Pointer。
NSString地址位很低,同樣的值分配的同一個空間,應該是在常量區。
NSDate同一個值存在不同的地址,應該不是Tagged Pointer。
令人疑惑的地方
回到最開始的問題,就是autoreleasepool可以降低記憶體峰值,這個很好測試,這邊就有個小測試。
autoreleasepool避免記憶體峰值
不過自己測試下來發現一個奇怪的地方:
- (void)doSomething {
for (int i = 0; i < 10e6; ++i) {
第一種:
[NSString stringWithFormat:@"%d", i];
[NSString stringWithFormat:@"%d", -i];
第二種:
[NSString stringWithFormat:@"%d%d",i,-i];
}
}
上面一種情況測試下來是:
第二種情況是:
即使沒有autoreleasepool,第一種情況記憶體絲毫不漲,但是第二種情況漲的很快,而且結束後感覺記憶體沒有回收。
MRC測試
帶著一些疑問,首先想到測試下計數。換成MRC環境:
NSString *a = @"a";
NSString *b = @"aaaaaaaaaaa";
NSString *c = [NSString stringWithFormat:@"a"];
NSString *d = [NSString stringWithFormat:@"%@%@",a,b];
NSString *e = [NSString stringWithFormat:@"%@%@",a,c];
NSLog(@"%ld and %ld and %ld and %ld and %ld", (unsigned long)[a retainCount], (unsigned long)[b retainCount], (unsigned long)[c retainCount], (unsigned long)[d retainCount], (unsigned long)[e retainCount]);
2017-04-16 13:50:16.934 MRCTest[2302:110760] -1 and -1 and -1 and 1 and -1
有的引用計數為-1,有的引用計數為1。
為-1的情況介紹很多,就是說不由引用計數來管理記憶體釋放,由系統來管理。
為1的情況肯定還是由引用計數來管理。
感覺應該和Tagged Pointer有關係。
NSString特殊的記憶體管理
靈機一動,想到了NSString判斷字面量是否相等是不用==
,而是用isEqualToString來判斷的,這些和引用計數,Tagged Pointer是不是有關係呢?
繼續測試,還是剛纔上面的5個值:
2017-04-16 14:04:55.427 NSStringTest[2729:120949] a:0x10817d068 __NSCFConstantString
2017-04-16 14:04:55.428 NSStringTest[2729:120949] b:0x10817d088 __NSCFConstantString
2017-04-16 14:04:55.428 NSStringTest[2729:120949] c:0xa000000000000611 NSTaggedPointerString
2017-04-16 14:04:55.428 NSStringTest[2729:120949] d:0x600000030180 __NSCFString
2017-04-16 14:04:55.428 NSStringTest[2729:120949] e:0xa000000000061612 NSTaggedPointerString
因為存儲地址從高位到地位為棧區,堆區,常量區。
所以很明顯可以得出結論:
類型 | 存儲區 | 引用計數 |
---|---|---|
__NSCFConstantString | 常量區 | -1 |
NSTaggedPointerString | 棧區 | -1 |
__NSCFString | 堆區 | 1 |
NSString每種初始化方式,或者字元的長度都會影響到他的類型和存儲區。所以不能用==
來判斷。
根據上面的記憶體情況,NSTaggedPointerString確實是提高性能,節省記憶體的類型。所以,如果字元串很短,應該用stringWithFormat的方式初始化。
所以很多時候不是僅僅解決了問題就行了,還要往深處挖,知道為什麼這樣解決,正是這次的記憶體峰值,讓我知道了NSString的特殊之處,在後來一眼就解決了一個很少見很奇特的bug。
很少見的bug
主要是做的游戲是cocos2dx寫的,需要傳string值給oc。簡化後就是下麵這種狀況:
std::string cstr = "1";
void *c = &cstr; //第一種場景
//void *c = (void *)cstr.c_str(); //第二種場景
NSString *tempC = [NSString stringWithUTF8String:(char *)c];
NSMutableDictionary<NSString *, NSString *> *dict = [[NSMutableDictionary alloc] init];
[dict setObject:@"123" forKey:tempC];
NSLog(@"%d",[dict.allKeys containsObject:@"1"]);
大家可以寫寫測測看看類型,還可以在ios8(Tagged Pointer還沒出來)下測測,兩種場景是不一樣的,挺有意思的。