资讯详情

内存管理(一)MRC

内存管理(1)MRC

管的谁

在Objective-C创建的对象分布在堆区,内存管理也针对这个区域。

引用计数

Objective-C事实上,内存管理的核心是引用计数和引用计数(Reference Count)管理对象生命周期是一种简单有效的方式。当我们创建新对象时,它的引用计数是 1.当有一个新的指针指向这个对象时,我们引用计数加 1.当指针不再指向对象时,我们引用计数减少 1.当对象的引用计数变为 0 这意味着对象不再被任何指针指向。此时,我们可以销毁对象并回收内存

Objective-C有两种内存管理机制:手动管理(MRC)和自动管理(ARC)。目前基本都是用来开发的ARC。最开始学习iOS也用过MRC,先介绍下MRC的机制。

MRC

操作对象的四种方式:

  • 生成并持有对象:alloc/new/copy/mutableCopy等, retainCount : 1
  • 持有对象:retain,retainCount : 1
  • 释放对象:release,retainCount :-1
  • 废弃对象:dealloc, 内存自动释放

内存管理的四条规则:

  • 拥有自己生成的对象
  • 你也可以持有非自己生成的对象
  • 当你持有对象时,你不再需要释放对象
  • 非自己持有的对象无法释放

示例代码

拥有自己生成的对象:

以 alloc/new/copy/mutableCopy 该方法创建的对象归调用者持有

