在一个面试被问到一个问题:在一个核心入口函数处统计每一个调用方的调用次数,入口函数知道每一个调用方的函数名(NSString *),问题是要怎么统计。
我回答构建一个 NSMutableDictionary<NSString *, NSNumber *>的 static 对象应该就可以解决问题了,用 dispatch_once 保证初始化的线程安全。但是被追问这样的话 NSMutableDictionary 的内存会撑爆,调用量会非常大,怎么优化?
我没答上来,事后也没想明白。内存增大应该是 NSString 的原因,NSNumber 毕竟只是值的变化,能占多少内存。NSMutableDictionary 确实会对 key 进行 copy,我在想什么量级的 NSString 会撑爆内存。
有想法?
1
w99wen 2019-11-07 15:36:16 +08:00
你这个有两个问题,
第一:多线程你这个方法不能保证多线程安全,加锁影响效率。 第二:内存占用吃不消。 你的错误想法: 内存释放的理解有根本错误,再看看书吧。 推荐: 切到串型队列,存储一部分数据后写盘,或者持续写盘。 现在还有问这个的。有意思。 |
2
w99wen 2019-11-07 15:39:19 +08:00
如果问你的人就只能反问成这样,他水平也一般啊。
|
3
ai277014717 2019-11-07 16:38:41 +08:00
感觉 NSNumber 拆装箱应该也考虑进去
|
4
kera0a 2019-11-07 16:46:40 +08:00 via iPhone
是我没看明白吗?
一个调用方调用 1 次和 100w 次,占用的内存不是一样的嘛。 一个程序能用几个调用方啊,这种场景怎么着也和内存无关吧 |
5
wutiantong 2019-11-07 16:47:09 +08:00
不懂为啥会撑爆内存,怀疑面试官的水平。
|
6
xingheng OP @w99wen 确实想过串行的方法,把初始化和 dict 的操作都挪到串行队列里面去。主要问题还是内存,以我的理解,NSMutableDictionary 在 setValue:forKey:的时候确实会对 NSString \*key 进行 copy,但是这个 copy 不会产生新的内存分配(假定是 inmutable string ),只是把原有的 imutable string's retain count 加 1。上面说 NSMutableDictionary 的撑爆内存其实是指 NSMutableDictionary 持有的 NSString 把内存撑爆了。
这个理解有错误吗?请指正。 写 io 的话也想过,一旦 NSMutableDictionary 的数量级到了某个设定量就写文件,这样的话就还需要再次汇总结果了,可能不符合对方的预期。 这个问题来自蚂蚁金服的面试官。 |
7
xingheng OP @kera0a 对方明确说明了调用方数量确实会有很大,我猜测还是 NSString 作为 key 的内存占用量会很大。
也不排除因为他们的 app 在正常运行的时候其他需求上已经占用大量内存了,只是针对或者设计了这样一个问题来优化这个统计结果。 |
8
xingheng OP @ai277014717 NSNumber 拆装箱过程中可能会产生局部变量,内存会在每次退出函数的时候就被释放了,我觉得不至于影响 NSMutableDictionary 所持有的内存。
|
9
kera0a 2019-11-07 17:35:07 +08:00 via iPhone
@xingheng 我觉得出题者没考虑实际情况啊
一个方法能有 100 个调用位置就算它业务复杂了,算下来这个字典顶多 100 个 String + 100 个 number 决定 这个 Dict 大小的只有 key 有多少个,很显然 Key 不可能很多 |
10
w99wen 2019-11-07 17:44:39 +08:00
api 的数量会很大的,内存占用就不能小看了。
比如手淘 /支付宝之类的超大 app,底层做数据统计,整个 app 的 api 绝对是很恐怖的存在。 加上有的 api 是很长的,这个内存要求没问题的。 举个例子,你见过 6000 个会话的用户吗? 我在统计后台看到过。 |
11
w99wen 2019-11-07 17:48:05 +08:00
比如说,你 hook 的 objc_msgsend,统计整个 app 的 api 调用,包括系统底层的调用。那你这个 api 的数量,肯定不能在存内存了。也不能在当前线程操作。
|
12
ai277014717 2019-11-07 18:46:25 +08:00
@xingheng 并不是指内存,而是当遇到这种问题性能也应该考虑进去。
|
13
ai277014717 2019-11-07 18:48:53 +08:00
@xingheng 函数退出时机并不保证会释放内存 参见 autoreleasepool
|
14
luopengfei14 2019-11-07 20:58:02 +08:00 via iPhone
大佬好多,曾经的菜鸡 iOSer 觉得 iOS 开发已经配不上这么深入的研究了。不喜勿喷…
|
15
hoyixi 2019-11-07 21:08:38 +08:00
这玩意的统计,难道不该实时传给服务器 or 缓存到本地到了一定条件同步给服务器吗,难道要常驻在内存里?
|
16
xingheng OP 简单写一下目前我能想的代码结构再讨论吧
``` 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]; } } }); } ``` |
17
xingheng OP @ai277014717 以我的理解,只有给对象发送了 autorelease 消息的对象才会在 autoreleasepool 闭合的时候 release,其他对象还是一直存在的。ARC 下,上面的 url, filename 可以算是 autorelease 对象。
我印象中以前有看过关于 dispatch queue 在执行的时候外围其实已经包了一个 @autoreleasepool{ },这样的话我觉得 autoreleased 对象并不能对内存构成威胁。 请指正。 |
18
xingheng OP @hoyixi 那种存服务器的统计以前我还真写过,就是先写内存然后批量发到服务器,发送失败就临时写文件。但是我觉得面试官在这里应该不是问的一个设计上的问题,还是语言级的内存管理问题。
|
19
samlee123 2019-11-08 09:33:42 +08:00
确实会造成内存暴增,他问的就是 hook msgsend 然后做统计吧 ,调用方法需要开辟方法栈,评论里居然还有人质疑面试官水平。。。。。。🐶
|
20
xingheng OP @samlee123 抱歉,是我没有描述清楚。确定不是 hook msgsend,面试官明确说了被调用方知道是谁调用了自己,参见#16 的示例代码。
|
21
ai277014717 2019-11-08 10:59:02 +08:00
@xingheng 除了个别 init copy new 等方法外一般默认都是 autorelease 对象 例如 numberWithInt:
dispatch_queue 中确实出现了 autoreleasePool |