美文网首页
内存管理

内存管理

作者: 意一ineyee | 来源:发表于2018-08-17 15:59 被阅读69次
OC高级编程:内存管理、block、GCD

目录

一、内存分区
 1、RAM和ROM
 2、内存的五大分区
二、内存管理
 1、OC内存管理是指什么?OC内存管理的本质又是什么?我们又是如何进行OC内存管理的?(关键词:引用计数)
 2、MRC及MRC是如何管理引用计数的(关键词:alloc、retain、release、autorelease和autoreleasepool、dealloc)
 3、ARC
三、使用ARC需注意
 1、使用ARC要遵守一些规则
 2、属性的内存管理(捎带把属性的相关知识做了下总结)
  (1)成员变量、实例变量、属性之间的区别(关键词:@property、@synthesize、@dynamic)
  (2)属性修饰符(关键词:原子性、读写权限和内存管理语义)
 3、避免循环引用造成的内存泄漏
  使用代理设计模式的时候,要避免循环引用
  使用block的时候,要避免循环引用
  使用NSTimer的时候,要避免循环引用
 4、避免抛出异常时造成的内存泄漏
 5、在dealloc方法里remove掉观察者,避免向野指针发送消息导致程序崩掉
 6、使用自动释放池,避免大次数循环创建大量临时变量,造成的内存暴涨问题,降低内存使用峰值
 7、非OC对象,我们需要自己管理其内存
四、调试内存问题

一、内存分区


在了解内存管理之前,我们先需要了解一下内存的分区,以便知道我们到底管理的是哪一块的内存。

1、RAM和ROM

内存有两种类型RAM和ROM。

  • RAM(random access memory):是指运行内存,即CPU可以直接访问的内存。它的特点是访问速度快,但不能掉电存储。

  • ROM(read only memory):它是存储性内存,CPU 不可以直接访问。它的特点是存储空间大,可以掉电存储。

由于RAM不可以掉电存储,所以我们的App啊、下载的一些文件啊等等都是存储在ROM中的,但是由于ROM不能直接被CPU访问,所以在App运行的时候系统会将所需资源读取到RAM中来运行。而我们通常所说的内存的五大分区就指的是RAM上的分区

2、内存的五大分区

(内存地址从高到低依次为:栈区 > 堆区 > 静态全局区 > 常量区 > 代码区)

  • 栈区:栈区是一块由系统自动管理的内存区域,用来存储局部变量

  • 堆区:堆区是一块需要我们自己管理的内存区域,我们创建的所有OC对象都存储在这里,同时因为属性和成员变量总是作为OC对象的一部分而、存在,所以属性和成员变量当然也是存储在堆区的

  • 静态全局区:静态全局区也是一块由系统自动管理的内存区域,用来存储静态变量、全局变量和静态全局变量

局部变量:在函数体或代码块内创建的变量为局部变量,存储在栈区,生命周期为函数体或代码块内。

静态变量:局部变量经过static修饰,成为静态变量,存储在静态全局区,生命周期得以延长,而且只会被初始化一次

全局变量:在函数体或代码块外创建的变量为全局变量,存储在静态全局区,全局变量在程序开始运行的时候就创建(并不是在该全局变量所在的类创建时才创建),直到程序运行结束时才释放,生命周期为整个程序的运行周期。

静态全局变量:全局变量经过static修饰,成为静态全局变量,该全局变量只能在类内使用

对象和属性(成员变量):要注意等号右边我们new出来的那个东西才是OC对象,千万不要把等号左边的指针变量当成对象。而属性(成员变量)总是作为对象的一部分存在。

#import "ViewController.h"

NSString *globalString = @"11";// 全局变量
static NSString *staticGlobalString = @"11";// 静态全局变量,

@interface ViewController ()

@property (nonatomic, strong) UILabel *label;// 属性(成员变量),存储在堆区

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *localString = [NSString new];// localString是一个局部变量,存储在栈区,而[NSString new]出来的对象存储在堆区。(要注意localString可不是OC对象啊,它只是个指针变量,后面new出来那个才是OC对象)
    
    static NSString *staticString = @"11";// 静态变量
}

