深入理解内存管理(一)

目录

  • 1.内存五大分区
  • 2.iOS内存管理
    • 1.MRC
    • 2.ARC
  • 3.MRC的细节
    • 1.存取方法
    • 3.不要在初始化或dealloc方法中调用存取方法
  • 4.ARC
    • 1.strong
    • 2.weak
    • 3.autorelease
  • 5.内存泄漏
    • 1.循环引用
      • 1.delegate
      • 2.block
      • 3.timer
    • 2.野指针、僵尸对象
  • 6.垃圾回收
    • 1.引用计数方式
    • 2.GC大一统理论

一、内存五大分区

  • 栈(stack)区:存放程序运行创建的局部变量,存放函数调用的信息及其参数值等。内存由系统管理
  • 堆(heap)区:存放程序运行中动态分配的内存,内存由程序员管理
  • 全局区/静态区:定义在函数外边的全局变量和静态变量存放在这里。
  • 常量存储区:存放常量,如字符串常量。
  • 代码区:存放程序代码。

二、iOS内存管理

iOS的内存管理是基于引用计数实现的。在cocoa的术语中也叫做对象的所有权(ownership)

基本内存管理规则:

  • 1.你拥有你所创建的任何对象:使用 “alloc”, “new”, “copy”, “mutableCopy”开头的方法来创建一个对象。
  • 2.可以使用 retain 保留对象的所有权
  • 3.当你不再需要它,你必须放弃你自己的对象的所有权:您可以通过发送一个 release 消息或 autorelease 消息放弃对象的所有权。
  • 4.对于你不拥有的对象,不要尝试放弃所有权:这条规则是以上规则的推论,为了强调其重要性,在这里明确列出来。

简单来说就是:

  • 1.谁拥有,谁释放;不是自己拥有的不能释放;
  • 2.当对象不再被需要时,需要主动释放。

1.MRC

即手动引用计数管理,开发者需要手动管理对象的引用计数。MRC中对象内存管理操作对应的OC方法有四种:

2.ARC

即自动引用计数管理,开发者只需设置好对象的内存管理语义,编译器和runtime会负责对象的引用计数管理,比如说:

  • 编译器会在适当的地方插入retian、release、autorelease方法,配合autoreleasepool使用。
  • 运行时系统会在运行过程中将weak指针放入到对象的weak表中;runloop会在休眠前释放掉放在自动释放池里的对象。

内存管理语义:

  • __strong:对变量进行强引用,变量引用计数+1。
  • __weak:对变量进行弱引用,不增加变量的引用计数。且当变量被回收时,weak指针会被置为nil。其主要用途就是避免ARC环境下的循环引用。
  • __autoreleasing
  • __unsafe_unretained

三、MRC细节

一个简单的例子说明上述规则:

{
    Person *aPerson = [[Person alloc] init];
    // ...
    NSString *name = aPerson.fullName;
    // ...
    [aPerson release];
}

1.存取方法

