zoukankan      html  css  js  c++  java
  • 从一次内存峰值说起

    最近把一个游戏内嵌到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还没出来)下测测,两种场景是不一样的,挺有意思的。

  • 相关阅读:
    python集成开发环境Anaconda的安装
    hasMap2
    WireShark:TCP三次握手 抓包
    CCF 201409-4 最优配餐
    201403-4 无线网络
    java IO的字节流和字符流及其区别
    平衡二叉树DSW算法
    警惕Java编译器中那些“蜜糖”陷阱
    laravel 学习笔记 —— 神奇的服务容器
    LNMP下安装phpmyadmin的一个小错误解决办法
  • 原文地址:https://www.cnblogs.com/stevenfukua/p/6721537.html
Copyright © 2011-2022 走看看