@end
  • 常量区:常量区也是一块由系统自动管理的内存区域,用来存储常量(如整型常量“1111”、浮点型常量“11.11”、字符串常量“hello world”等)。

  • 代码区:代码区也是一块由系统自动管理的内存区域,用来存储二进制执行代码,代码本身是存储在ROM中的,但是实际运行的时候会被拷贝到RAM的代码区。

二、内存管理


1、OC内存管理是指什么?OC内存管理的本质又是什么?我们又是如何进行OC内存管理的?
  • OC内存管理是指什么:由上面内存分区的知识,可以知道我们所说的内存管理就是指对堆区的内存进行管理,因为其它分区的内存都是由系统自动管理的。我们要通过代码来正确的管理堆区内存的开辟和回收,从而避免内存的泄漏。

  • OC内存管理的本质又是什么OC内存管理的本质就是对OC对象引用计数的管理。那什么又是引用计数?所谓引用计数是指我们在创建一个OC对象的时候,系统会为每个对象都分配一个整数,用来表征当前有多少人想让该对象继续存活。那我们凭什么说OC内存管理的本质就是对OC对象引用计数的管理?因为一个OC对象所占内存的开辟和回收,完全是由它的引用计数操控的。比如说对象在刚创建的时候,我们会为该对象开辟内存,它的引用计数为1;如果想让该对象存活,就将其引用计数加1,它所占的内存不会被回收;如果不再想让该对象存活,就将其引用计数减1,如果引用计数没减为0,那它所占用的内存依旧不会被回收;如果引用计数减为0了,就代表没有人希望该对象继续存活了,它就会被释放,它所占用的内存就会被回收。

  • 我们又是如何进行OC内存管理的:OC内存管理的方式有两种,MRC和ARC。MRC是指手动管理内存,即手动管理对象的引用计数,需要我们程序员自己写内存管理的代码。而ARC是指自动管理内存,即自动管理对象的引用计数,是由编译器自动在合适的时机和位置帮我们插入内存管理的代码,ARC是iOS5之后才引入的。现在的话我们一般都采用ARC来管理内存,不过我们还是需要了解一下MRC,这将更有利于我们理解OC的内存管理。接下来,我们详细说一下我们是如何进行OC内存管理的--MRC和ARC

2、MRC及MRC是如何管理引用计数的

MRC(Manual Reference Counting),即手动管理引用计数,需要我们程序员自己写内存管理的代码。

那具体怎么个手动管理引用计数法呢?嗯,我们程序员主要是通过NSObject类和NSObject协议提供的几个方法来实现手动管理引用计数的,方法分别是:

  • alloc、new、copy、mutableCopy创建对象,对象的引用计数为1。
  • retain持有对象,使对象的引用计数加1。
  • release释放对象,使对象的引用计数减1。
  • autorelease释放对象,但是不会立马使对象的引用计数减1,而是会把对象加入到当前的自动释放池中,等到销毁自动释放池的时候,再使它们的引用计数减1。
  • dealloc销毁对象,当对象的引用计数减为0时,会触发该方法来销毁对象。

接下来,我们看下这几个方法的内部实现,来加深对这它们的理解。但是由于NSObject类的代码是没有公开的,所以我们GNUstep的源代码来学习,两者的源代码实现是极其相似的。

(1)alloc的内部实现
+ (id)alloc {
    
    return [self allocWithZone:NSDefaultMallocZone()];
}

+ (id)allocWithZone:(struct _NSZone *)zone {
    
    return NSAllocateObject(self, 0, zone);
}

struct obj_layout {
    NSUInteger retained;// 系统会为每个对象分配一个整数,即引用计数
};

NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
    
    int size = ...;// 计算对象所需占用的内存大小
    id new = NSZoneMalloc(zone, 1, size);// 分配内存
    memset (new, 0, size);
    new = (id)&((obj)new)[1];// 生成指向内存块的指针
}

