iOS --- Objective-C中的内存管理

来源:互联网 发布:mac电源适配器坏了 编辑:程序博客网 时间:2024/06/05 08:35

iOS开发中, 之前一直使用swift, 因此对于Objective-C的内存管理机制长期处于混乱的一知半解状态. 今天终于看到一篇讲得透彻的博客Objective-C内存管理教程和原理剖析, 感谢作者.
本文只是个人的一个学习摘要, 更详细内容请参考原文.
ARC黄金法则:

The basic rule to apply is:
Everything that increases the reference counter with alloc, [mutable]copy[WithZone:] or retain is in charge of the corresponding [auto]release.

基本的内存分配

OC的对象是在堆里边生成, 需要一个指针指向它.

ClassA *obj1 = [[ClassA alloc] init];

使用完之后不会自动销毁, 需执行dealloc来销毁, 否则会出现内存泄露.

[obj1 dealloc];

那么看如下代码:

ClassA *obj1 = [[ClassA alloc] init];ClassA *obj2 = obj1;[obj1 hello];[obj1 dealloc];// obj1, obj2都是指针, 指向同一对象. 而[obj1 dealloc]已经销毁了该对象. 因此下边的代码无法执行, obj2是无效指针.[obj2 hello];[obj2 dealloc];

引用计数

为了避免出现上边的无效指针, OC采用引用计数的方式. 引用计数加1的有:alloc, retain, strong, 减1的有release. 当对象的引用计数为0的时候, 会自动调用dealloc.

ClassA *obj1 = [[ClassA alloc] init]; // 计数为1[obj1 release]; // 计数为0, 自动deallocClassA *obj1 = [[ClassA alloc] init]; // 计数为1ClassA *obj2 = obj1; // 计数为1, 指针赋值不会增加引用计数[obj1 hello];[obj1 release]; // 计数为0, 自动dealloc// 因对象已被dealloc, 故以下代码不会执行, obj2仍然是无效指针.[obj2 hello];[obj2 release];

那么如何解决obj2的无效指针问题呢? 通过retain使得引用计数加1.

ClassA *obj1 = [[ClassA alloc] init]; // 计数为1ClassA *obj2 = obj1; // 计数为1[obj2 retain]; // 计数为2[obj1 hello];[obj1 release]; // 计数为1[obj2 hello];[obj2 release]; // 计数为0, 自动dealloc

所以, 引用计数的关键在于当对象的引用计数为0时, 会自动调用dealloc销毁该对象. 指针赋值时, retain count不会自动增加, 需要手动retain.
另外, release一个对象之后, 应立即将指针清空(release一个空指针是合法的). 如:

ClassA *obj1 = [[ClassA alloc] init];[obj1 release];obj1 = nil;

autorelease

自动释放池其实是NSAutoreleasePool, 可以自动释放对象.

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSAutoreleasePool内部包含一个NSMutableArray, 用于保存声明为autorelease的所有对象.
下边看一下Apple的例子:
首先不用autorelease,

- (NSString *)fullName {    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName];    return string;}

则, 该string不能找到合适的时机释放, 因此会出现内存泄露.
那么, 采用autorelease之后,

- (NSString *)fullName {    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName] autorelease];    return string;}

虽然, string不会立即释放, 但autorelease会在其release的时机将其释放.
或者采用更直接简便的方式, 这种方式没有alloc, 所以也不要求release.

- (NSString *)fullName {    NSString *string = [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];    return string;}

其实, 此方式也是通过alloc+autorelease的方式来实现的, 只是对使用者而言, 省掉了alloc+autorelease的步骤.
NSAutoreleasePool自身在销毁的时候, 会遍历并试图release其中的所有autorelease对象. 如果该对象的引用计数为1, 则将其引用计数减1, 销毁该对象; 如果该对象的引用计数大于1, 则autorelease之后其引用计数仍大于0, 对象未销毁, 出现内存泄露.
如在iOS工程的main.m文件中

