内存管理说明白点

 

写在前面html


  

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

      到底什么是ARC?安全

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

     

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


 

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

      在OC中,咱们办公室的照明设备用来比喻咱们的“对象”,“对象的使用环境”至关于咱们上班进入办公室的人,“对象的使用环境”多一次就等因而上班进来一我的,这样咱们就能够这样理解“引用计数”:源码分析

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

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

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

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

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

      一、本身生成的对象本身拥有

      二、非本身持有的对象本身也能持有

      三、不在须要本身持有的对象时候释放这个对象

      四、非本身持有的对象本身是不能去释放的

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

 

 

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

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

      第二:注意最后一点,没法释放非本身持有的对象,也就是经过前面说的使用alloc/new/copy/mutableCopy方法生成并持有的对象或使用retain方法持有的对象,因为持有者是本身,因此在不须要该对象时须要将其释放。除了这些方法以外的对象,本身是没法释放的,还有就像书中写的例子同样,已经realese掉的对象你在执行其余操做,就是释放非本身持有的对象,就会形成程序崩溃。        

     

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


 

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

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

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

 

       一、Objc 对象的此生今世

      二、iOS NSObject.mm源码解析

     三、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] 这句代码,那就有循环引用而且会形成内存泄漏。(内存泄漏的缘由:应当废弃的对象在超出其做用域的以后任然存在,这就会形成内存泄漏

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

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

      二、咱们的Block是持有__block对象的,这个没必要多说

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

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

     

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

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

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

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

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

 

 这些得注意


     

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

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

      二、@autoreleasepool

      三、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 * 类型之间的转换该怎么作呢?咱们说下面三个转换方法:

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

      二、"__bridge_retained 转换类型", 这个转换可使 要转换赋值的变量也持有所赋值变量持有的对象 ,一般用做OC对象转换成CF对象

      三、"__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修饰符那样去写他们的实现,估计得写好久好久,而且那一块的代码按照个人能力理解是有点点吃力,这个之后要是本身彻底懂了,有能力在总结这些修饰符的具体的实现。

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

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