可见alloc的内部实现为:

  • 创建对象:根据调用alloc方法的类的结构来创建一个对象(对象的本质是一个结构体)。

  • 计算并分配内存:然后计算该对象所需占用的内存大小,为该对象分配内存,并生成该内存块的指针来让alloc返回。

  • 分配引用计数,并存在引用计数表中:同时在这一步,系统也会为该对象分配一个引用计数,设置为1,并将其存放在引用计数表中。OC的引用计数是通过引用计数表来管理的,该表中存储着一对儿一对儿的key-value,key就是某个对象的内存地址,value就是该对象的引用计数。

  • 而接下来我们要调用的init等方法,仅仅是用来对这块内存中对象一些数据的初始化并再次返回该对象。

(2)retain的内部实现
- (id)retain {
    
    NSIncrementExtraRefCount(self);
    return self;
}

inline void NSIncrementExtraRefCount(id anObject) {
    
    // 当前引用计数超出最大值,抛出异常
    if (((obj)anObject)[-1].retained == UINT_MAX - 1) {
        
        [NSException raise: NSInternalInconsistencyException
                    format: @"NSIncrementExtraRefCount() asked to increment too far"];
    }
    
    ((obj_layout)anObject)[-1].retained++;// 引用计数加1
}

可见retain方法的内部实现实质上仅仅是:使对象引用计数加1。

(3)release的内部实现
- (void)release {
    
    if (NSDecrementExtraRefCountWasZero(self)) {
        
        [self dealloc];
    }
}

BOOL NSDecrementExtraRefCountWasZero(id anObject) {
    
    // 如果当前的引用计数为0,则调用dealloc函数
    if (((obj)anObject)[-1].retained == 0) {
        
        return YES;
    }
    
    // 如果当前的引用计数为大于0,则引用计数减1
    ((obj)anObject)[-1].retained--;
    return NO;
}

可见release的内部实现是:判断当前对象的引用计数,如果引用计数为0,就让对象调用dealloc方法去销毁,如果引用计数大于0,则使引用计数减1。

(4)dealloc的内部实现
- (void) dealloc {
    
    NSDeallocateObject (self);
}

inline void NSDeallocateObject(id anObject) {
    
    obj_layout o = &((obj_layout)anObject)[-1];
    free(o);// 释放
}

可见dealloc的内部实现是:销毁对象并回收该对象所占的内存。注意即便在MRC下,dealloc也是被动触发的,这个方法不需要我们主动调用。

(5)autorelease和autoreleasepool的内部实现

autorelease的内部实现:

- (id)autorelease {
    
    NSAutoreleasePool *pool = ...;
    [pool.array addObject:self];
}

可见autorelease的内部实现:并不是使对象的引用计数减1,而是获取当前正在使用的自动释放池,并把对象加入到自动释放池的数组中

autoreleasepool的内部实现:

// 创建,没什么特殊的,我们不做特别说明
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// 销毁
- (void)drain {
    
    [self dealloc];
}

- (void)dealloc {
    
    for(id obj in array) {
        
        [obj release];
    }

    [array release];
}

可见自动释放池释放操作的内部实现为:在销毁自动释放池的时候,自动释放池也会触发自己的dealloc方法,在它dealloc方法中会让池中的所有对象都调用一下release。

那自动释放池既然是自动释放的,我们就不得不问一下自动释放池是什么时候创建的,又是什么时候销毁的

  • 这需要一些runLoop的知识,详见文章runLoop简介。我们知道线程和runLoop是一一对应的,当主线程创建好后,它所对应的runLoop也就自动创建好了,而创建runLoop的时候,系统会添加两个专门用来处理自动释放池的观察者。第一个观察者观察的是runLoop即将进入循环,它的回调里会调用_objc_autoreleasePoolPush()来创建自动释放池,它的优先级是最高的,保证其它所有的回调发生在创建自动释放池之后。第二个观察者观察的是runLoop即将休眠和runLoop即将退出,即将休眠时回调里会调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()销毁旧池子并创建新池,即将退出的回调里会调用_objc_autoreleasePoolPop()来销毁自动释放池,它的优先级是最低的,保证池子的销毁发生在其它所有的回调之前。所以综上,自动释放池在runLoop开始进入循环时创建,runLoop休眠时会销毁并重建,runLoop退出时会销毁。

  • 上面我们说的是主线程中自动释放池的创建和销毁时机,那么其它线程呢?使用GCD创建的线程中,会默认为我们创建自动释放池。但是使用其它方式创建的子线程中,我们需要自己创建自动释放池。那么,子线程自动释放池的创建和销毁则和线程的创建和销毁是一致的,和子线程的runLoop没关系,因为子线程的runLoop都不一定创建了

  • 所以我们不要以为自动释放池和runLoop有多么大的联系,它俩其实是没什么直接的联系,只不过刚好主线程里自动释放池的创建于销毁和主线程的runLoop有点联系而已。