int main(int argc, char * argv[]) {    @autoreleasepool {        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));   }}

其实可以自行使用NSAutoreleasePool,

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];for (int i=0;i<100;i++) {    for (int j=0;j<10000;j++) {        // 产生autorelease对象        [NSString stringWithFromat:@"1234567890"];    }}[pool release];return 0;

可以看出, 所有autorelease对象都只能在NSAutoreleasePool执行release的时候销毁, 显然不能很好地利用内存. 那么可以使用内嵌的NSAutoreleasePool, 代码优化如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];for (int i=0;i<100;i++) {    NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];    for (int j=0;j<10000;j++) {        // 产生autorelease对象        [NSString stringWithFromat:@"1234567890"];    }    [loopPool release];}[pool release];return 0;

这样, 就可以做到及时地释放内存.
所以, 实例化一个对象可以有两种方法:
1. 采用alloc + release的方法

Dog *dog = [[Dog alloc] init];// 代码[dog release]; // alloc 和 release 必须一一对应.
  1. 采用autorelease的方法
// 这种方式把dog对象加到autorelease pool中, 就不需要手动release了.+ (id)dog {    return [[[Dog alloc] init] autorelease];}

所以, NSAutoreleasePool的关键在于, 放置到其中的autorelease对象, 会在该pool release的时候自动销毁(绝大多数情况下, 都是正常的引用计数为1的autorelease对象). 因此对象本身就没必要调用release方法. 同时NSAutoreleasePool的release时机很重要. 因为autorelease pool不会立即释放, 当它到了最近的pool release的时候才会自动检测retainCount是否为0, 一旦为0则释放pool. 当pool本身销毁的时候, 其中的对象才会执行release操作, 否则将一直占用内存空间.
所以, 不要滥用autorelease, 如对象的生命周期很清晰, 则结束使用后立即release. 过多等待autorelease对象浪费内存.

property中的关键字

@property中的关键字表示属性如何存储, 其实全都是用于设置getter/setter的操作特性, 如果我们将getter/setter展开来看的话就会很明白.

assign

assign相当于指针赋值, 不对引用计数进行操作, 如原对象不用了, 一定要将其设置为nil. 一般基本变量用该属性声明, 如int, BOOL.
即: 调用setter方法时直接赋值, 不进行retain操作. 不改变引用计数.
assign与weak的区别在于:

当指针所指向对象release之后, weak会赋nil给该指针, 而assign不会赋nil给指针. 所以使用assign的时候, 一旦指针所指向对象release之后, 再给该对象发送消息(调用方法), 程序即会crash.
weak用于OC对象的弱引用, assign用于C原始类型.

retain

retain其实是指针拷贝(地址相同), 会增加对象的引用计数,而OC中的基本数据类型没有引用计数。
如: 把对象添加到数组中[array addObject:obj];,该对象obj的引用计数retainCount将加1. 如下图
NSArray retain会增加对象的引用计数
使用retain, 调用setter方法时, 先release旧值, 对赋予的新值进行引用计数+1. 二者的内存地址一样.
看下边的例子:

// Person.h@interface Person: NSObject {    Dog *_dog;}@property (retain) Dog *dog;@end
// Person.m@implementation Person@synthesize dog = _dog;- (void) dealloc {    self.dog = nil; // 其实调用[self setDog:nil];方法    [super dealloc];}@end

参考之前的一篇文章Objective-C中类的成员变量与属性, 可知@property与@synthesize的关系, 总之, 这里编译器会识别@synthesize, 并自动为dog属性添加getter/setter方法.
编译器对 @property (retain) Dog *dog; 展开为:

- (Dog *)dog;- (void)setDog:(Dog *)aDog;

编译器对 @synthesize dog = _dog; 展开为:

- (Dog *)dog {    return _dog;}- (void)setDog:(Dog *)aDog {    if (_dog != aDog) {        [_dog release]; // 即使_dog的retainCount已为0, 对其做release也不会出错.        _dog = [aDog retain];    }}

getter/setter方法是被编译器隐藏的, 如果不使用@synthesize dog = _dog;的话, 必须自己编写这两个方法.

copy

不同于retain的指针拷贝, copy都是内容拷贝. 即复制一个对象变成新的对象(新的内存地址), 新对象的引用计数为1, 原对象的引用计数不变. 所以, copy得到的均为一个独立的对象, 跟原对象没有关系.
一般来说, block都是使用copy关键字. 如下边的SelectedCity的使用, 在这里只是简单地截取部分代码, 不做太具体的解释. 需要详细了解block的同学, 可以参考转: 深入理解Objective-C的Block.
在CityListViewController中

// CityListViewController.htypedef void(^SelectedCity)(NSString *);@interface CityListViewController: UIViewController@property (nonatomic, copy) SelectedCity selectedCity;@end// CityListViewController.mif (_selectedCity) {    _selectedCity(cell.textLabel.text);}

在MainViewController中:

// MainViewController.htypedef void(^SelectedCity)(NSString *);@interface MainViewController: UIVieController@property (nonatomic, copy) SelectedCity selectedCity;@end// MainViewController.m- (void)viewDidLoad {    __weak MainViewController *weakVC = self;    _selectedCity = ^(NSString *city) {        weakVC.cityLabel.text = city;        weakVC.vTracks.selectedCity = city; // vTracks中有selectedCity属性        [weakVC.vTracks invokeRefresh];    };}- (IBAction)cityListClicked:(UIButton *sender) {    CityListViewController *cityListVC = [CityListViewController alloc] init];    cityListVC.selectedCity = _selectedCity;    [self.navigationController pushViewController:cityListVC animated:YES];}

编译器对 @property (copy) NSString *str; 展开为

- (NSString *)str {    return _str;}- (void)setStr:(NSString *)newStr {    if (_str != newStr) {        [_str release];        _str = [newStr copy];    }}

copy其实有两种:
1. copy得到的是不可变类型, 如NSString, NSArray, NSDictionary, NSData, NSSet.
2. mutableCopy得到的是mutable类型. 如NSMutableString, NSMutableArray, NSMutableDictionary, NSMutableData, NSMutableSet.
copy操作必须遵循NSCopying和NSMutableCopying协议, 实现其中的copyWithZone:和mutableCopyWithZone:方法, 即告诉编译器如何做到copy操作.

- (id)copyWithZone:(NSZone *)zone {    Dog *dog = [[[[self class] allocWithZone:zone] init] autorelease];    dog.name = self.name;    dog.year = self.year;    return dog;}

deep copy

上边的copy默认都是浅拷贝, 如对于NSArray而言, 浅拷贝只是copy一份NSArray, 该NSArray中的每一个元素仍然指向原NSArray中指向的对象. 如下图:
浅拷贝
而deep copy则将NSArray中元素指向的对象也做一份拷贝, 一般用于NSArray, NSDictionary. 如下图:
deep copy
deep copy有两种实现方式:
1. 使用copyItems
copyItems

- (id)initWithArray:(NSArray *)array copyItems:(BOOL)flag;- (id)initWithDictionary:(NSDictionary *)otherDictionary copyItems:(BOOL)flag;

使用如下:

NSMutableArray *_carList = [[NSMutableArray alloc] init];// 向_carList中填充内容NSMutableArray *carList1 = [[NSMutableArray alloc] initWithArray:_carList];// 浅拷贝, 同样会改变原_carList中的内容[[carList1 objectAtIndex:0] setName:@"浅拷贝内容"];NSMutableArray *carList2 = [[NSMutableArray alloc] initWithArray:_carList copyItems:YES];// deep copy, 则不会改变原_carList中的内容[[carList2 objectAtIndex:0] setName:@"deep copy内容"];

2. 使用归档NSKeyedArchiever
归档是将整个数组以二进制形式存到NSData中, 需要的时候就将其读取出来再转换成原来数组的形式.

// 将NSArray的内容保存到NSDataNSData *data = [NSKeyedArchiver archivedDataWithRootObject:_carList];// 将二进制对象data还原成NSArrayNSMutableArray *carList3 = NSKeyedArchiver unarchiveObjectWithData:data]; 

归档需要继承NSCoding协议, 实现encodeWithCoder:和initWithCoder:两个方法.

// 存档方法- (void)encodeWithCoder:(NSCoder *)aCoder {    [aCoder encodeObject:self.name forKey:@"name"];    [aCoder encodeObject:self.year forKey:@"year"];}// 解档方法- (id)initWithCoder:(NSCoder *)aDecoder {    self = [super init];    if (self) {        self.name = [aDecoder decodeObjectForKey:@"name"];        self.year = [aDecoder decodeIntForKey:@"year"];    }    return self;}

strong

strong是强引用, 持有对象, 对象的引用计数+1, 如string1和string2都指向一个字符串, 则string1=nil, 而string2不变.
所有strong修饰符的变量在超出其变量作用域时, 释放其被赋予的对象. 即持有强引用的变量在超出其作用域时被放弃, 随着强引用的失效, 引用的对象会随之释放.
strong变量执行ARC计数, 不会自动释放. 其死亡直接决定了所指向对象的死亡.
为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。哪怕实例的引用数为1,ARC都不会销毁这个实例。
为了使之成为可能,无论你将实例赋值给属性,常量或者是变量,属性,常量或者变量,都会对此实例创建强引用。之所以称之为强引用,是因为它会将实例牢牢的保持住,只要强引用还在,实例是不允许被销毁的。
实际上, strong和retain的意思相同, 推荐使用strong代替retain.
id类型和对象类型的所有权修饰符默认为strong, 会影响对象回收

weak

使用strong容易引起循环引用的问题. 为了防止循环强引用,可采用弱引用和无主引用。这两种允许循环引用中的一个实例引用另外一个实例而不保持强引用,这样实例能够相互引用而不产生循环强引用。
弱引用 :对于生命周期中会变为nil的实例采用。
无主引用:对于初始化赋值后再也不会变为nil的实例采用。
weak是弱引用, 不持有对象, 在超出其变量作用域时, 对象即被释放. 在持有某对象的弱引用时, 若该对象被释放, 则此弱引用将自动失效且等于nil.
对象的计数不变. 如string1和string2都指向一个字符串, 则string1=nil, 那么string2也会变为nil.
因其没有retain内存地址, 若其指向的地址对象一旦释放, 则该指针会指向nil.
ARC空闲时释放, 对象释放时自动将指针置NULL. 不决定对象的存亡, 即使一个对象被持有无数个弱引用, 只要没有强引用指向它, 则最后还是会清除
如两个对象互相为对方的成员变量, 则一定不能同时retain, 否则dealloc函数形成死锁, 两个对象都无法释放. weak可以用于防止野指针和死锁, 避免循环引用.
ARC机制中,为什么UI对象用weak,代理用weak?比如我们往view里加一个button,相当于在subviews这个数组里加一个button,这个数组空间里会有一块内存指向button,是强指针,只要控制器还在,button就在,所以当创建button时用 weak,防止内存泄漏。代理如果是强指针的话,它指向控制器,那么所有东西都不能relese了,前面道理同UI控件。
weak不会影响对象回收, 一般UI对象和delegate用weak

__weak

__weak 声明了可以自动nil化的弱引用.
注意: block被copy时, 会对block中用到的对象产生强引用(引用计数+1). 如果block中又引用了对象的其他成员变量, 就会对该变量本身产生强引用, 则变量本身和它自己的block属性就形成了循环引用. 即使用self之类的有可能导致retain cycle, 所以一般用__weak的方式.
请看下边的城市列表例子.
在CityListVC中, 通过block将city的值传递给MainVC.
在CityListVC.h中: 定义block块对象

typedef void(^SelectedCity)(NSString *);@property(copy,nonatomic) SelectedCity selectCity;CityListVC.m中:if (_selectCity) {  _selectCity(cell.textLabel.text); // 这里要通过block进行View之间的数据传递.}

MainVC.h中:

typedef void(^SelectedCity)(NSString *);@property(copy,nonatomic) SelectedCity selectCity;

MainVC.m中:

    self.vTracks.selectedCity = @"城市";    __weak MainViewController *weakVC = self;    _selectCity = ^(NSString *city){        weakVC.cityLabel.text = ([city isEqualToString:@"全国"]) ? @"城市" : city;        weakVC.vTracks.selectedCity = ([city isEqualToString:@"城市"] | [city isEqualToString:@"全国"]) ? nil : city;        [weakVC.vTracks invokeRefresh];    };

在block中, 使用__weak的方式声明了一个weakVC, 即对自身对象的弱引用. 即block对象不对self对象进行retain, 弱引用的方式可以避免出现循环引用. 如果是non-ARC, 则使用__block替换__weak, 即在block中引入一个新的结构体成员变量指向这个__block变量, __block typeof(self) weakSelf = self.

static

static, 全局变量.
1. 函数体内static变量的作用范围为该函数体, 该变量只分配一次, 其值在下次调用的时候仍维持上次的值.
2. 模块内的static可被模块内所有函数访问, 但不能被模块外其他函数访问.
3. 类中的static成员变量可以视作类变量, 跟对象实例无关, 属于整个类所有, 对类的所有对象只有一份拷贝. 已经实例化的各个对象之间的该static变量不会相互影响, 但对其的改变会影响到class本身. 若再次实例化新的对象, 则该新对象就拥有最新的static类变量.
4. 类中的static成员函数属于整个类所有, 该函数不接收this指针, 只能访问类的static成员变量.

const

const, 常量不能修改. 超出作用域后会释放. 即const对于对象生存期内是常量, 对于整个类而言可变.
1. 可以指定指针, 指针所指向数据, 或二者都为const.
2. 函数中, const修饰形参, 则该输入参数在函数内部不能修改.
3. 类的成员函数若指定const, 则为常函数, 不能修改类的成员变量. (常见于返回一个const值的函数)

extern

extern: 该模块中的变量和函数可在其他模块中使用.

读写权限

readwrite, readonly: 控制成员变量的访问权限, 对setter/getter的作用.

原子操作

nonatomic: 非原子性访问, 不加同步, 多线程并发访问.
atomic 线程保护. 互斥锁.

总结

assign: 一般用于int, BOOL等基本变量类型, 防止循环引用.

weak: 一般用于storyboard或xib中的UI对象和delegate对象.
delegate对象使用weak是为了防止循环引用。

assign与weak区别:当对象被释放后,weak会自动将指针指向nil,而assign不会。
所以向nil发送消息导致野指针错误unrecognized selector sent to instance。防止野指针

strong: 一般用于id类型(非delegate)和对象类型(NSString, UITableView等). 默认, 对应于retain.
如果代码写的UI对象(使用alloc+init),要使用strong,否则可能在release的时候出如下警告:
Assigning retained object to weak variable; object will be released after assignment.
而如果是IB拉过来的UI对象,会自动设置为weak。

_unsafe_unretained与weak功能一致, 区别在于当指向的对象销毁之后, weak将变量置为nil, 防止调用野指针.

block一般用copy。block一般使用copy关键之进行修饰,block使用copy是从MRC遗留下来的“传统”,在MRC中,方法内容的block是在栈区的,使用copy可以把它放到堆区。但在ARC中写不写都行:编译器自动对block进行了copy操作。

copy:用@property声明 NSString、NSArray、NSDictionary 经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为NSString时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

循环引用:
typeof (&*self) __weak weakSelf = self;

autorelease:在retainCount为1时,不会继续减一。而是标记为需释放,准确的释放时机暂不明确,可能与runloop有关。

1 0
原创粉丝点击