天天看点

【iOS开发进阶】-内存管理1.内存管理模型2.MRC内存管理3.ARC内存管理4.自动释放池5.内存泄漏6.僵尸对象7.CoreFoundation框架中的内存管理8.id与void*

1.内存管理模型

对于面向过程的C语言而言,其设计的内存管理方式十分直接,内存的申请和释放都由开发者手动处理。这种管理方式简单,但是会大量增加编码过程中的工作量,也会增加代码的复杂度。

在面向对象语言中,内存管理通常会由模型机制来完成,常见的有垃圾回收与引用计数两种内存管理模型。Java中的JVM使用的就是垃圾回收管理模型,而Objective-C语言采用的是引用计数的内存管理模型。

在iOS程序中,内存通常被分成5个区域:

  • 栈区:存储局部变量,在作用域结束后内存会被回收
  • 堆区:存储Objective-C对象,需要开发者手动申请和释放
  • BSS区:用来存储未初始化的全局变量和静态变量
  • 数据区:用来存储已经初始化的全局变量、静态变量和常量
  • 代码段:加载代码

上述五个内存区域中,除了堆区需要开发者手动进行内存管理外,其他区域由系统自动进行回收。

引用计数是Objective-C语言提供的内存管理技术,其实每一个Objective-C对象都有一个retainCount属性,这个属性可以简单理解为一个计数器。每有一个地方引用它,它自身的引用计数就加一,当一个地方不再引用它时,它自身的引用计数就减一。最终根据其引用计数值是否为0来确定这块内存是是否被回收。

2.MRC内存管理

MRC内存管理有两个原则:

  • 谁持有对象,谁负责释放,不是自己持有的不能释放
  • 当对象不再被需要时,需要主动释放

Objective-C中会对对象进行持有的方法:

  • alloc 进行内存分配
  • new 创建并初始化对象
  • copy 复制对象
  • mutablecopy 可变复制对象
  • retain 进行持有

前四个方法都是创建新的对象,让其引用计数为1,retain方法的作用是对当前对象进行持有。

在早期的iOS开发中,开发者不得不使用MRC进行自己项目的内存管理,在MRC工程中,充斥着大量包含retain,release字段的代码,而且开发者要时刻提醒自己,不要忘记释放已经不用的资源。

3.ARC内存管理

在ARC中,编译器会自动帮助我们进行retain的添加,开发者位移要做的是使用指针指向这个对象,当指针被指控或者被指向新值时,原来的对象会被release一次。同样,对于自己生产的对象,当其离开作用域时,编译器也会为其添加一个release操作。

在ARC中,有几个修饰关键字非常重要,分别是:_ _strong、 _ _weak、 _ _unsafe _unretained、 _ _autoreleasing。这些关键字在ARC中被称为所有权修饰符。指针默认都是使用 _ _strong关键字修饰的。

_ _strong修饰符通常用来对变量进行强引用,主要有以下三个作用:

  1. 使用该修饰符的变量如果是自己生成的,则会被添加进自动释放池,在作用域结束后,会被release一次
  2. 使用该修饰符的变量如果不是自己生成的,则会被强引用,即会被持有使其引用计数增加1,在离开作用域后会被release一次
  3. 使用该修饰符的变量指针如果重新赋值或者置为nil,则变量会被release一次。

_ _weak修饰符通常用来对变量进行弱引用,最大的作用是避免ARC环境下的循环引用问题。主要作用是两个:

  1. 使用该修饰符的变量仅提供弱引用,不会使其引用计数增加。变量对象如果是自己生成的,则会被添加到自动释放池中,会在离开作用域时被release一次。如果不是自己生成的,则离开作用域不会被release。
  2. 使用该修饰符的变量指针,如果变量失效,则指针会被自动置为nil,这是一种比较安全的设计方式,大量减少野指针造成的异常。

_ _unsafe _unretained修饰符的作用也是对变量进行弱引用,与 _ _weak类似,但是当变量对象失效时,其指针不会被自动置为nil。当变量失效后,被修饰指针不会被安全处理为nil,即旧地址依然保存。

当被修饰的对象已经失效时,指针依然指向其所在的地址,这种情况下的指针通常被称为野指针,这在编程中是一种非常危险的情况。

_ _autoreleasing修饰符的作用与自动释放池相关。

ARC内存管理有以下几大原则:

  • 不能使用retain、release、autorelease函数,不可访问retainCount属性
  • 不能调用dealloc函数,可以覆写,但是在实现中不可调用父类的dealloc函数
  • 不能使用NSAutoreleasePool,可以使用@autoreleasepool代替
  • 对象型变量不能作为C语言的结构体

内存管理相关属性修饰符:

assign:直接赋值,和引用计数无关,用来声明简单数据类型的属性,如int

retain:对旧对象进行释放,并强引用新的对象,使其引用计数加一,用在MRC中