3、ARC

上面我们谈到了MRC,这是iOS5之前的内存管理方式了,使用MRC手动管理内存,不尽代码量相当大,而且稍微处理不好,内存泄漏的风险就很大。因此iOS5之后才引入了ARC。

所谓ARC(Automatic Reference Counting),是指由编译器自动管理引用计数,即编译器会在合适的时间和地方自动的帮我们插入retain、release、autorelease等语句来改变对象的引用计数,而不再需要我们手动的敲入,但其实ARC并不仅仅是简单的帮我们插入内存管理的代码,它在编译时和运行时都做了很多内存管理的优化。这样使用ARC,就大大减少了我们的代码量和内存泄漏的风险,我们也只需要专注于业务的开发。

我们要知道在ARC下,所有的变量在默认情况下都是对它所指向对象的一个强引用,即strong语义。

但这也并不是说有了ARC我们就什么也不用做了,使用ARC时还是有一些需要注意的问题的,下面我们一一介绍。

三、使用ARC需注意


1、使用ARC要遵守一些规则:
  • 不能再直接使用retain、release、autorelease、retainCount等内存管理的方法。

  • 使用@autoreleasepool替代NSAutoreleasePool类。

  • 不要显式调用dealloc方法,但是可以重写dealloc方法,其内部也不要写[super dealloc]这样的语句。

2、属性的内存管理(捎带把属性的相关知识做了下总结)

既然提到属性了,我们不妨在这里把属性的相关知识也做下总结。

(1)成员变量、实例变量、属性之间的区别

我们先来看下什么是成员变量、实例变量和属性。这就要追述到iOS5之前了,那个时候还没有出现属性这个概念,如果我们想要为一个类包装一些数据,那我们只能通过直接的方式给这个类添加成员变量。如下:

-----------Person.h-----------

#import <Foundation/Foundation.h>

@interface Person : NSObject {
    
    NSString *_name;
    NSString *_sex;
    NSInteger _age;
}

@end

这大括号里的_name、_sex、_age就称为Person类成员变量,至于为什么前面要加下划线,是OC官方的命名约定,是为了避免变量泄漏。而_name、_sex又是对象类型的,也就是说是它们是某个类的实例,所以它们又可称为实例变量

所以说:成员变量=实例变量+基本数据类型的变量

那什么又是属性呢?这也得从iOS5之前成员变量的存取方式说起。上面我们已经成功的为一个类添加了三个成员变量,但是如何访问它们呢?答案就是按指定的格式,手动为每个成员变量添加setter和getter方法。如下:

-----------Person.h-----------

#import <Foundation/Foundation.h>

@interface Person : NSObject {
    
    NSString *_name;
    NSString *_sex;
    NSInteger _age;
}

- (void)setName:(NSString *)name;
- (NSString *)name;

- (void)setSex:(NSString *)sex;
- (NSString *)sex;

- (void)setAge:(NSInteger)age;
- (NSInteger)age;

@end

就是这样,整个项目中有多少成员变量,你就得相应的写那么多的setter和getter方法。哇,这不崩盘了吗,累都累死了。所以iOS5之后才引入了@property(属性)@synthesize(合成)@dynamic(动态)三个关键字,就是为了解决为成员变量添加setter和getter方法这个问题的,具体是怎么解决的呢?

我们不再需要直接创建成员变量,而是使用@property关键字来创建属性

-----------Person.h-----------

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property NSString *name;
@property NSString *sex;
@property NSInteger age;

@end

