OC ARC&MRC

124 阅读13分钟

要打开监视僵尸对象

内存管理简介

  • 内存管理的管理范围

    • 任何继承了NSObject的对象
  • 只有OC对象才需要进行内存管理的本质原因

    • OC对象存放于堆里面
    • 非OC对象一般放在栈里面(栈内存会被系统自动回收)
  • 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出);

  • 堆(操作系统):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。

image.png

引用计数器

  • 什么是引用计数器

    • 每个OC对象都有自己的引用计数器
    • 它是一个整数(4个字节)
    • 从字面上, 可以理解为”对象被引用的次数”
    • 也可以理解为: 它表示有多少人正在用这个对象,多少个指针指向这个对象
  • 当没有任何人使用这个对象时, 系统才会回收这个对象, 也就是说

    • 当对象的引用计数器为0时,对象占用的内存就会被系统回收
    • 如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )
  • 任何一个对象, 刚生下来的时候, 引用计数器都为1

    • 当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
  • 引用计数器的常见操作

    • 给对象发送一条retain消息,可以使引用计数器值+1(retain方法返回对象本身)
    • 给对象发送一条release消息, 可以使引用计数器值-1
  • 自动引用计数

    • ARC: Automatic(自动) Reference(引用) Counting(计数)

    • 什么是自动引用计数? 

    • 不需要程序员管理内容, 编译器会在适当的地方自动给我们添加release/retain等代码

    • 注意点: OC中的ARC和java中的垃圾回收机制不太一样, java中的垃圾回收是系统干得, 而OC中的ARC是编译器干得

野指针&僵尸对象&空指针

  • 僵尸对象

    • 已经被销毁的对象(不能再使用的对象)
  • 野指针

    • 指向僵尸对象(不可用内存)的指针
    • 给野指针发消息会报EXC_BAD_ACCESS错误
  • 空指针 p = nil;

    • 没有指向存储空间的指针(里面存的是nil, 也就是0)

    • 给空指针发消息是没有任何反应的

    • 为了避免野指针错误的常见办法

      • 在对象被销毁之后, 将指向对象的指针变为空指针

引用计数器与内存管理

  • 内存管理原则
    • 谁创建谁release :

      • 如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease
    • 谁retain谁release:

      • 只要你调用了retain,就必须调用一次release
    • 多个对象

      • 只要还有人在用某个对象,那么这个对象就不会被回收
      • 只要你想用这个对象,就让对象的计数器+1
      • 当你不再使用这个对象时,就让对象的计数器-

set方法内存管理

(1)retain需要使用的对象

(2)release之前的对象

(3)只有传入的对象和之前的不同才需要release和retain
- (void)setRoom:(Room *)room
{
    // 避免指针重复指向,过度释放
    if (room != _room)
    {
        //指针更换指向
        // 对当前正在使用的房间(旧房间)做一次release
        [_room release];

        // 对新房间做一次retain操作
         _room = [room retain];
    }
}

dealloc方法的内存管理

  • 当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收

  • 对象即将被销毁时系统会自动给对象发送一条dealloc消息 (因此, 从dealloc方法有没有被调用,就可以判断出对象是否被销毁)

  • dealloc方法的重写

    • 一般会重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言
    • 一旦重写了dealloc方法, 就必须调用[super dealloc],并且放在最后面调用
  • 使用注意

    • 不能直接调用dealloc方法
    • 一旦对象被回收了(僵尸对象), 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)
    • 当A对象释放的时候, 一定要对B对象进行一次release, 这样才能保证A对象释放了, B对象也会随之释放, 避免内存泄露
- (void)dealloc
{
    // 当人不在了,代表不用房间了
    // 对房间做一次release操作
    [_room release];
    NSLog(@"%s", __func__);
    [super dealloc];
}
//如果微博中有用户, 用户中有账号(微博最高级),微博的dealloc如下
- (void)dealloc
{
    NSLog(@"%s", __func__);
    /*
    [_text release];
    _text = nil;
    
    [_picture release];
    _picture = nil;

    [_author release];
    _author = nil;

    [_repostStatus release];
    _repostStatus = nil;
     */

    // 下面这句话相当于调用了set方法
    // 先release旧值, 然后再将新值赋值给属性
    self.text = nil;
    self.picture = nil;
    self.author = nil;
    self.repostStatus = nil;
    [super dealloc];
}