@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;
- (NSNumber *)count {
    return _count;
}
- (void)setCount:(NSNumber *)newCount {
    [newCount retain];
    [_count release];
    _count = newCount;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。

2.不要在初始化或dealloc方法中调用存取方法

初始化和dealloc方法中是唯一不应该使用存取方法的地方,使用一个number对 象来将counter初始化为0,你需要这样实现初始化方法:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

由于Counter类有一个实例变量,你也必须实现dealloc方法。这个方法中必须释 放实例变量的所有权,并且,最后还要调用基类的dealloc方法:

- (void)dealloc {
    [_count release];
    [super dealloc];
}

四、ARC细节

1.strong(默认)

使用strong修饰的对象,在其作用域结束后 / 重新赋值 / 置为nil,会被release一次。

@interface HJPerson : NSObject
@end
@implementation HJPerson
- (void)dealloc{
    NSLog(@"%s", __func__);
}
@end

int main() {
    NSLog(@"111");
    {
        HJPerson* person = [[HJPerson alloc]init];
    }
    NSLog(@"222");
}

在作用域结束后,person对象会被release,然后触发dealloc。

2.weak

NSString* __weak string = [[NSString alloc] initWithFormat:@"my age = %d", 26];
NSLog(@"%@", string);

虽然 string是在初始化赋值之后使用,但是在赋值的时候并没有其它强引用指向字符串对象;因此字符串对象会马上释放掉。log语句显示 stirng的值为 null。

在MRC下的实现大概如下:

NSString* temp = [[NSString alloc] initWithFormat:@"my age = %d", 26];
NSString* __weak string = temp;
[temp release];

Q:把weak对象放到NSArray中引用计数会增加吗?

会!weak变量不增加原对象的引用计数,即编译器只是简单不会增加retain语句。但是weak变量指向的还是原对象,放到NSArray会对原对象retain。

3.autorelease

autorelease的核心作用:延迟release。通常用在作为函数返回值时。

+ (instancetype)person{
    HJPerson* p = [[HJPerson alloc]init];
    return p;
}

在ARC下,超过大括号后,p会被清除,HJPerson对象就因为没有强引用指向而被释放,所以编译器会加上autorelease,使其延迟释放。如下:

+ (instancetype)person{
    HJPerson* p = [[HJPerson alloc]init];
    return [p autorelease];
}

五、内存泄漏

iOS内存泄漏的核心问题是循环引用。使用ARC的垃圾回收机制,虽然可以帮助开发者在大部分情况下正确管理内存,但是对于循环引用束手无策。

循环引用导致内存泄漏的原因在于:

  • 一个对象在引用计数降为0时,才会对其内部属性发送release;同样,其内部属性只有在接收了release消息时,才会对它持有的对象进行释放。
  • 在这种循环引用的情况下,该对象及其属性不能在超出作用域时正常的被释放,所以造成了内存泄漏

1.循环引用

循环引用有三种典型例子delegate、block、timer,它们的共性就是:对象内的某个属性再次对当前对象进行强引用,则会发生强引用

(1)delegate

演示:self.myView.delegate = self;
解决方法:使用weak修饰符

(2)block

演示:person.block = ^{NSLog(@"%d", person.age);};
解决方法:使用__weak声明的weakSelf

(3)timer

演示:self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

注意weakSelf不能解决问题:就算是用weakSelf,timer内部还是会直接对weakSelf指向的对象强引用。就像把weak对象放进数组中,还是会对weak对象进行强引用。

  • block使用weakSelf可以解决的原因在于:block捕获对象时会带着它的内存管理语义一起捕获。

解决方式1:使用block+weakSelf

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    [weakSelf test];
}];

解决方式2:使用代理对象(NSProxy)

代理对象将消息转发给viewController。

2.野指针、僵尸对象

当一个对象被释放后,如果其指针没有置空,则这个指针就变成了野指针

野指针指向的就是僵尸对象

六、垃圾回收

1.引用计数算法

优点:是GC算法中最简单也最容易实现的。

缺点:

  • 1.无法释放循环引用的对象;
  • 2.必须在引用发生增减时对引用计数做出正确的增减;
  • 3.引用计数管理不适合并行处理;

2.GC大一统理论

像标记清除和复制收集之类的算法是从根开始扫描以判断对象生死的算法,被称为跟踪回收(Tracing GC)。而引用计数算法则是当对象之间的引用关系发生变化时,通过对引用计数进行更新来判定对象生死。

2004年IBM研究中心发表了一篇论文,提出了一个理论:任何一种GC算法都是跟踪回收和引用计数两种方式的组合,两者的关系正如“物质”和“反物质”一样,是相互对立的。对其中一方进行改善的技术之中,必然存在对另一方进行改善的技术,而其结果只是两者的组合而已。

你可能感兴趣的:(深入理解内存管理(一))