然后再配合一步@synthesize关键字,正是这一步系统会自动的为我们创建一堆与属性名字相同但是前面带下划线的成员变量,并自动生成访问这些成员变量的setter和getter方法,并支持我们使用点语法来访问属性。这样就省去了我们手动反复的写setter和getter方法的繁琐,这的确轻松了不少。

-----------Person.m-----------

#import "Person.h"

@implementation Person

@synthesize name = _name;
@synthesize sex = _sex;
@synthesize age = _age;

@end

再后来,@property更牛了,连@synthesize都吃了。也就是说,我们只要使用@property创建了属性,系统就默认的为我们创建了对应的成员变量和setter、getter方法,一步到位了。

除非有某些情况下,我们偏偏不需要系统默认为我们这样做,这时可以用@dynamic(动态)关键字,它恰恰是告诉编译器在创建属性时的时候,不要为我们自动创建与属性相关的成员变量,也不要自动生成setter和getter方法。我们自己会在运行时动态的创建,比如说为分类添加属性就是一个例子。

(2)属性修饰符

我们平常使用的属性修饰符主要有三对儿:原子性、读写权限和内存管理语义属性修饰符影响的主要就是系统为属性自动生成的setter和getter方法的实现上。

  • 原子性

atomic:默认为atomic,使用atomic的话,编译器为该属性生成的setter和getter方法中会使用同步锁来保证其访问的原子性,比如说有多个线程同时访问这个属性,那么访问到的属性值是相对安全的。

nonatomic:但是我们在实际开发中总是使用nonatomic,是因为在iOS开发中使用同步锁的开销很大,会带来严重的性能问题。另外使用atomic其实并不能保证线程的绝对安全,如果真有这方面的需求,我们需要采取更深层的锁来实现属性的原子性。

  • 读写权限

readwrite:默认为readwrite,表征属性可读可写,系统会自动为属性生成setter和getter两个方法。

readonly:表征属性只能读取,不能写入,系统只会为属性生成getter方法。

  • 内存管理语义

上面两对儿修饰符都会影响属性的setter和getter方法,而内存管理语义这对儿修饰符只会影响属性的setter方法,所以它们的区别也就体现在setter方法不同的内部实现上,所以让我们说区别的时候就可以从这里下手。

MRC下有:assign、retain、copy
ARC下又新增了:weak、strong

assign:使用assign修饰属性的时候,setter方法内部只会进行简单的赋值操作。assign不会使对象的引用计数加1,一般用来修饰基本数据类型。

- (void)setAge:(NSInteger)age {
    
    // 简单的赋值操作
    _age = age;
}

retain:使用retain修饰属性的时候,setter方法内部会先保留新值,然后释放成员变量的旧值,最后把成员变量指向新值。retain会使对象的引用计数加1,一般用来修饰对象类型。(现在我们来看一下“retain会使对象的引用计数加1”是什么意思,我们看到setter方法的内部会对传进来的对象调用一下retain,然后让一个新指针--即成员变量--指向传进来的这个对象,所以retain会使传进来的对象引用计数加1,原因就是retain产生了一个新的指针指向传进来的对象。)

- (void)setName:(NSString *)name {
    
    // 先保留新值
    [name retain];
    
    // 然后释放成员变量的旧值
    [_name release];
    
    // 最后把成员变量指向新值
    _name = name;
}

copy:copy也会使对象的引用计数加1。但是不同于retain的地方是,使用copy的时候,setter方法内部不是保留新值,而是拷贝新值,所以copy会产生一个对象,增加引用计数的是这个拷贝出来的新对象,而不是作为参数传进来的这个对象。

- (void)setSex:(NSString *)sex {
    
    // 先拷贝新值
    [sex copy];
    
    // 然后释放成员变量的旧值
    [_sex release];
    
    // 最后把成员变量指向新值
    _sex = sex;
}
  • copy一般用来修饰block,因为需要我们将栈block复制到堆区。(因为栈区的内存是系统自动管理的,所以存储在栈上的block它的销毁时机是不确定的,这样万一我们在调用block的时候它已经被释放了,程序就会崩掉,所以我们需要把栈block拷贝到堆block,这样block的生命周期就和持有它的对象的生命周期是一样的了,只要block的持有者不释放,block就不会释放,我们就可以安全的使用该block)。

  • copy还用来修饰具有可变子类的不可变类,如NSString,NSArray,NSDictionary等,因为如果不用copy而用strong来修饰,在给不可变类赋值的时候,如果我们把可变类赋值上去了,那么因为使用strong修饰的,所以只是产生了一个新的指针指向可变对象,这样不可变类就会随着可变类的值的改变而改变,这样就不符合我们的设计意图了,容易出问题,而copy修饰的对象则是将对象拷贝了一份产生了一个新对象所以不会出现这样的情况。例子如下:

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickname;

