V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xingheng
V2EX  ›  Objective-C

iOS/Objective-C - NSMutableDictionary 撑爆了内存?

  •  2
     
  •   xingheng · 2019-11-07 13:45:54 +08:00 · 5741 次点击
    这是一个创建于 1841 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在一个面试被问到一个问题:在一个核心入口函数处统计每一个调用方的调用次数,入口函数知道每一个调用方的函数名(NSString *),问题是要怎么统计。

    我回答构建一个 NSMutableDictionary<NSString *, NSNumber *>的 static 对象应该就可以解决问题了,用 dispatch_once 保证初始化的线程安全。但是被追问这样的话 NSMutableDictionary 的内存会撑爆,调用量会非常大,怎么优化?

    我没答上来,事后也没想明白。内存增大应该是 NSString 的原因,NSNumber 毕竟只是值的变化,能占多少内存。NSMutableDictionary 确实会对 key 进行 copy,我在想什么量级的 NSString 会撑爆内存。

    有想法?

    21 条回复    2019-11-08 10:59:02 +08:00
    w99wen
        1
    w99wen  
       2019-11-07 15:36:16 +08:00
    你这个有两个问题,
    第一:多线程你这个方法不能保证多线程安全,加锁影响效率。
    第二:内存占用吃不消。

    你的错误想法:
    内存释放的理解有根本错误,再看看书吧。

    推荐:
    切到串型队列,存储一部分数据后写盘,或者持续写盘。

    现在还有问这个的。有意思。
    w99wen
        2
    w99wen  
       2019-11-07 15:39:19 +08:00
    如果问你的人就只能反问成这样,他水平也一般啊。
    ai277014717
        3
    ai277014717  
       2019-11-07 16:38:41 +08:00
    感觉 NSNumber 拆装箱应该也考虑进去
    kera0a
        4
    kera0a  
       2019-11-07 16:46:40 +08:00 via iPhone
    是我没看明白吗?
    一个调用方调用 1 次和 100w 次,占用的内存不是一样的嘛。
    一个程序能用几个调用方啊,这种场景怎么着也和内存无关吧
    wutiantong
        5
    wutiantong  
       2019-11-07 16:47:09 +08:00
    不懂为啥会撑爆内存,怀疑面试官的水平。
    xingheng
        6
    xingheng  
    OP
       2019-11-07 17:03:04 +08:00
    @w99wen 确实想过串行的方法,把初始化和 dict 的操作都挪到串行队列里面去。主要问题还是内存,以我的理解,NSMutableDictionary 在 setValue:forKey:的时候确实会对 NSString \*key 进行 copy,但是这个 copy 不会产生新的内存分配(假定是 inmutable string ),只是把原有的 imutable string's retain count 加 1。上面说 NSMutableDictionary 的撑爆内存其实是指 NSMutableDictionary 持有的 NSString 把内存撑爆了。

    这个理解有错误吗?请指正。

    写 io 的话也想过,一旦 NSMutableDictionary 的数量级到了某个设定量就写文件,这样的话就还需要再次汇总结果了,可能不符合对方的预期。

    这个问题来自蚂蚁金服的面试官。
    xingheng
        7
    xingheng  
    OP
       2019-11-07 17:08:03 +08:00
    @kera0a 对方明确说明了调用方数量确实会有很大,我猜测还是 NSString 作为 key 的内存占用量会很大。

    也不排除因为他们的 app 在正常运行的时候其他需求上已经占用大量内存了,只是针对或者设计了这样一个问题来优化这个统计结果。
    xingheng
        8
    xingheng  
    OP
       2019-11-07 17:10:58 +08:00
    @ai277014717 NSNumber 拆装箱过程中可能会产生局部变量,内存会在每次退出函数的时候就被释放了,我觉得不至于影响 NSMutableDictionary 所持有的内存。
    kera0a
        9
    kera0a  
       2019-11-07 17:35:07 +08:00 via iPhone
    @xingheng 我觉得出题者没考虑实际情况啊
    一个方法能有 100 个调用位置就算它业务复杂了,算下来这个字典顶多 100 个 String + 100 个 number
    决定 这个 Dict 大小的只有 key 有多少个,很显然 Key 不可能很多
    w99wen
        10
    w99wen  
       2019-11-07 17:44:39 +08:00
    api 的数量会很大的,内存占用就不能小看了。
    比如手淘 /支付宝之类的超大 app,底层做数据统计,整个 app 的 api 绝对是很恐怖的存在。
    加上有的 api 是很长的,这个内存要求没问题的。

    举个例子,你见过 6000 个会话的用户吗?
    我在统计后台看到过。
    w99wen
        11
    w99wen  
       2019-11-07 17:48:05 +08:00
    比如说,你 hook 的 objc_msgsend,统计整个 app 的 api 调用,包括系统底层的调用。那你这个 api 的数量,肯定不能在存内存了。也不能在当前线程操作。
    ai277014717
        12
    ai277014717  
       2019-11-07 18:46:25 +08:00
    @xingheng 并不是指内存,而是当遇到这种问题性能也应该考虑进去。
    ai277014717
        13
    ai277014717  
       2019-11-07 18:48:53 +08:00
    @xingheng 函数退出时机并不保证会释放内存 参见 autoreleasepool
    luopengfei14
        14
    luopengfei14  
       2019-11-07 20:58:02 +08:00 via iPhone
    大佬好多,曾经的菜鸡 iOSer 觉得 iOS 开发已经配不上这么深入的研究了。不喜勿喷…
    hoyixi
        15
    hoyixi  
       2019-11-07 21:08:38 +08:00
    这玩意的统计,难道不该实时传给服务器 or 缓存到本地到了一定条件同步给服务器吗,难道要常驻在内存里?
    xingheng
        16
    xingheng  
    OP
       2019-11-07 21:46:04 +08:00
    简单写一下目前我能想的代码结构再讨论吧

    ```

    void core_func(NSString *caller)
    {
    static NSMutableDictionary<NSString *, NSNumber *> *dict;
    static dispatch_queue_t serialQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    dict = [NSMutableDictionary new];
    serialQueue = dispatch_queue_create("initializer.serial.queue", DISPATCH_QUEUE_SERIAL);
    });

    dispatch_async(serialQueue, ^{
    if (dict[caller]) {
    dict[caller] = @([dict[caller] unsignedIntegerValue] + 1);
    } else {
    dict[caller] = @1;
    }

    if (dict.count > 1000) {
    NSString *url = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *filename = [NSString stringWithFormat:@"stastics-data-%.3f", NSDate.date.timeIntervalSince1970];

    url = [url stringByAppendingPathComponent:filename];

    if ([dict writeToFile:url atomically:YES]) {
    [dict removeAllObjects];
    }
    }
    });
    }

    ```
    xingheng
        17
    xingheng  
    OP
       2019-11-07 22:03:33 +08:00
    @ai277014717 以我的理解,只有给对象发送了 autorelease 消息的对象才会在 autoreleasepool 闭合的时候 release,其他对象还是一直存在的。ARC 下,上面的 url, filename 可以算是 autorelease 对象。
    我印象中以前有看过关于 dispatch queue 在执行的时候外围其实已经包了一个 @autoreleasepool{ },这样的话我觉得 autoreleased 对象并不能对内存构成威胁。

    请指正。
    xingheng
        18
    xingheng  
    OP
       2019-11-07 22:07:43 +08:00
    @hoyixi 那种存服务器的统计以前我还真写过,就是先写内存然后批量发到服务器,发送失败就临时写文件。但是我觉得面试官在这里应该不是问的一个设计上的问题,还是语言级的内存管理问题。
    samlee123
        19
    samlee123  
       2019-11-08 09:33:42 +08:00
    确实会造成内存暴增,他问的就是 hook msgsend 然后做统计吧 ,调用方法需要开辟方法栈,评论里居然还有人质疑面试官水平。。。。。。🐶
    xingheng
        20
    xingheng  
    OP
       2019-11-08 10:23:11 +08:00
    @samlee123 抱歉,是我没有描述清楚。确定不是 hook msgsend,面试官明确说了被调用方知道是谁调用了自己,参见#16 的示例代码。
    ai277014717
        21
    ai277014717  
       2019-11-08 10:59:02 +08:00
    @xingheng 除了个别 init copy new 等方法外一般默认都是 autorelease 对象 例如 numberWithInt:
    dispatch_queue 中确实出现了 autoreleasePool
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3065 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 12:53 · PVG 20:53 · LAX 04:53 · JFK 07:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.