iOS五大块知识总结之内存管理

1.1 管理的原因

  • 只有OC对象才需要管理内存,非OC对象(如:char、int、folat)则不需要管理内存的本质原因:
    OC对象是放在堆内存里,非OC对象是放在栈内存里,栈内存里的东西系统会自动管理

1.2 释放过渡

  • 当一个对象创建出来,执行了多次release操作,就会报错--释放过渡
  • 简单来说就是
    //引用计数器是1
    Person *p = [[Person alloc]init];
    
    //此时p的引用计数器是0,(p指向的内存已经是坏内存了,称person对象为僵尸对象)
    [p release];
    
    //注意:此时再次调用relesase方法,则会报错`message sent to deallocated instance 0x110201950`(向一个已经释放了的对象发送消息)
    [p release];
    
    //此时p指向僵尸对象(坏内存),则称p为野指针    //给空指针发送消息不会报错,

1.3 野指针、空指针

  • 僵尸对象:已经被销毁的对象(不能再使用的对象)
  • 野指针:指向僵尸对象(不可用内存)的指针
  • 给野指针发消息会报EXC_BAD_ACCESS错误
  • 空指针:没有指向存储空间的指针(里面存的是nil,也就是0)
  • 给空指针发消息是没有任何反应的
  • 为了避免野指针错误的常见办法是在对象被销毁后,将指向对象的指针变为空指针
  • 默认情况下,Xcode是不会管僵尸对象的,使用一块被释放的内存也不会报错,为了方便调试,应该开启僵尸对象监控。如下图:


    iOS五大块知识总结之内存管理_第1张图片
    开启僵尸对象检测

1.4 dealloc方法的重写

  • 当一个对象的引用计数器值为0时,这个对象即将别销毁,其占用的内存被系统回收,系统会自动给对象发送一条dealloc消息
  • 一旦重写了dealloc方法,就必须调用[supper dealloc],且放在最后面调用

1.5 重写setter方法

- (void)set:Room:(Room *)toom
{
    //传进来的room和_room不一样的时候
    if(_room != room){
    //对旧值(当前正在使用的房间)做一次release
    [_room release];
    
    //对新房间做一次retain操作
    [room retain];
    _room = room; 
    //后两步,也可以简化成_room = [room retain]
    }
}
//getter方法
- (Room*)room
{
  return _room;
}

原因:举例子来说,试想这样的场景:一个名叫张三的人想开一间房,比方说最开始他开了房号为01的房子,等张三打算入住的时候发现房间01采光不好,想要换一间房子,此时张三该怎么做呢?他应该是先退掉01号房间,再去开02房子(喜新厌旧可以,但是需要对旧的东西负责),还有加上判断条件,是为了防止重复赋值的时候 出现野指针的错误

1.6 @property属性定义

如果只是用@property修饰一个属性,默认生成对应的setter方法和getter方法。但具体实现是这样的:

//声明属性
@property Dog *dog;
//默认的setter方法实现
- (void)setDog:(Dog *)dog
{
  _dog = dog;
}

显然不能这么干,所以需要加一些修饰属性的关键字,如retain、assign、copy等

  • retain:(MRC下)系统默认重写setter和getter方法,具体实现和1.5的getter、setter方法一样,修饰OC对象,release旧值,retain新值。
  • assign:直接赋值,不做任何内存管理(默认,用于非OC对象)。
  • copy:release旧值,copy新值(一般用于字符串NSString*)。

1.7 @class和#import

  • 作用:#import会包含引用类的所有信息(内容),包括引用类的变量和方法。@class仅仅是告诉编译器有这么一个类,具体这个类里有什么信息,完全不知。
  • 效率:如果有上百个头文件都#import了同一个文件,或者这些文件依次被#import,那么一旦最开始的头文件稍有改动,后面引用到这个文件的所有类都需要重新编译一遍,编译效率非常低。相对来说,使用@class方式就不会出现这种问题。
  • ** 补充三点:**
  • <1>. #import跟#include都能完整的包含某个文件的内容,#import能防止同一个文件被包含多次;
  • <2>. @class仅仅是声明一个类,并不会包含类的完整声明;@class还能解决循环包含的问题;
  • <3>. #import<>用来包含系统自带的文件,#import""用来包含自定义的文件

1.8 autorelease

  • 给对象发送一条autorelease消息,会将对象放到一个自动释放池中
  • 当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
  • 会返回对象本身,调用完autorelease方法后,对象的计数器不变
//自动释放池什么时候销毁?
kCFRunLoopEntry//创建一个自动释放池
kCFRunLoopBeforeWaiting//销毁自动释放池,创建一个新的自动释放池
kCFRunLoopExit//销毁自动释放池
  • 简述一下自动释放池底层怎么实现?

自动释放池以栈的形式实现:当你创建一个新的自动释放池时,它将被添加到栈顶,当一个对象收到发送autorelease消息时,他被添加到当前线程的处于栈顶的自动释放池中,当自动释放池被回收时,他们从栈中被删除,并且会给池子里所有的对象都会做一次release操作。

1.9 string的内存管理

NSString *str1 = @"Jack";
NSString *str2 = [NSString stringWithFormat:@"Rose"];
NSString *str3 = @"Jack";
NSString *str4 = [NSString stringWithFormat:@"Rose"];