// 可变对象
NSMutableString *tempName = [@"11" mutableCopy];
NSMutableString *tempNickname = [@"12" mutableCopy];

Person *p = [[Person alloc] init];
p.name = tempName;
p.nickname = tempNickname;

NSLog(@"======%@, %@", p.name, p.nickname);

// 修改可变对象
[tempName appendString:@"11"];
[tempNickname appendString:@"12"];

NSLog(@"------%@, %@", p.name, p.nickname);


// 可见用copy修饰数据不会出错,而用strong修饰数据出错了
2018-08-02 15:51:29.524143+0800 Test[1556:429871] ======11, 12
2018-08-02 15:51:29.524350+0800 Test[1556:429871] ------11, 1212

weak:在ARC下,weak和assign有相同的语义,即不会使对象的引用计数加1。但weak的内部实现其实要比assign复杂多了,assign用来修饰基本数据类型,而weak是用来修饰对象类型(一般用来修饰代理对象和timer)。用weak修饰的变量只会弱引用它所指向的对象,所谓弱引用是指weak修饰的变量根本不是通过引用计数和引用计数表来和它所指向的对象建立联系,而是通过weak表来建立联系,不会持有对象,不会使对象的引用计数加1。接下来我们看下weak的内部实现来具体的理解下这句话。

注意:

  • 如果我们直接定义一个weak类型的变量,因为weak变量不会持有它所指向的对象,所以该对象会立马释放,也就是说对象创建出来就释放了,生命周期短到没意义。


  • 所以我们使用weak,一定要修饰经过强引用的对象,这样保证对象存在的前提下,弱引用也才是有意义的存在。


当我们定义了一个weak变量并把它指向一个对象后,其内部所做的操作其实是把该对象的内存地址作为key,weak变量作为value存到了一个weak表中,以此代表形成了弱引用,而和引用计数表没有直接的关系,不会使对象的引用计数加1。那么当对象被释放的时候,就会拿着这个对象的内存地址作为key去weak表中找对应的value,这个value就是我们的weak变量嘛,把这个weak变量置位nil,而weak表一旦检测到某个变量的值为nil,就会从weak表中把它移除,弱引用宣告结束。此外,weak变量所指向的对象会被注册到自动释放池中

// OC代码
__strong id strongObject = [[NSObject alloc] init];
__weak id weakObject = strongObject;
// 内部模拟代码
id weakObject = nil;// 初始化weakObject为0
objc_storeWeak(&weakObject, strongObject);// 把该对象的内存地址作为key,weak变量作为value存到了一个weak表中,以此代表形成了弱引用
objc_storeWeak(&weakObject, nil);// weak表一旦检测到某个变量的值为nil,就会从weak表中把它移除,弱引用宣告结束

strong:在ARC下,strong具有和retain一样的语义,会使对象的引用计数加1,用来修饰对象类型。用strong修饰的变量会强引用它所指向的对象,强引用的意思就很简单了,和retain内部所做的操作一样,strong修饰的变量正是通过引用计数表里的引用计数来与它所指向的对象建立联系的,会持有对象,使对象的引用计数加1strong变量指向的对象不一定会注册到自动释放池中。