autorelease(延迟释放机制)

@autoreleasepool
{ //开始代表创建自动释放池

} //结束代表销毁自动释放池
  • autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
@autoreleasepool{
//调用完autorelease方法后,对象的计数器不变
Person *p = [Person new];
p = [p autorelease];

//或者
Person *p = [[[Person alloc] init] autorelease];
}

注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。

  • 在iOS程序运行过程中,会创建无数个池子(自动释放池可以嵌套使用)。这些池子都是以栈结构存在(先进后出,最后创建的自动释放池,会放到栈顶,也最先释放)
@autoreleasepool { // 栈底自动释放池
  @autoreleasepool {
      @autoreleasepool { // 栈顶自动释放池
          Person *p = [[[Person alloc] init] autorelease];
      }
      Person *p = [[[Person alloc] init] autorelease];
  }
}

//自动释放池中不适宜放占用内存比较大的对象
  • 应用
+ (instancetype)person
{
    return [[[self alloc] init] autorelease];
}

通过类方法创建的基本都是ARC:NSMutableArray *arrM = [NSMutableArray array];

ARC(编译器特性)

  • Automatic Reference Counting,自动引用计数,即ARC。当ARC开启时,编译器将自动在代码合适的地方插入retain, release和autorelease,[super dealloc]

  • ARC是编译器特性,而不是运行时特性

  • ARC的判断原则

    • 只要还有一个强指针变量指向对象,对象就会保持在内存中
  • 强指针

    • 默认所有指针变量都是强指针
    • 被__strong修饰的指针
 Person *p1 = [[Person alloc] init];
 __strong  Person *p2 = [[Person alloc] init];
  • 弱指针

    • 被__weak修饰的指针
__weak  Person *p = [[Person alloc] init];

//千万不要使用弱指针保存新创建的对象
// p是弱指针, 对象会被立即释放
__weak Person *p1 = [[Person alloc] init];

注意:当使用ARC的时候,暂时忘记“引用计数器”,因为判断标准变了。

  • 循环引用问题 ARC和MRC一样, 如果A拥有B, B也拥有A, 那么必须一方使用弱指针
@interface Person : NSObject

//@property (nonatomic, retain) Dog *dog;
@property (nonatomic, strong) Dog *dog;

@end

@interface Dog : NSObject

// 错误写法, 循环引用会导致内存泄露
//@property (nonatomic, strong) Person *owner;

// 正确写法, 当如果保存对象建议使用weak
//@property (nonatomic, assign) Person *owner;
@property (nonatomic, weak) Person *owner;
@end

集合内对象的内存管理

  • 集合本身不是对象,不需要进行retain release操作
  • 如果将一个对象添加到一个数组中, 那么数组会对对象进行一个retain [arrM addObject:p];
  • 当数组对象释放之后, 会给数组中所有的对象发送一条release消息 [arrM release];
  • 当数组移除一个对象之后, 会给这个对象发送一条release消息 [arrM removeObject:p];

集合对象内存管理总结

  • 1.官方内存管理原则

    • 1> 当调用alloc、new、copy(mutableCopy)方法产生一个新对象的时候,就必须在最后调用一次release或者autorelease
    • 2> 当调用retain方法让对象的计数器+1,就必须在最后调用一次release或者autorelease
  • 2.集合的内存管理细节

    • 1> 当把一个对象添加到集合中时,这个对象会做了一次retain操作,计数器会+1
    • 2> 当一个集合被销毁时,会对集合里面的所有对象做一次release操作,计数器会-1
    • 3> 当一个对象从集合中移除时,这个对象会一次release操作,计数器会-1
  • 3.普遍规律

    • 1> 如果方法名是add\insert开头,那么被添加的对象,计数器会+1

    • 2> 如果方法名是remove\delete开头,那么被移除的对象,计数器-1

Copy

copy基本概念

  • OC中的copy

    • 作用:利用一个源对象产生一个副本对象
  • 特点:

    • 修改源对象的属性和行为,不会影响副本对象
    • 修改副本对象的属性和行为,不会影响源对象

