美文网首页
【iOS】一道经典面试题引发的思考

【iOS】一道经典面试题引发的思考

作者: TommyWu0889 | 来源:发表于2018-11-29 15:08 被阅读0次

网上有这么一道面试题:
请指出下列代码的问题,并提出解决方案。

for(int i = 0; i<10000000 ;i++)
    {
            NSString *str = @"abc";
            str = [str lowercaseString];
            str = [str stringByAppendingString:@"xyz"];
    }

答案是需要添加自动释放池:

for(int i = 0; i<10000000 ;i++)
    {
        @autoreleasepool{
            NSString *str = @"abc";
            str = [str lowercaseString];
            str = [str stringByAppendingString:@"xyz"];
        }
    }

网上的答案五花八门,我重新整理了一下,主要还是内存管理方面的知识。

1.MRC 和 ARC

首先明确一点,无论是 MRC(手动引用计数) 还是 ARC(自动引用计数) ,OC对对象内存的管理都是使用引用计数来实现,也就是以下这些方法:

- 【生成并持有对象】引用计数+1     alloc/new/copy/mutableCopy等方法  
- 【持有对象】引用计数+1          retain方法         
- 【释放对象】引用计数-1          release方法      

这里关键是 release方法,调用release方法后引用计数-1,如果为0则給对象发送dealloc消息销毁对象回收内存。
ps:这也是为什么说release不是回收对象,它只是将引用计数-1而已,真正的回收是给对象发送dealloc消息。

只有在MRC环境下才需要手动调用 retain ,release方法,ARC环境下编译器会插入相关方法,以实现自动引用计数。

2.autoreleasePool 自动释放池

既然ARC已经为我们实现了自动引用计数管理,那么是否表示万事大吉了呢?其实这里忽略一点,就是加入到 autoreasepool的对象。
在MRC下我们可以调用 [xxx autorealse]会将对象 push到autorelasePool里(同时发送retain 引用计数加1),在当前runloop休眠前,autorelasePool会进行清空(pop操作),同时向对象发送release 消息,引用计数减1,此时如果引用计数为0,则发送dealloc,以此达到自动回收内存当过程。(关键还是realse消息)

void dosomthing{

    Person* p = [[Person alloc] init]; //生成并持有 引用计数+1
    [p autorelease]; //加入到自动释放池中 

}//方法结束了也不会回收

如果看过main方法就会发现,在开启主线程时,系统已经创建好了一个自动释放池。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

那么在ARC环境下,哪一些是autorease对象呢?
答案就是使用了 非 alloc/new/copy/mutableCopy 等方法生成的对象,例如:[NSArray array], [NSDate dataWith] 等。这些方法虽然没有显式调用autorelease 方法,但是ARC在编译时会插入相关代码,自动加入到autoreleasePool中。

所以同样的ARC下无法调用 autorelease方法。

好了,这下可以回到上面的问题了,再看一下代码:

for(int i = 0; i<10000000 ;i++)
    {
        @autoreleasepool{
            NSString *str = @"abc";
            str = [str lowercaseString];
            str = [str stringByAppendingString:@"xyz"]; //autorelease对象
        }
    }

这下明白了,问题原因就是在for循环里生成的大量autorealease对象,而这些对象只有等到RunLoop休眠前,在main函数里的autorealsepool被摧毁时才会被回收,所以可能导致内存峰值过高,甚至超出阀值app崩溃。所以需要在循环体内增加一个autorealsepool,当每次循环结束autorealsepool被摧毁时,及时的回收这些对象。

这也是苹果官方给出autoreleasePool的用法之一:
If you write a loop that creates many temporary objects.
You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.

关于网上的回答有几点不清晰:
1.没有理解MRC和ARC的关键点都是引用计数。
2.没有理解autoreleasepool的真正作用:在摧毁时向池内的所有对象发送release消息。
3.对RunLoop的误解,网上有些答案说“只有等到RunLoop循环结束才会被回收”,这么说其实不严谨,会让人觉得好像是Runloop在做回收内存,其实起关键作用的是autoreleasepool。

关于autoreleasepool更深入的探讨可以参考这篇文章:https://www.jianshu.com/p/5559bc15490d

3.扩展思考

for(int i = 0; i<10000000 ;i++)
    {
            Person* p = [Person new];
            [p doSomthing];
            //[p release]; //MRC
    }

3.1 每次循环结束都会回收内存,ARC下自动插入release代码。不需要autoreleasepool。

for(int i = 0; i<10000000 ;i++)
    {
             int a  = 99999999;
             float b  = 999999999.999999;
             //大量的计算
    }

3.2 基础数据类型会被放在栈里,当超出作用域时(也就是循环体),会被弹出栈回收内存(参考C语言)。

结语

ARC在提高开发效率的同时,也隐藏了大量的内存管理细节,所以在日常开发中可能会忽略掉一些习以为常但却很重要的细节。比如循环引用,内存泄漏等,所以还是需要深入理解其原理和实现。

本篇到此结束希望能帮到您,如有错漏的地方欢迎指正, 谢谢~

相关文章

网友评论

      本文标题:【iOS】一道经典面试题引发的思考

      本文链接:https://www.haomeiwen.com/subject/bxnxcqtx.html