这样我们就知道了weak和strong的区别:

  • ARC下,默认所有的变量都是strong语义的,即所有的变量都会强引用它所指向的对象。(而强引用和弱引用都是指,strong或weak的修饰的变量是否会持有它指向的对象,而不是别人是否持有它。然而变量又都是它所属对象的一部分而已,所以变量的释放依赖于它所属对象的释放,因此我们可以把变量仅仅看做是一个引用的媒介,而直接把强引用和弱引用看做是对象之间的事,这样可以方便并简化我们的理解。)

  • weak修饰的变量只会弱引用它所指向的对象,即用weak表来和对象形成联系,不持有对象,不会是对象的引用计数加1;而strong修饰的变量则是强引用它所指向的对象,即通过引用计数表和引用计数来和对象形成联系,会持有对象,会是对象的引用计数加1。

  • weak变量所指向的对象会被注册到自动释放池,而strong修饰的变量不一定会被注册到自动释放池。

3、避免循环引用造成的内存泄漏
  • 使用代理设计模式的时候,用weak修饰代理属性,避免循环引用

    • 我们先来分析一下代理设计模式为什么会造成循环引用:

      以tableView的使用为例:如果tableView的delegate属性用strong来修饰,那么delegate属性就会强引用当前控制器,而delegate属性仅仅是tableView对象的一个属性,它的释放依赖于tableView对象的释放,所以换句话说就是tableView对象强引用着当前控制器对象。同理当前控制器的tableView成员变量也是用strong修饰的,所以它也强引用着它所指向的tableView对象,而tableView成员变量也仅仅是当前控制器的一个成员变量而已,它的释放也依赖于当前控制器的释放,所以换句话说就是当前控制器强引用着tableView对象。这样tableView强引用着当前控制器,当前控制器又强引用着tableView,这就造成了对象的循环引用,最终双方都无法正常的释放内存,导致内存泄漏。

    • 然后再看weak修饰代理属性是如何打破循环引用的:

      了解了循环引用是如何造成的,weak修饰代理属性是如何打破循环引用的其实就很明显了,依旧以tableView的使用为例:用weak修饰了delegate属性之后,delegate属性仅仅是弱引用当前控制器,不会使对象的引用计数加1,所以最后的结果就是tableView弱引用当前控制器,当前控制器强引用tableView,所以就打破了循环引用。而之所以用weak而不用assign修饰代理属性,是因为weak在对象释放之后会自动将代理属性置为nil,避免野指针的出现,而assign不会,所以使用weak更安全一些。

  • 使用block的时候,要避免循环引用,可用__weak和__strong来打破循环引用

    • 使用block的时候为什么会造成循环引用?

      当栈或者堆block在捕获变量后,如果该变量指向的是一个OC对象,并且该对象是strong语义的,那么当block从栈拷贝到堆区的时候,block就会自动持有该对象。所以如果恰好该对象也持有了block的话,就会造成循环引用。

    • __weak是如何打破循环引用的?

      __weak打破循环引用的道理和使用weak修饰符修饰变量是一样的,都是使得变量仅仅对它所指向对象弱引用,不持有对象,不使对象的引用计数加1。

      但是通常我们又会在block内部刚开始执行的时候,对__weak变量使用一下__strong,这是是为了避免block在执行之前self就被释放掉了。这个强引用只是临时的,只是在block的作用域内有效,所以block一执行完的瞬间,强引用就会消失,恢复弱引用,所以是不会再次导致循环引用的。

  • 使用NSTimer的时候,要避免循环引用:因为使用scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:创建timer的时候,timer会持有target,所以如果target也持有了timer,那就会造成循环引用。解决办法详见文章:runLoop的实际应用场景

4、避免抛出异常时造成的内存泄漏

当我们在程序中抛出一个异常的时候,那么异常后面的对象是没有办法释放掉的,这样就容易造成内存泄漏。

然而在ARC下,系统默认是没有为我们生成清理异常后面对象内存的代码的,当然我们可以通过-fobjc-arc-exceptions编译标志来告诉系统帮我们生成,但是这样做会极大的损耗运行时性能,所以还是不建议这么做。

而推荐的做法是只有在出现致命错误的时候再抛出异常,这样出现了致命的错误时,程序就应该终止并退出,而程序都已经退出了,就不存在什么异常后面对象的内存泄漏不泄漏的问题了。

5、在dealloc方法里remove掉观察者,避免向野指针发送消息导致程序崩掉

为什么要remove呢?