Copy的使用

  • 如何使用copy功能

    • 一个对象可以调用copy或mutableCopy方法来创建一个副本对象
    • copy : 创建的是不可变副本(如NSString、NSArray、NSDictionary)
    • mutableCopy :创建的是可变副本(如NSMutableString、NSMutableArray、NSMutableDictionary)
    • 与被拷贝的类无关,可变不可变取决于copy,mutableCopy
  • 使用copy功能的前提

    • copy : 需要遵守NSCopying协议,实现copyWithZone:方法
@protocol NSCopying
- (id)copyWithZone:(NSZone *)zone;
@end
  • 使用mutableCopy的前提

    • 需要遵守NSMutableCopying协议,实现mutableCopyWithZone:方法
@protocol NSMutableCopying
- (id)mutableCopyWithZone:(NSZone *)zone;
@end

自定义对象实现copy

//     1.以后想让自定义的对象能够被copy只需要遵守NSCopying协议
//     2.实现协议中的- (id)copyWithZone:(NSZone *)zone
//     3.在- (id)copyWithZone:(NSZone *)zone方法中创建一个副本对象, 然后将当前对象的值赋值给副本对象即可
#import <Foundation/Foundation.h>

@interface Person : NSObject<NSCopying, NSMutableCopying>
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@end

#import "Person.h"

