iOS - 详解内存管理

 

写在前面


  

      下面的内容,《Obcject-C 高级编程 iOS与OS X 多线程和内存管理》一书是去年看的。那时想总结的,忘记了,趁着最近有时间,再把这本书回炉重新理解再看一遍,对比自己的理解,以及一些Swift内存管理的知识总结的内容,可能文章内容会比较长,就是希望自己能把内存管理这方面的知识真正的仔细总结一下,也方便自己以后回顾:

      到底什么是ARC?

      在书中一句话总结成了“ARC(Automatic Reference Counting)代表的是自动引用计数,自动引用计数是指内存管理中对引用采取自动计数的技术”。

     

理解ARC先清楚这个“引用计数”和内存管理的思考方式


 

      书中关于理解“引用计数”这个概念引入的“开关房间的灯”的例子也是挺经典的,这里只是一个简单的说明:

      在OC中,我们办公室的照明设备用来比喻我们的“对象”,“对象的使用环境”相当于我们上班进入办公室的人,“对象的使用环境”多一次就等于是上班进来一个人,这样我们就可以这样理解“引用计数”:

      1、第一个人进入办公室,办公室的照明设备(对象)使用的人多了一个,加1,计数值就从0变成了1

      2、之后每当上班的人多一个(对象的使用环境多一个),那我们的引用计数就+1

      3、下班后上班的人少一个( 对象的使用环境少一个),那我们的引用计数就-1

      4、最后一个人也走了,引用计数再-1就变成了0,没人再去使用对象,这时候引用计数变成0,我们就废弃对象

      要结合它说的这个例子去理解引用计数相信还是比较容易的,下面再说说书中总结的内存管理的思考方式,罗列出这四点思考方式之后大家先别着急往下面看,好好的想一下说的这四点思考方式,理解一下我们能怎样去对应着四点做相应的操作!

      1、自己生成的对象自己拥有

      2、非自己持有的对象自己也能持有

      3、不在需要自己持有的对象时候释放这个对象

      4、非自己持有的对象自己是不能去释放的

      上面你也能看到提到了“生成”、“释放”、“持有”等词,这些对象操作在对应Object-C的方法中是下面这样一个对应关系,一张表总结一些,先有个印象,后面在数对它的理解以及一些需要注意的点:

 

 

      在书中是对这四点的思考方式做了一一的说明的,我这里就不再去一一的说明这几点,说说需要我们理解记住的几个地方:

      第一:注意一下上面说的“生成并持有对象”对应的几个方法,它并不是只有这四个方法才能让“生成并持有对象”!而是使用那四个名称“开头”(切记是开头)的方法都意味着自己生成并持有对象,要理解这个开头的意思,命名要符合“驼峰法”(这个不理解的自己去查查)的系统方法才算是用它们开头,像我随便写一个allocWithUser(),或者allocreat()等等的方法是不行的,这个相信都是能理解并作出正确的判断的。

      第二:注意最后一点,无法释放非自己持有的对象,也就是通过前面说的使用alloc/new/copy/mutableCopy方法生成并持有的对象或使用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。除了这些方法之外的对象,自己是无法释放的,还有就像书中写的例子一样,已经realese掉的对象你在执行其他操作,就是释放非自己持有的对象,就会造成程序崩溃。        

     

从代码中看看alloc/retain/release/dealloc       


 

      下面的源码是从苹果公开的代码中我们查看的,你可以点击这里查看 runtime 源码!里面就有NSObject.mm源码供我们学习。

      后面的具体关于源码的解析这里就不总结了,因为这一块的内容单独写出来都能写几篇文章,几句是说不清它的实现过程的,但我们不说并不代表就没法好好看一下这部分的内容了,既然NSObject.mm源码部分以及公开了,有时间你就可以好好看看这部分的内容。

      关于源码解读我找了几篇非常不错的文章收录在这里,尤其推荐第一篇,但就是篇幅有点长,需要耐心去读。

 

       1、Objc 对象的今生今世

      2、iOS NSObject.mm源码解析

     3、iOS Copy解析以及源码分析

       

   

 循环引用


       

      在理解这个循环引用之前在书中总结了一下几个所有权的修饰符  __strong 和 __weak ,那这些修饰符是用在哪里的呢,当然是对象类型。

      所谓的对象类型就是指向NSObject这样的OC类的指针,例如 NSObject * , id类型用于隐藏对象类型的类名部分,相当于C语言常用的 void * 

      __strong 修饰符是id类型和所有对象类型默认的修饰符,这点我们知道就可以,它标识对对象的“强引用”,持有强引用的变量在超出其作用域时候被废弃,随着强引用的失效,引用的对象会随之释放。

     下面通过这张图我们看一下“强引用”的概念,然后结合简单的代码让我们掌握什么到底什么是“强引用”!

       看下面的代码:

@interface TestObject:NSObject
{
        id __strong _obj;
}
-(void)setObject:(id __strong)obj;
@end

@implementation TestObject
- (instancetype)init{
        self = [super init];
        if (self) {
        }
        return self;
}
-(void)setObject:(id __strong)obj{
        _obj = obj;
}
@end

 

      要是我们有下面这样的引用关系就会导致循环引用:

      id  test0 = [TestObject alloc]init];

      id  test1 = [TestObject alloc]init];

      [test0 setObject test1];

      [test1 setObject test0];

       

      上面这段代码我们一般肯定不会这么写,我们在这里只是简单的说明一下什么是“循环引用”,上面这段代码相信能明白什么是“循环引用”,在看这本书Block内容的时候有一个比较好的例子,准便也给大家看看:

typedef void(^Block)();
@interface TestObject:NSObject
{
        Block _block;
}
@end

@implementation TestObject
- (instancetype)init
{
        self = [super init];
        if (self) {
                __block id tmp = self;
                _block = ^{
                        NSLog(@"self = %@",tmp);
                        tmp = nil;
                };
        }
        return self;
}

-(void)execBlock{
        _block();
}

-(void)dealloc{
        NSLog(@"dealloc");
}

@end

int main(){
        
        id testObject = [[TestObject alloc]init];
        [testObject execBlock];
        return 0;
}

      大家分析一下这段代码有没有“循环引用”! 

      答案是:上面这种写法没有引起“循环引用”,关键点就是我们用testObject 这个对象调用了execBlock 这个方法,而这个方式是执行了一下Block,那执行一下为什么就没有循环引用呢,我们这样解释!

      假如:我们没有  [testObject execBlock] 这句代码,那就有循环引用并且会造成内存泄漏。(内存泄漏的原因:应当废弃的对象在超出其作用域的之后任然存在,这就会造成内存泄漏

      上面我们假如没调用之后说有“循环引用”,那这个引用关系又是什么样子的?分析一下:

      1、初始化我们的 TestObject 之后,我们的TestObject就持有了 Block (这个Block就是赋值给_block变量的Block表达式)

      2、我们的Block是持有__block对象的,这个不必多说

      3、  __block id tmp = self  这句代码就让tmp变量持有了 TestObject,在进入下面将Block表达式赋值给_block变量之后,由于Block表达式有截获局部变量的属性,所以tmp被Block表达式截获,是有了tmp,这样我们的_block变量也就持有了tmp,也就是是有了TestObject对象。

      这样就有了如下图的一个持有关系:

     

      这就是上面的持有关系,这样就形成了“循环引用”!

      通过调用 execBlock 这个方法,也就是执行了一下我们的Block表达式之后为什么就不会有“循环引用”呢?关键的一点就是这句:  tmp = nil;

      这样之后我们的_block变量中的tmp就被赋值成nil,这样_block对TestObject的强引用就失效,我们的_block也就不再持有TestObject对象!这样就没有了循环引用!就没有了内存泄漏,调用之后的持有关系如下:

      上面说的其实就是利用block来改变“循环引用”,那__weak呢?它又是怎样作用的?

      __weak 修饰符它是弱引用,只指向不会持有对象,也就避免了对象之间的相互持有造成的“循环引用”,__weak 还有一个优点就是,在持有某对象的弱引用时,要是这个对象被废弃,则该弱引用将自动失效且处于nil被赋值的状态,也就是所谓的“空弱引用”!所以,通过检查被__weak修饰的变量是否为nil,来判断被赋值的对象是否已经被废弃!

 

 这些得注意


     

      这一块的东西我们主要说说下面的这几个内容:

      1、一些关于内存管理的规则

      2、@autoreleasepool

      3、OC 对象和 Core Foundation对象之间的转换

  

      第一点:一些关于内存管理的规则

      (1)、在ARC中由于内存管理是编译器的工作,因此没有必要使用内存管理的方法。也就是在设置了ARC后,就无需再去使用retain或者是release代码。

      (2)、无论ARC是否有效,只要对象的所有者不在持有对象的时候该对象就会被废弃,对象被废弃时,不管ARC是否有效,都会调用对象的dealloc方法,在ARC有效的时候就不在显式的调用dealloc方法。

      (3)、内存管理的方法命名规则

 

      第二点:@autoreleasepool

      顾名思义,autorelease就是自动释放,那也就能理解autoreleasepool就是自动释放池,要了解autoreleasepool就需要我们了解autorelease,理解了autorelease也就理解了autoreleasepool。

      autorelease会像C语言的自动变量那样来对待对象实例,当超出其作用域时候,对象实例的release实例方法就会被调用。但是在大量生成autorelease对象时,只要不废弃,也就造成内存不足,有一个典型的处理方式,我们一起了解一下:

      在读入大量图片的同时改变尺寸,大概过程是图像文件读入到NSData对象,并从中生成UIImage,改变该对象的尺寸之后生成新的UIImage对象,这种情况下就会产生大量的autorelease对象。下面这段伪代码表达的就是这种情况。

for (int i=0; i<图片数; i++) {
             
        /*
           读入图片
           大量产生autorelease对象
           由于没有废弃NSAutoreleasepool对象
           最后导致内存不足
       */          
}

      那面对这种情况,我们该怎么处理呢?下面的这段伪代码有给了我们答案:

for (int i=0; i<图片数; i++) {              
      /*
        在此情况下,有必要在适当的地方生成、持有或者废弃NSAutoreleasePool对象
        读入图像
        大量产生autorelease对象
       */
        NSAutoreleasePool * pool =[[NSAutoreleasePool alloc]init];
                
       /*
        通过 drain 方法
        autorelease对象被一起release
       */
        [pool drain];
}

      那我们说了这么多,好像没有说到 @autoreleasepool ,其实在ARC有效的情况下我们就直接使用 @autoreleasepool{} 代替了NSAutoreleasePool,但它们做的事以及其中的原理确实相同的,明白了NSAutoreleasePool也就明白了@autoreleasepool 。

      最后,在Cocoa框架中,也有许多的类方法用于返回 autorelease  对象,比如 NSMutableArray 类的 arrayWithCapacity 类方法,比如下面两个方法是一样的,只不过在ARC环境中不需要我们自己再去写 autorelease

NSMutableArray * array = [NSMutableArray arrayWithCapacity:1];
NSMutableArray * array = [[NSMutableArray arrayWithCapacity:1] autorelease];

 

      第三点:OC 对象和 Core Foundation对象之间的转换

      id 类型就是我们的OC对象   void * 类型就是C类型对象 ,我们上面说的 Core Foundation 框架就是C语言编写的,它们两者之间就存在着一个相互转换的关系。其实Core Foundation对象和OC的对象区别很小,区别之处就是是使用Core Foundation框架还是Foundation框架生成的区别,所以在ARC无效的时候,只用简单的C语言转换就能实现互换,另外这种转换不需要使用额外的CPU资源,因此也被称为“免费桥”。

      那id类型和void * 类型之间的转换该怎么做呢?我们说下面三个转换方法:

      1、"__bridge 转换类型",要是只是简单的赋值,就可以使用它,这个转换可以 不改变对象的持有状况 它的缺点就是,要是转换为void * 的__bridge转换其安全性与赋值给__unsafe_unretained修饰符相近,甚至更低,如果管理不善,没有注意渎职对象的所有者,就很容易因为野指针而造成程序崩溃 

      2、"__bridge_retained 转换类型", 这个转换可以使 要转换赋值的变量也持有所赋值变量持有的对象 ,通常用作OC对象转换成CF对象

      3、"__bridge_transfer 转换类型", 这个转换可以使 被转换的变量所持有的对象在该变量被赋值给转换目标变量之后随之释放  通常用作CF对象转换成OC对象

      有一点需要说一下的,在下面的伪代码中,CFBridgingRetain 和 __bridge_retained是相同的,CFBridgingRelease 和__bridge_transfer是相同的。

Demo1:

         CFMutableArrayRef mutableRef = NULL;
        {
                // obj 变量持有NSMutableArray生成的对象
                id obj = [[NSMutableArray alloc]init];
                // 通过CFBridgingRetain这个转换,obj持有的对象mutableRef也持有
                mutableRef = CFBridgingRetain(obj);
                CFShow(mutableRef);
                // 这个时候在检查mutableRef变量持有对象的引用计数,由于这个对象被obj
                // 和mutableRef两个变量持有,count就成了2
                printf("mutableRef count = %ld\n",(long)CFGetRetainCount(mutableRef));
                
                // 出了这个作用域的时候,obj超出其作用域,它对NSMutableArray生成的对象强引用就失效
                // 释放持有的对象
        }
        
        // 由于obj超出其作用域,释放持有的对象
        // 前面NSMutableArray生成的对象就只有mutableRef一个持有
        // 所以mutableRef持有对象的引用计数就成 1
        printf("mutableRef count = %ld\n",(long)CFGetRetainCount(mutableRef));
        
        // 这里就使用CFReleas释放mutableRef持有的对象,它的引用计数就减 1 成了 0
        // 废弃这个对象
        CFRelease(mutableRef);

Demo2: 

{
                // CF框架生成并持有自己生成的对象,并且将指向这个对象的指针赋给mutableRef变量
                // 这个时候这个生成的对象的强引用数为 1
                CFMutableArrayRef mutableRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
                printf("mutableRef count = %ld\n",CFGetRetainCount(mutableRef));
                
                /*
                 由于obj变量是强引用修饰符,所以变量obj持有对象强引用
                 通过CFBridgingRelease的赋值
                 变量obj持有对象强引用的同时
                 mutableRef持有的对象要被释放掉
                 */
                id obj = CFBridgingRelease(mutableRef);
                printf("mutableRef count = %ld\n",CFGetRetainCount(mutableRef));
         
                /*
                   obj强引用了对象,对象的引用计数加1,但与此同时
                   mutableRef指向的对象要被释放掉,强引用数减 1
                   这个时候对象也就只有被obj一个变量强引用,所以计数器值为1
                 
                 */
                
                /*
                 另外:由于赋值给变量mutableRef的指针也指向仍然存在的对象
                 所以可以正常的使用
                 */
                NSLog(@"class %@",obj);
        }
        /*
          到了这,变量obj也超出了它的作用域
          强引用失效,对象引用计数为0,就被释放
          随后没有持有者在持有对象,对象被废弃

这时候考虑一下mutableRef是什么情况!!!!
*/

 

      最后,考虑一下,还是上面这个伪代码,如果不是使用 CFBridgingRelease 而是使用 __bridge 会是什么情况呢?

               // 要是是这种情况,使用__bridge转换,会是怎样的情况
                // 首先是对象的引用计数会加1,这个理由和前面的一样
                // 下面的打印计数就会成为2,因为没有release这么一说
                // 当obj超出作用域后,强引用失效,引用计数减去1,
                // 但是引用计数减去1之后就会是1,对象任然存在,找超出其作用域之后任然没有得到释放
                // 内存泄漏
                id obj = (__bridge id)mutableRef;

 

ARC怎么实现的?


  

      我们就得聊聊我们说了这么多的ARC到底是怎么实现的!

      你要是像书中那样去具体的讨论__strong或者__weak修饰符那样去写他们的实现,估计得写很久很久,并且那一块的代码按照我的能力理解是有点点吃力,这个以后要是自己完全懂了,有能力在总结这些修饰符的具体的实现。

      我们在这里大概的提一句: 苹果的实现按照书中的说是采用的可能是“散列表”也就是引用计数表来实现。

      剩下的我们这里就不在具体的说了,有兴趣的可以去翻翻书中的具体代码。

posted @ 2018-06-22 10:25  MrRisingSun  阅读(749)  评论(0编辑  收藏  举报