我们这里所说的主要是针对iOS9之前的系统,那个时候,我们调用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;在通知中心添加观察者的时候,通知中心对观察者的语义为unsafe_unretained,这个语义和weak差不多,不会持有对象不会使对象的引用计数加1,但是unsafe_unretained在对象释放之后不会把它置为nil,就会出现野指针,而weak会将释放掉的对象置为nil。所以如果我们不移除观察者,通知中心的字典里就还有这个观察者,此时发送一个通知,就是向野指针发送了一个消息,App会崩掉。

而iOS9之后通知中心对观察者引用变成了weak,所以即便我们不移除观察者,App也不会崩掉,因为向nil发送消息只会立马返回,什么也不发生,更好的是系统在dealloc的时候会自动调用- (void)removeObserver:(id)observer;方法帮我们移除掉观察者。

但是我们最好还是在dealloc里面手动移除观察者,因为有些情况系统是没办法帮到我们的,比如使用block的方式在通知中心添加观察者,又比如在分类里面添加观察者,这些情况下都需要我们自己来移除,确保程序的安全。

注意:

dealloc里尽量不要再做清理观察者之外的任何事情。

6、使用自动释放池,避免大次数循环创建大量临时变量,造成的内存暴涨问题,降低内存使用峰值

下面代码有何问题?

for (int i = 0; i < 10000000000; i ++) {
    
    NSString *string = [NSString stringWithFormat:@"%d", I];
    NSLog(@"%@", i);
}

看似好像也没什么问题,但是因为这种大次数的循环内会创建大量的临时变量,而所有的临时变量要等到for循环结束时才会一起释放,所以内存的峰值就会很高,容易造成内存暴涨。

所以我们可以把循环体放在自动释放池中,让系统及时清理掉不需要的临时变量,从而降低内存使用峰值。

for (int i = 0; i < 10000000000; i ++) {
    
    @autoreleasepool {
        
        NSString *string = [NSString stringWithFormat:@"%d", I];
        NSLog(@"%@", i);
    }
}
7、非OC对象,我们需要自己管理其内存

我们知道ARC只是针对OC对象的内存管理方式,所以代码中一些其它框架下的东西或者纯C语言写的东西,还是需要我们手动管理其内存。

四、调试内存问题


  • 采用“僵尸对象”调试内存问题
  • 待补充......

相关文章

  • iOS内存管理详解

    目录 block内存管理 autorelease内存管理 weak对象内存管理 NSString内存管理 new、...

  • 第10章 内存管理和文件操作

    1 内存管理 1.1 内存管理基础 标准内存管理函数堆管理函数虚拟内存管理函数内存映射文件函数 GlobalMem...

  • 操作系统之内存管理

    内存管理 包括内存管理和虚拟内存管理 内存管理包括内存管理概念、交换与覆盖、连续分配管理方式和非连续分配管理方式(...

  • JavaScript —— 内存管理及垃圾回收

    目录 JavaScript内存管理内存为什么需要管理?内存管理概念JavaScript中的内存管理JavaScri...

  • OC - OC的内存管理机制

    导读 一、为什么要进行内存管理 二、内存管理机制 三、内存管理原则 四、MRC手动内存管理 五、ARC自动内存管理...

  • 3. 内存管理

    内存管理 内存管理包含: 物理内存管理; 虚拟内存管理; 两者的映射 除了内存管理模块, 其他都使用虚拟地址(包括...

  • Go语言——内存管理

    Go语言——内存管理 参考: 图解 TCMalloc Golang 内存管理 Go 内存管理 问题 内存碎片:避免...

  • jvm 基础第一节: jvm数据区

    程序内存管理分为手动内存管理和自动内存管理, 而java属于自动内存管理,因此jvm的职能之一就是程序内存管理 j...

  • 内存管理

    内存管理的重要性。 不进行内存管理和错误的内存管理会造成以下问题。 内存泄露 悬挂指针 OC内存模型 内存管理是通...

  • 11-AutoreleasePool实现原理上

    我们都知道iOS的内存管理分为手动内存管理(MRC)和自动内存管理(ARC),但是不管是手动内存管理还是自动内存管...

网友评论

      本文标题:内存管理

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