- (void)test1 { 
             id obj0 = [NSObject alloc]; // 创建一个NSObject对象返回给变量obj, 并归调用者持有     NSLog(@"obj0 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj0)); } 

输出

obj0 引用计数1 

分析 创建一个NSObject对象返回给变量obj, 并归调用者持有 alloc创建对象的过程取决于我的文章

也可以持有非自己生成的对象:

alloc/new/copy/mutableCopy 以外方式创建的对象不归调用者持有

- (void)test2 { 
             id obj = [NSMutableArray array];     id obj2 = [obj retain];     NSLog(@"obj2 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj2)); } 

输出

obj2 引用计数2 

分析

不是自己生成的对象,通过它存在autorelease来实现的。autorelease在超出生命周期后,它提供了一种正确释放对象的方法(通过调用)release方法)机制,以便将对象返回给调用器,让调用器持有后释放对象。否则,在被调用者持有之前,系统会释放对象。调用autorelease后对象不会立刻被释放,而是被注册到autoreleasepool中,然后当autoreleasepool在销毁结束时,调用对象release释放对象的方法

释放不需要自己持有的对象时

- (void)test3 { 
             Person *p = [[Person alloc] init];     [p release]; } 

非自己持有的对象无法释放:

由于当前的调用者并不持有该对象,不能进行释放操作,否则导致程序崩溃

- (void)test4 { 
        
    // 由于当前的调用者并不持有改对象,不能进行释放操作,否则导致程序崩溃。 
    // 如果要释放该对象,需要先对对象进行retain操作。
    id obj = [NSMutableArray array];
    [obj release];
}

代码改成这样

注意:如果返回给obj的是NSMutableArray对象,会导致程序崩溃,但是如果是NSArray就不会

- (void)test5 { 
        
    id obj = [NSMutableArray array];
    [obj retain]; // 当前调用者obj持有NSMutableArray对象
    NSLog(@"obj 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
    [obj release];
}

分析

如果要释放该对象,需要先对对象进行retain操作

属性的引用计数情况

定义一个Person类 代码如下

@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
+ (Person *)create;

@end

@implementation Person

+ (Person *)create { 
        
    Person *per = [[Person alloc] init];
    [per autorelease];
    return per;
}

- (void)dealloc { 
        
    NSLog(@"%s",__func__);
    [super dealloc];
}
@end

VC代码

@interface VC2 ()
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, retain) Person *per;
@end

- (void)viewDidLoad { 
        
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    [self test6];
}
- (void)test6 { 
        
    self.array = [[NSArray alloc] initWithObjects:@1, nil];
    NSLog(@"array 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)_array));
}

输出

array 引用计数2

上面不是说过 alloc创建一个对象并且返回给调用者持有 引用计数为1吗? 这里为什么此时对象的引用计数是2呢?

分析

了解属性的细节 看我这篇文章 OC语言中 .语法 其实就是调用setter方法 @property (nonatomic, retain) Person *per; 定义per 属性时候 是retain关键字 所以生成的标准setter方法内部会进行retain操作

- (void)setArray:(NSArray *)array { 
        
    if(_array != array) { 
        
       [_array release];  
       _array = [array retain]; // retain新值
    }
}

所以此时对象的内存引用情况是:alloc创建时retainCount为1,setter方法中retain了一次引用计数加1,所以此时retainCount变为了2

类似于如下操作:

NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 引用计数+1
self.array = temp; 引用计数+1

所以一般在使用属性赋值的时候一般这么写: 用autorelease抵消一次retain操作

self.array = [[[NSArray alloc] initWithObjects:@2, nil] autorelease];

或者这样

NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 
self.array = temp; 
[temp release];

理解了上面,我们用Person类 观察下有没有内存泄漏

- (void)viewDidLoad { 
        
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    [self test7];
}

- (void)test7 { 
        
    self.per = [[[Person alloc] init] autorelease];
    // alloc1次 setter方法一次 所以是2 
    // autorelease在NSLog时候 还没有释放Person对象 所以还是2,但是终究会 -1
    NSLog(@"per 引用计数%ld",CFGetRetainCount((__bridge CFTypeRef)_per));
}


// autorelease1次 VC delloc中 1次 所以Person对象最终会被释放
- (void)dealloc { 
        
    [_per release];
    NSLog(@"%s",__func__);
    [super dealloc];
}

输出

per 引用计数2
[Person dealloc]
[VC2 dealloc]

分析 都能够正常销毁,不存在内存泄漏 autorelease延迟释放抵消一次 放弃Person所有权 生成的标准的setter方法,不会自动的在dealloc中生成release的代码,所以要手动的重写dealloc方法,加上release的代码 VC delloc中 在发送一条 release 所以Person对象最终会被销毁 为了防止野指针, 可以加上nil

- (void)dealloc { 
        
    [_per release];
    _per = nil;
    NSLog(@"%s",__func__);
    [super dealloc];
}

大家一定要搞清楚什么是野指针 本篇简单说下野指针和僵尸对象 详细的底层原理可以看我这篇文章

野指针和僵尸对象

野指针

指向一个已经被删除的对象或者访问受限内存区域的指针就是野指针,野指针不是nil指针,而是指向了垃圾内存的指针

野指针的场景:

1、 对象释放后,指针没有置空

  • 使用unsafe_unretained修饰符,对象释放后,没有手动置为nil
  • KVO没有移除观察者

2、对象提前释放

  • 异步函数中block使用的self没有强引用,导致外部已经释放掉,但是里面还在使用

3、对象多次释放

僵尸对象

一个已经被释放掉的对象就是僵尸对象 一个OC对象的引用计数为0,调动dealloc后销毁之后,就是僵尸对象。 一个对象虽然被销毁掉了,但是数据依然在内存中,所以如果通过野指针去访问僵尸对象,一旦这个僵尸对象的内存已经被分配给其他人了,就会出错。

为什么不开启僵尸对象检测? 这样每次通过指针访问对象的时候都会检查是否为僵尸对象,这样很影响效率

为什么每次不去把内存上的数据清零? 没必要,影响效率, 数据每次都是覆盖。

例子:

@interface Person : NSObject
@property(nonatomic, label)NSString *name;
@end

@implementation Person

- (void)dealloc { 
        
    NSLog(@"%s",__func__);
    [super dealloc];
}
@end

@implementation ViewController

// crash 
- (void)viewDidLoad { 
        
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    Person *__unsafe_unretained wp = person;
    person.name = @"yang";
    [person release];
    NSLog(@"wp==%@",wp.name);
}

输出崩溃 Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

Printing description of person:
<Person: 0x600000e683e0>
(lldb) x/5gx 0x600000e683e0
0x600000e683e0: 0x000000010a944530 0x000000010a93f040
0x600000e683f0: 0x0000000000000000 0x0000000000000000
0x600000e68400: 0x0000000000000000
(lldb) p person
(Person *) $0 = 0x0000600000e683e0
(lldb) p wp
(Person *) $1 = 0x0000600000e683e0
-[Person dealloc]
(lldb) p wp
(Person *) $2 = 0x0000600000e683e0
(lldb) 0x600000e683e0对象已经释放了 但是wp指针变量还指向0x600000e683e0对象  0x600000e683e0现在就属于僵尸对象  wp就是野指针  会造成程序崩溃

分析

wp指针弱引用[Person alloc] 对象 不影响对象释放 0x600000e683e0对象已经释放了 但是wp指针变量还指向0x600000e683e0对象 0x600000e683e0现在就属于僵尸对象 wp就是野指针 会造成程序崩溃

把指针设为nil之后

- (void)viewDidLoad { 
        
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    Person *__unsafe_unretained wp = person;
    person.name = @"yang";
    [person release];
    person= nil;
    wp = nil;
    NSLog(@"wp==%@",wp.name);
}

输出

-[Person dealloc]
wp==(null)

或者使用 __weak

- (void)viewDidLoad { 
        
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    Person *__weak wp = person;
    person.name = @"yang";
    [person release];
    person= nil;
    NSLog(@"wp==%@",wp.name);
}

输出

(lldb) p wp
(Person *) $0 = 0x00006000018487e0
-[Person dealloc]
(lldb) p wp 
(Person *) $1 = nil
(lldb)

两种方式都可以保证程序正常运行

思考个问题

Q: 既然 __weak 更安全,那么为什么已经有了 __weak 还要保留 __unsafe_unretained ? 1、__weak仅在ARC中才能使用,而MRC只能使用__unsafe_unretained 2、__weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained比__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。 比如,MyViewController 持有 MyView,MyView 需要调用 MyViewController 的接口。MyView 中就可以存储__unsafe_unretained MyViewController *_viewController 对于__weak底层源码分析可以看我这篇文章

下篇文章我们继续总结ARC

后记

记得在《寻梦环游记》里对于一个人的死亡是这样定义的:当这个这个世界上最后一个人都忘记你时,就迎来了终极死亡。类比于引用计数,就是每有一个人记得你时你的引用计数加1,每有一个人忘记你时,你的引用计数减1,当所有人都忘记你时,你就消失了,也就是从内存中释放了。

如果再深一层,包含我们后面要介绍的ARC中的强引用和弱引用的话,那这个记住的含义就不一样了。强引用就是你挚爱的亲人,朋友等对你比较重要的人记得你,你的引用计数才加1。

而弱引用就是那种路人,一面之缘的人,他们只是对你有一个印象,他们记得你是没有用的,你的引用计数不会加1。当你挚爱的人都忘记你时,你的引用计数归零,你就从这个世界上消失了,而这些路人只是感觉到自己记忆中忽然少了些什么而已。

外链

1、iOS属性的底层原理 2、__weak实现底层原理 3、野指针僵尸对象底层原理

标签: mrc多量程传感器setra

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司