注意:直接赋值的,则相应的字符串会放到常量区,常量区的字符串有且只有一份,即str3和str1指向的是同一个字符常量。但通过类方法stringWithFormat自定义创建的字符串会放在堆里面,即使字符串内容相同,也会再次开辟新空间,然后指针就是在栈里面,保存对象的地址。内存分布空间如下:

  • + stringWithFormat:类方法,返回一个autorelease的NSString实例,不用手动Release,在自动释放池中会自动释放。
  • – initWithFormat:实例方法,返回一个自己Alloc申请内存的NSString实例,根据OC内存管理黄金法则,管杀管埋,它则需要自己手动Release。

2.0 copy的内存管理

首先理解一下深复制和浅复制:
深复制

  • 源对象和副本对象是不同的两个对象;
  • 源对象引用计数器不变,副本对象计数器为1(因为是新产生的);
  • 本质是:产生了新的对象。
    浅复制
  • 源对象和副本对象是同一个对象
  • 源对象(副本对象)引用计数器+1,相当于做一次retain操作;
  • 本质是:没有产生新对象。
    注意:只有源对象和副本对象都不可变时,才是浅拷贝,其他都是深拷贝
NSString *str1 = [NSString stringWithFormat:@"address is only one"];
  NSString *str2 = [str1 copy];
  NSLog(@"%p ,%p",str1,str2);
//结果发现两个地址一样,都是0x610000052990。
/**
  1.copy:产生的肯定是不可变副本
  2. 如果是不可变对象调用copy方法产生出不可变副本,那么不会产生新的对象 
*/

然后数组、字典的情况和字符串类似。总结字符串调用copy和mutableCopy的情况如下:

1. NSMutableString调用mutableCopy : 深复制。
2. NSMutableString调用copy : 深复制。
3. NSString调用mutableCopy : 深复制。
4. NSString调用copy : 浅复制

2.1 单例模式

  • 系统单例如:UIApplication 、NSUserDefaults、UIDevice....公用一份,省内存,方便管理,一些整个程序都用的上的数据
  • 单例工具类的简单写法
static NetworkTools *_networkTools;
@implementation NetworkTools

+ (instancetype)sharedNetworkTools
{
    return [[self alloc]init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    if (_networkTools == nil) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _networkTools = [super allocWithZone:zone];
        });
    }
    return _networkTools;
}

- (instancetype)init
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _networkTools = [super init];
    });
    return _networkTools;
}

2.2 KVO

假设有两个类A和B,其中B有个属性age(int类型),现在让A类监听B类的age属性变化,当发生变化时打印一句话。

/**
 *  A对象监听B对象的age属性发生变化
 *  @param options  值变化
    1. NSKeyValueObservingOptionNew:新值、
    2. NSKeyValueObservingOptionOld:旧值
 *  @param event  
 */
[B addObserver:A forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

//KVO实现过程,其代码在运行时会创建一个B类的派生类NSKVONotifying_B,并在NSKVONotifying_B类里重写属性age的setter方法
- (void)setAge:(int)age
{
    [supper setAge:age];
    //并在sette方法里调用这两个方法,当调用了这两个方法,就会通知B类执行那个监听方法
    [self willChangeValueForKey:@"age"];
    [self didChangeValueForKey:@"age"];
}


//MARK:- 在B类里
/**
 *  属性发生改变时执行
 *
 *  @param keyPath 检测的属性 此时是age
 *  @param object  谁的属性
 *  @param change  改变(oldValue、newValue)
 *  @param context
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"监听到%@属性发生改变了,change= %@,context= %@",object,change,context);
}
  • KCO的底层实现原理:

KVO是基于runtime机制实现的。当某个类的对象第一次被观察时,系统就会在运行期动态的创建该类的一个派生类,在这个派生类中重写基类的中任何被观察属性的setter方法。派生类在被重写的setter方法实现真正的通知机制(Person -> NSKVONotifying_Person)

2.3 block的内存管理

  • 默认情况下block的内存是在栈中(不需要手动去管理block内存),它不会对所引用的对象进行任何操作
  • 如果对block进行了copy操作, block的内存会搬到堆里面,它会对所引用的对象做一次retain操作
  • 对于普通的局部变量,block只会引用它的初值,不能跟踪它的改变;block内部能够一直应用被__block修饰的变量,block内部能够一直引用被static修饰的变量,block内部能够一直引用全局变量;
  • block的本质是“带有自动变量的匿名函数”,其实就是一段代码块的内存的指针
  • 非ARC: 如果所引用的对象用了__block修饰,就不会做retain操作。
  • ARC: 如果所引用的对象用了__unsafe_unretained、__weak修饰,就不会做retain操作。(注意:__unsafe_unretained和__weak的相同点是两者都不持有该对象,当他拥有的对象被释放的时候,那此时这个若引用也会自动失效,区别是__weak会被置为nil的状态。__unsafe_unretained不会置nil,会有野指针的风险)
  • 为什么加上 __block就可以修改外部的变量了?

真正的原因是这样的:我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

本文参考资料:
[iOS] stringWithFormat 和 initWithFormat 有何不同?

你可能感兴趣的:(iOS五大块知识总结之内存管理)