從一次內存峯值提及

最近把一個遊戲內嵌到app裏,選用了微信開源的Mars,結果遇到了內存峯值。解決的方法很容易,加上@autoreleasepool就能夠了。可是作實驗的時候又有了好多疑惑,不停地往深處挖,最終了解了autoreleasepool的實現,Tagged Pointer,和NSString內存管理的特殊性。python

Marsios

咱們作的小遊戲須要實時傳輸數據,數據很小,就選用了Mars。結果內存一直漲,在這裏加個autoreleasepool就能夠避免內存峯值。objective-c

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改爲NSDateapp

//場景一
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。ui

NSNumber的地址位很高,明顯是棧上,符合Tagged Pointer。
NSString地址位很低,一樣的值分配的同一個空間,應該是在常量區。
NSDate同一個值存在不一樣的地址,應該不是Tagged Pointer。.net

使人疑惑的地方
回到最開始的問題,就是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還沒出來)下測測,兩種場景是不同的,挺有意思的。

相關文章
相關標籤/搜索