strong:对新对象进行强引用,释放旧对象,使其引用计数加一,作用域retain类似,用在ARC中

copy:在实现Setter方法时,采用copy函数,会生成新的对象被自己持有

weak:弱引用,不对所赋值的对象进行持有,但是安全,当对象不可用时,会被置为nil,用在ARC中

unsafe_unretained:弱引用,和weak不同的是,引用的对象不可用时,当前指针不会被置为nil,会产生野指针

注:

  1. 属性修饰符与变量修饰符的区别
  2. ARC与MRC不一定必须独立存在,ARC和MRC是可以进行混编的

4.自动释放池

OC对象的生命周期取决于引用计数,其中有两种方式可以释放对象:一种是直接调用release释放,另一种是调用autorelease将对象加入自动释放池中。使用了autorelease方法的对象被称为自动释放对象,自动释放对象的内存管理是交给自动释放池处理的,即自动释放池用于存放那些需要在稍后某个时刻释放的对象,其本质上是使release函数的调用被延迟了。

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
           

上述代码中的@autoreleasepool{}就是自动释放池,当自动释放池操作结束后,其会向北添加进自动释放池的所有对象发送release消息。要将对象添加进自动释放池,即调用autorelease方法。

iOS系统在运行应用程序时,会自动创建一些线程,每一个线程都默认拥有自动释放池,在每次执行事件循环时,都会将其自动生成释放池清空。

5.内存泄漏

内存泄漏的核心问题是循环引用。

循环引用

在 Objective-C设计中,对象会对其内的属性进行持有,当一个对象的引用计数为0,将其内存回收时,这个对象会向其中所有的属性发送release消息,让其中的属性对象进行释放。对象对其中属性的持有关系如图:

【iOS开发进阶】-内存管理1.内存管理模型2.MRC内存管理3.ARC内存管理4.自动释放池5.内存泄漏6.僵尸对象7.CoreFoundation框架中的内存管理8.id与void*

如果对象内的某个属性再次对当前对象进行了持有,则会产生循环引用,因为对象只有在引用计数降为0时,才会向其内的属性发送release消息,同样,只有其内属性接收到了release消息,才会对它们所持有的所有对象进行释放,当前对象的引用计数才可能降为0,此时就产生了循环引用,类似死锁。

【iOS开发进阶】-内存管理1.内存管理模型2.MRC内存管理3.ARC内存管理4.自动释放池5.内存泄漏6.僵尸对象7.CoreFoundation框架中的内存管理8.id与void*

Block与循环引用

在Block中使用外部的对象时,都会对对象进行一次强引用,因此如果在Block中使用持有它自己的对象时,就会造成反向持有,从而形成循环引用。如下所示:

self.myblock = ^BOOL(int param) {
  NSLog(@"%@",self);
  return YES;
};
           

因为myBlock是当前类中的属性,所有当前类对象对myBlock持有强引用,只有当前类对象引用计数降为0时,才会对myBlock发送release消息,同样,由于在myBlock内部使用到了self关键字,使得myBlock对当前对象又进行了强引用,当前类对象想要释放,必须等myBlock对象的引用计数降为0,这就产生了循环引用。

解决由于属性Block中引用了当前类对象本身造成的循环引用并不困难,只需要在Block中使用弱引用指针即可:

__weak typeof(self) __self = self;
self.myblock = ^BOOL(int param) {
  NSLog(@"%@",__self);
  return YES;
};
           

代理与循环引用

View.h

@protocol ViewDelegate <NSObject>
- (void)buttonClick:(NSString *)value;
@end

@interface View : UIView
@property(nonatomic, strong)id<ViewDelegate> delegate;
@end
           

View.m

@implementation View
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if(self) {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
        button.frame = CGRectMake(0, 0, 40, 20);
        [button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
        self.backgroundColor = [UIColor redColor];
        [self addSubview:button];
    }
    return self;
}
- (void)buttonClick {
    if([self.delegate respondsToSelector:@selector(buttonClick:)]) {
        [self.delegate buttonClick:@"HelloWord"];
    }
}
@end
           

ViewController.m

@interface ViewController () <ViewDelegate>
@property(strong)View *myView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.myView = [[View alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [self.view addSubview:self.myView];
    self.myView.delegate = self;
}

- (void)buttonClick:(NSString *)value {
    NSLog(@"%@",value);
}
@end
           

myView作为视图控制器对象的属性,视图控制器对myView保持强引用,在设置代理时,代理属性delegate采用了strong修饰符修饰,因此ViewController对象又被myView对象强引用了,从而形成循环引用,产生内存泄漏。

需要解决代理模式中的循环引用,需要使用一方变成弱引用,最便捷的方式就是在声明delegate属性时使用weak修饰符修饰。

定时器引起的内存泄漏

定时器是用来执行循环任务的,NSTimer是iOS中极易产生循环引用的一个类。

@interface ViewController ()
@property(nonatomic, strong)NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
}

