之前我们学习过C语言的内存剖析,对于iOS移动设备开发来说,内存是极其有限的,因此管理好内存是相当重要的当移动设备的程序占用太多内存无法释放,有可能就会导致我们平时经常会遇到的闪退现象,这时就需要回收一些不需要再使用的内存空间,比如不需要使用的对象或者变量。
管理范围:任何继承NSObject的对象,对其他的基本数据类型(int、char、float、double、struct、enum等)无效。
本质原因是因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。
第一讲 引用计数器和set方法的内存管理
一、 引用计数器
1. 引用计数器的概念
*每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使用这个对象。
*对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。
*在每个OC对象内部,都专门有4个字节的存储空间来存储引用计数器。
2. 引用计数器的作用
当使用alloc、new或者copy创建一个新对象时,新对象的引用计数器默认就是1
当一个对象的引用计数器值为0时,对象占用的内存就会被系统回收。换句话说,如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收,除非整个程序已经退出
3. 引用计数器的操作
*给对象发送一条retain消息,可以使引用计数器值+1(retain方法返回对象本身)
*给对象发送一条release消息,可以使引用计数器值-1
*可以给对象发送retainCount消息获得当前的引用计数器值
4. 概念
僵尸对象:所占用内存已经被回收的对象,僵尸对象不能再使用
野指针:指向僵尸对象(不可用内存)的指针,给野指针发送消息会报错(EXC_BAD_ACCESS)
空指针:没有指向任何东西的指针(存储的东西是nil,NULL,0),给空指针发送消息不会报错
两个常见的报错:
报错1:-[Person setAge:]: message sent to deallocated instance 0x1002067b0
// 给已经释放的对象发送了一条-setAge:消息
报错2:EXC_BAD_ACCESS:访问了一块坏的内存(已经被回收,已经不可用的内存)
// 野指针错误
// OC中不存在空指针错误,给空指针发送消息,不报错
5. 对象的销毁
*当一个对象的引用计数器值为0时,那么它将被销毁,其占用的内存被系统回收
*当一个对象被销毁时,系统会自动向对象发送一条dealloc消息
*一般会重写dealloc方法,在这里释放相关资源,dealloc就像对象的遗言
*一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用
*不要直接调用dealloc方法
*一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)
// 当一个Person对象被回收的时候,就会自动调用这个方法
- (void)dealloc
{
NSLog(@"Person对象被回收");
// super的dealloc一定要调用,而且要放在最后面
[super dealloc];
}
二、 set方法的内存管理
1. 内存管理原则
原则分析:只要还有人在用某个对象,那么这个对象就不会被回收
只要你想用这个对象,就让对象的计数器+1
当你不再使用这个对象时,就让对象的计数器-1
*你想使用(占用)某个对象,就应该让对象的计数器+1(让对象做一次retain操作)
*你不想再使用(占用)某个对象,就应该让对象的计数器-1(让对象做一次release)
*谁retain,谁release:只要调用了retain,无论这个对象是如何生成的,都要调用relesae
*谁alloc,谁release:如果你通过alloc、new或[mutable]copy来创建一个对象,那么你必须调用release或autorelease
换句话说,不是你创建的,就不用你去[auto]release
总结:有始有终,有加就有减
曾经让对象的计数器+1,就必须在最后让对象计数器-1
2. 内存管理代码规范
如果你有个OC对象类型的成员变量,就必须管理这个成员变量的内存。比如有个Car *_car
内存管理代码规范:
(1) 只要调用了alloc,必须有release(autorelease),如果对象不是通过alloc产生的就不需要release
(2)set方法的代码规范
1> 基本数据类型:直接赋值
- (void)setAge:(int)age
{
_age = age;
}
2> OC对象类型
- (void)setCar:(Car *)car
{
1.先判断是不是新传进来的对象
if(car != _car)
{
2. 对旧对象做一次release
[_car release];
3. 对新对象做一次retain
_car = [car retain];
}
}
3. dealloc方法的代码规范
1>一定要[super dealloc] ,而且放到最后面
2>对self(当前)对象所拥有的其他对象做一次release
- (void)dealloc
{
[_car release];
[super dealloc];
}
第二讲 @property参数和循环引用
一、 @property参数
1. 内存管理相关的参数
* retain:release旧值,retain新值(适用于OC对象类型)
* assign:直接赋值(默认的,适用于非OC对象类型),也就是基本数据类型的赋值默认有个assign参数
* copy:release旧值,copy新值
2. 是否要生成set方法
* readwrite:同时生成setter和getter的声明跟实现
* readonly:只会生成getter的声明跟实现
3. 多线程管理
* nonatomic:性能高(一般就用这个)
* atomic:性能低(默认)
4. setter和getter方法的名称
* setter:决定了set方法的名称,一定要有个冒号:
* getter:决定了get方法的名称(一般用在BOOL类型的get方法)
1 #import <Foundation/Foundation.h>
2 @interface Person : NSObject
3 // 只读,类型不冲突的参数可以同时写,比如readonly, assign,但是readonly, readwrite不能同时写
4 @property (readonly, assign) int height;
5
6 @property (getter = abc, setter = setAbc:) int age; // 注意这里set方法要加上:
7
8 // 返回BOOL类型的方法名一般以is开头
9 @property (getter = isRich) BOOL rich;
10
11 @end
5. 模型设计的练习
设计微博用户,属性包括:姓名、微博号码、密码、头像、性别、手机、生日
User类的声明:
// 微博用户:姓名、微博号码、密码、头像、性别、手机、生日
#import <Foundation/Foundation.h>
typedef enum // 性别用枚举类型
{
SexMan, // 男
SexWoman // 女
} Sex;
typedef struct // 生日用结构体类型
{
int year;
int month;
int day;
} Date;
@interface User : NSObject
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *account;
@property (nonatomic, retain) NSString *password;
@property (nonatomic, retain) NSString *icon; // 头像一般传的是资源地址
@property (nonatomic, assign) Sex sex;
@property (nonatomic, retain) NSString *phone;
@property (nonatomic, assign) Date birthday;
@end
User类的实现:(主要考虑内存管理需要release)
#import "User.h"
@implementation User
- (void)dealloc
{
[_name release];
[_account release];
[_icon release];
[_password release];
[_phone release];
[super dealloc];
}
@end
二、 循环引用
1. @class
使用场景:两个类相互依赖时,person引用idcard,idcard引用person,如果使用#import,编译就会报错
这时就需要使用@class。@class 类名 仅仅告诉编译器这是一个类。提高性能。
2. @class和#import的区别
(1)#import方式会包含被引用类的所有信息,包括被引用类的变量和方法;@class方式只是告诉编译器在A.h文件中 B *b 只是类的声明,具体这个类里有什么信息,这里不需要知道,等实现文件中真正要用到时,才会真正去查看B类中信息
(2)如果有上百个头文件都#import了同一个文件,或者这些文件依次被#improt,那么一旦最开始的头文件稍有改动,后面引用到这个文件的所有类都需要重新编译一遍,这样的效率也是可想而知的,而相对来讲,使用@class方式就不会出现这种问题了
(3)在.m实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import方式引入被引用类
3. 开发中引用一个类的规范
1> 在.h文件中用@class来声明类
2> 在.m实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import方式引入被引用类
4. 循环retain
*比如A对象retain了B对象,B对象retain了A对象
*这样会导致A对象和B对象永远无法释放
5. 解决方案
当两端互相引用时,应该一端用retain,一端用assign。使用assign的在dealloc中也不用再release。
第三讲 autorelease和ARC基本使用
一、 autorelease
1. autorelease基本用法
1>会将对象放到一个自动释放池中
2>当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
3>autorelease方法会返回对象本身
4>调用完autorelease方法后,对象的计数器不变
5>autorelease实际上只是把对release的调用延迟了,对于每一次autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release
2. autorelease的好处
1>不用再关心对象释放的时间
2>不用再关心什么时候调用release
3. autorelease的使用注意
1>占用内存较大的对象不要随便使用autorelease,不能精确控制对象释放的时间
2>占用内存较小的对象使用autorelease,没有太大影响
4. 错误写法
1>alloc之后调用了autorelease,又调用了release
@autoreleasepool
{
Person *p2 = [[[Person alloc] init] autorelease];
[p release];
}
2>连续调用多次autorelease
@autoreleasepool
{
Person *p2 = [[[[Person alloc] init] autorelease] autorelease];
}
5. 自动释放池
1>在IOS程序运行过程中,会创建无数个池子,这些池子都是以栈结构存在(先进后出)
2>当一个对象调用autorelease方法时,会将这个对象放到栈顶的释放池
6. 自动释放池的创建方式
1> iOS 5.0前
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Person *pp = [[[Person alloc] init] autorelease];
[pool release]; // [pool drain];
2> iOS 5.0开始
@autoreleasepool
{
}
自动释放池大括号一开始代表创建了释放池,大括号结束代表销毁了自动释放池。
7. autorelease的应用
系统自带的方法里面没有包含alloc、new、copy,说明返回的对象都是autorelease的
开发中经常会提供一些类方法,快速创建一个已经autorelease过的对象
1>创建对象时不要直接用类名,一般用self,能满足子类的需求
+ (id)person
{
// 此处改用self能满足子类的需求
return [[[self alloc] init] autorelease]; // 将autorelease封装成一个类方法
}
2>以下对象都是autorelease的,不需要再release
NSNumber *n = [NSNumber numberWithInt:100];
NSString *s = [NSString stringWithFormat:@"jack"];
NSString *s2 = @"rose";
在实际应用中,可以把alloc和init方法封装起来,这样在外部函数就看不到内部的结构和方法,下面举个例子:
Person类的声明:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) int age;
+ (id)person; // 类方法,返回一个对象
+ (id)personWithAge:(int)age; // 返回一个带age参数的对象
@end
Person类的实现:
1 #import "Person.h"
2 @implementation Person
3 + (id)person
4 {
5 // 此处改用self能满足子类的需求
6 return [[[self alloc] init] autorelease]; // 将autorelease封装成一个类方法
7 }
8
9 + (id)personWithAge:(int)age
10 {
11 // 此处改用self能满足子类的需求
12 Person *p = [[[self alloc] init] autorelease];
13 //Person *p = [[[Person alloc] init] autorelease];//[Person person]
14 p.age = age;
15 return p;
16 }
17 - (void)dealloc
18 {
19 NSLog(@"%d岁的人被销毁", _age);
20 [super dealloc];
21 }
22
23 @end
二、 ARC的基本使用
1. ARC基本简介
*ARC是自iOS 5之后增加的新特性,完全消除了手动管理内存的烦琐,编译器会自动在适当的地方插入适当的retain、release、autorelease语句。
你不再需要担心内存管理,因为编译器为你处理了一切
*ARC 是编译器特性,而不是 iOS 运行时特性,它也不是类似于其它语言中的垃圾收集器。
因此 ARC 和手动内存管理性能是一样的,有时还能更加快速,因为编译器还可以执行某些优化
2. ARC基本原理
ARC的判断准则:只要还有一个强指针变量指向对象,对象就会保持在内存中,只要没有强指针指向对象,就会释放对象
指针分两种:
1>强指针:默认情况下,所有的指针都是强指针 __strong 一般情况可以不写
2>弱指针:__weak 只要弱指针指向的对象不在就会把弱指针变为空指针
3. ARC特点总结
1>不允许调用release、retain、retainCount
2>允许重写dealloc,但是不允许调用[super dealloc]
3>@property的参数
*strong:成员变量是强指针(适用于OC对象类型)
*weak:成员变量是弱指针(适用于OC对象类型)
*assign:适用于非OC对象类型
4>以前的retain改为用strong
注意一个错误写法:
__weak Person *p = [[Person alloc] init]; 错误写法(没有意义)
4. ARC的循环引用
当两端循环引用时,解决方案:
1>ARC
一端用strong,一端用weak
2>非ARC
一端用retain,一端用assign
本章学习总结:
内存管理在整个OC的学习中都是非常重要的,首先要明白为什么要进行内存管理,内存管理的作用和意义是什么。苹果官方对内存管理做的真的是非常严谨的,根据内存管理原则去管理内存就不会出现内存泄漏的问题。而且如果利用ARC更加方便了开发者,虽然有了ARC之后可能不用太多去考虑内存的问题,但是在非ARC情况下的内存管理还是必须要掌握,只有掌握了内存管理的基本过程才能更好的利用ARC也才明白开发执行过程中起始和销毁。所以本章的知识需要多加领会练习。
本章主要分三讲内容,第一讲主要学习了引用计数器,包括引用计数器的作用和操作,retain计数器+1,release计数器-1,retainCount获得当前计数器的数值,刚创建的对象默认计数器为一,当计数器为零时对象销毁。此外还学习了set方法的内存管理,内存管理原则和set方法内存管理的代码规范需要熟记。第二讲主要学习了@property参数和循环引用,@property目前较常用的有retain、assign、nonatomic、readwrite、readonly等,要掌握这些参数分别的作用和使用场合,此外还学习了循环引用,要明白什么是循环引用,区分@class和 #import的作用和使用场合,要掌握循环retain的解决方案。本章第三讲主要讲述了autorelease和ARC,要掌握autorelease的好处和使用注意,明白自动释放池的工作原理。此外还学习了ARC,要掌握ARC的基本原理和特点,掌握强指针和弱指针的概念和使用,要学会怎么解决ARC的循环引用,跟非ARC的循环引用做一个对比。