@implementation Person
- (id)copyWithZone:(NSZone *)zone
{
    // 1.创建一个新的对象
    Person *p = [[[self class] allocWithZone:zone] init];
    // 2.设置当前对象的内容给新的对象
    p.age = _age;
    p.name = _name;
    // 3.返回新的对象
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone
{
    // 1.创建一个新的对象
    Person *p = [[[self class] allocWithZone:zone] init];
    // 2.设置当前对象的内容给新的对象
    p.age = _age;
    p.name = _name;
    // 3.返回新的对象
    return p;
}
@end

#import "Person.h"

@interface Student : Person
@property (nonatomic, assign) double height;
@end

#import "Student.h"

@implementation Student
- (id)copyWithZone:(NSZone *)zone
{
    id obj = [super copyWithZone:zone];
    // 2.设置数据给副本
    [obj setHeight:_height];
    // 3.返回副本
    return obj;
}
@end

深拷贝和浅拷贝

  • 浅复制(浅拷贝,指针拷贝,shallow copy)

    • 源对象和副本对象是同一个对象
    • 源对象引用计数器+1,相当于做一次retain操作
    • 本质是:没有产生新的对象
    • 如果是通过不可变对象调用了copy方法, 那么不会生成一个新的对象

    原因: 因为原来的对象是不能修改的, 拷贝出来的对象也是不能修改的

    既然两个都不能修改, 所以永远不能影响到另外一个对象, 那么已经符合需求

    所以: OC为了对内存进行优化, 就不会生成一个新的对象

  • 深复制(深拷贝,内容拷贝,deep copy)

    • 源对象和副本对象是不同的两个对象

    • 源对象引用计数器不变,副本对象计数器为1(因为是新产生的)

    • 本质是:产生了新的对象

    • 为什么会产生一个新的对象

    1.因为拷贝要求修改原来的对象不能影响到拷贝出来得对象,修改拷贝出来的对象也不能影响到原来的对象, 所以需要生成一个新的对象

    2.由于以前的对象是一个不可变的对象, 而通过mutableCopy拷贝出来的对象必须是一个可变的对象, 所以必须生成一个新的对象

只有源对象和副本对象都不可变时,才是浅复制,其它都是深复制

一次alloc/retain/copy 对应一次release,深拷贝release新的对象,浅拷贝release原来的对象

//浅拷贝
    NSString *str = [[NSString alloc]initWithFormat:@"lnj"];
    NSLog(@"str = %lu", [str retainCount]);
    //不会生成新的对象,但是需要注意,正是因为不会生产新的对象,所以系统会对以前的对象进行一次retain
    //如果是浅拷贝,那么系统就会对原来的对象进行retain
    NSString *str2 = [str copy];
    NSLog(@"str = %lu", [str retainCount]);
    [str release];
    [str release];
//深拷贝:会生成新的对象,正是因为会生成新的对象,所以系统不会对以前的对象进行retain,但是因为生成了新的对象,所以我们需要对新的对象进行release
    NSMutableString *strM = [str mutableCopy];
    NSLog(@"str = %lu", [str retainCount]);
    NSLog(@"strM = %lu", [strM retainCount]);
    NSLog(@"%p - %p", str, strM);
    [strM release];
    [str release];

@property中的copy关键字

  • 防止外界修改内部的数据
    NSMutableString *temp = [NSMutableString stringWithFormat:@"lnj"];
    Person *p = [[Person alloc] init];
    p.name = temp;
    // 问题: 修改了外面的变量, 影响到了对象中的属性
    [temp appendString:@" cool"];
    NSLog(@"name = %@", p.name);
    // 记住: 以后字符串属性都用copy
    //@property (nonatomic, copy) NSString *name;

block

    // block默认存储在栈中, 栈中的block访问到了外界的对象, 不会对对象进行retain
    // block如果在堆中, 如果在block中访问了外界的对象, 会对外界的对象进行一次retain(保证blcok内的外界对象存在,不会被提前释放)
    Person *p = [[Person alloc] init];
    NSLog(@"retainCount = %lu", [p retainCount]);//1
    void (^myBlock)() = ^{
        NSLog(@"%@", p);
        NSLog(@"retainCount = %lu", [p retainCount]);//2
    };
    Block_copy(myBlock); // 将block转移到堆中
    myBlock();
  • 可以使用copy保存block, 这样可以保住block中使用的外界对象的命
// 注意: 如果是block使用copy并不是拷贝, 而是转移
//typedef void (^myBlock)();
//@property (nonatomic, copy) myBlock pBlock;

// 避免以后调用block的时候, 外界的对象已经释放了
    Dog *d = [[Dog alloc] init];
    NSLog(@"retainCount = %lu", [d retainCount]);//1
    Person *p = [[Person alloc] init];
    p.pBlock = ^{
        NSLog(@"%@", d);
    };
    NSLog(@"retainCount = %lu", [d retainCount]); // 2
    // 如果狗在调用block之前释放了, 那么程序就会崩溃
    [d release]; 
    NSLog(@"retainCount = %lu", [d retainCount]); // 1
    p.pBlock();
    [p release];
    
    @implementation Person
    - (void)dealloc
    {
        // 只要给block发送一条release消息, block中使用到的对象也会收到该消息
        Block_release(_pBlock);
        NSLog(@"%s", __func__);
        [super dealloc];
    }
    @end
  • 注意点: copy block之后引发循环引用
    // 注意: 如果是block使用copy并不是拷贝, 而是转移
    //typedef void (^myBlock)();
    //@property (nonatomic, copy) myBlock pBlock;
    // 如果对象中的block又用到了对象自己, 那么为了避免内存泄露, 应该将对象修饰为__block
    __block Person *p = [[Person alloc] init];
    p.name = @"lnj";
    NSLog(@"retainCount = %lu", [p retainCount]);//1
    p.pBlock = ^{
        NSLog(@"name = %@", p.name);
    };
    NSLog(@"retainCount = %lu", [p retainCount]);//1
    p.pBlock();
    NSLog(@"retainCount = %lu", [p retainCount]);//1

    [p release];
    
    @implementation Person
    - (void)dealloc
    {
        // 只要给block发送一条release消息, block中使用到的对象也会收到该消息
        Block_release(_pBlock);
        NSLog(@"%s", __func__);
        [super dealloc];
    }
    @end

@property内存管理策略选择

  • 非ARC

    • 1> copy : 只用于NSString\block
    • 2> retain : 除NSString\block以外的OC对象
    • 3> assign :基本数据类型、枚举、结构体(非OC对象),当2个对象相互引用,一端用retain,一端用assign
  • ARC

    • 1> copy : 只用于NSString\block
    • 2> strong : 除NSString\block以外的OC对象
    • 3> weak : 当2个对象相互引用,一端用strong,一端用weak
    • 4> assgin : 基本数据类型、枚举、结构体(非OC对象)