- (void)timerRun {
    NSLog(@"run....");
}
@end
           

此处产生循环引用的原因是定时器对象实际上持有了当前视图控制器对象,只有当定时器失效后,其才会释放所持有的当前视图控制器。

需要解决定时器引起的循环引用,需要手动调用invalidate方法来使定时器失效:

[self.timer invalidate];
           

6.僵尸对象

僵尸对象与内存泄漏没有关系,当一个对象被释放后,如果其指针没有置空,则这个指针就变成了野指针,此时这个指针指向的就是僵尸对象。

在Objective-C中,内存的使用包含以下几个阶段:

  • 请求创建对象,向系统申请一块内存空间,在申请完成后,这块内存空间不能再做他用
  • 对象被释放,此时这块内存重新被申请使用之前,这块内存中的数据依然存在
  • 此时如果依然有指针指向这块内存,则此指针为野指针
  • 当野指针对这块内存进行访问时,如果这块内存已经被重新分配,则会出现系统问题,如果没有被分配,则不会出现系统问题。

Xcode默认不会对僵尸对象进行检查,当我们使用野指针视图访问一个僵尸对象时,如果此时内存数据有效,则会访问成功,如果无效,则会产生异常。这会对程序产生很大的影响,因为一个必现的异常要比非必现的异常容易解决的多,而且会使我们的程序非常不可控。

在ARC中,使用_ _weak修饰和 _ _strong修饰的变量指针在对象释放后会被自动置为nil,这就大大减少了野指针问题。也可以借助Objective-C的消息机制来规避所有的僵尸对象问题。

7.CoreFoundation框架中的内存管理

CoreFoundation框架是由C语言实现的一组编程接口,其与Foundation框架提供相似的基础功能,不同的是Foundation框架是由Objective-C实现的。

在CoreFoundation框架中,对象依然采用引用计数的方式进行内存管理,但并不支持ARC,即使在ARC环境下,如果我们用到CoreFoundation框架中的对象,也需要手动进行内存管理。

在CoreFoundation框架中,内存管理原则如下:

  1. 自己创建的对象要自己负责释放
  2. 如果使用别人创建的对象,要保证其可用,则需要对对象进行持有
  3. 如果对对象进行了持有,则当不再需要此对象时,要进行释放

CoreFoundation框架与Foundation框架混用

_ _bridge关键字可以进行CoreFoundation对象和Foundation对象的相互转换:

CFUUIDRef uuid = CFUUIDCreate(NULL);
__weak NSUUID *uid = (__bridge NSUUID *)(uuid);
NSLog(@"%@",uid);   //有值
CFRelease(uuid);
NSLog(@"%@",uid);   //null
           

使用_ _bridge进行转换的对象的引用计数并不会做任何额外操作。即不主动修改对象的引用计数。

_ _bridge _retained可以将Foundation对象转换为CoreFoundation对象,不同于 _ _bridge关键字,__bridge _retained关键字转换后会对对象进行强引用:

_ _bridge _transfer可以将CoreFoundation对象转化为Foundation对象,同样,转换后会对对象进行强引用。

8.id与void*

id

id是Objective-C中定义的一种泛型实现,可以表示任何对象类型。其中包含了三层含义:

1.作为参数或返回值

最基础的意义。

2.id类型的参数不会进行类型检查

声明为id类型的对象就相当于告诉编译器不进行类型检查(与NSObject类型的最大区别)。因此可以将id类型的变量赋值给任何对象类型,也可以将任何对象类型的变量赋值给id类型,使用id类型的对象可以调用任意方法,都不会进行类型检查。

3.id<protocol>是一种优雅的编程方式

由于id类型不会进行编译检查,因此约束类型方法实现的最好方式就是通过协议。因此这种方式不再关心类型,只注重约定的实现。

void与void *

开发中,void用得最多的地方就是标记Objective-C语言中无返回值的函数,和C语言的函数不同,Objective-C语言的函数必须有一个确定的返回值类型,如果没有返回值,则需要使用void来标记返回值类型。

void在C语言中还有一大用途在于约束无参函数,如果函数没有参数,但是调用时强制传入参数编译器也不会有错误提醒。但是使用void作为参数,就完全不能传入参数了。

void大多数时候用来表示”空“,而void *则完全不同,其所描述的实际上是任意类型的指针,与id类似,id描述的是Objective-C对象,但是本质上也是指针,id类型的数据和void *类型的数据是可以进行类型转换的。但是在ARC环境下不允许直接将id于void *进行转换。需要使用桥接的方式进行转换。

类型检查都是编译时的特性,真正传递的数据依然是运行时决定的。

参考:

深入总结iOS内存管理

IOS的内存管理模型