最近把一个游戏内嵌到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)
这个答案才对嘛,不过有两个疑惑:性能
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还没出来)下测测,两种场景是不同的,挺有意思的。