高性能iOS应用开发-内存管理

iOS设备中某个应用内存使用超过单个进程上的限制,会被系统终止使用。
内存问题常出现在重复的内存释放循环引用的情况。

内存消耗

内存消耗指的是应用消耗的RAM。iOS的虚拟内存模型并不包含交换内存,意味着不会被用来分页内存。
应用中内存消耗分为两部分:栈大小堆大小

栈大小

应用中新线程都有专用的栈空间,该空间由保留的内存和初始提交的内存组成。栈可以在线程存在期间自由使用。线程的最大空间很小,这就决定了以下限制:

  • 可被递归调用的最大方法数的限制,每个方法都有自己的栈帧或者可以看这个链接,消耗整体的栈空间。main中调用func1,func1中调用func2,这就存在三个栈帧,每个栈帧都会消耗一定字节的内存。
main() { //第一个栈帧
    func1(); //第二个栈帧
}

func1() {
    func2(); //在func1()上增加一个栈帧。
}
  • 一个方法中最多可以使用的变量个数的限制,所有的变量都会载入方法的栈帧中,并消耗一定栈空间。
  • 视图层级中可以嵌入的最大视图深度的限制,渲染符合视图,整个视图层级树种递归调用layoutSubviews和drawRect方法。若层级过深,会导致栈溢出。

堆大小

每个进程的所有线程共享同一个堆。应用并不能控制分配给它的堆。只有操作系统才能管理堆。
通过类创建的对象相关数据都存放在堆中。类可能包含属性或值类型的实例变量(iVars,基本数据类型),如int、char或struct。因为对象是在堆内创建的,所以他们只消耗内存。

使用NSString、载入图片、创建或使用JSON/XML数据、使用视图等都会消耗大量的堆内存。需要关注平均值和峰值内存使用的最小化
当对象被创建并赋值时,数据可能会从栈复制到堆。类似的,当值仅在内部使用时,也可能会被从堆复制到栈。
虽然没有强制规定,但内存最好不要超过80%~85%,要给系统内核留下足够内存。**不要忽视didReceiveMemoryWarning信号。

内存管理模型

管理模型基于持有关系(引用计数?retain or release?)的概念。如果一个对象正处于被持有状态,那它占用的内存就不能被回收。
当一个对象创建于某个方法内部十,那么方法就持有该对象。如果这个方法返回,则调用者声称建立了持有关系。这个值可以付给其他变量,对应的变量同样会声称建立了持有关系。
一旦与某个对象相关的任务全部完成,就是放弃了持有关系。这一过程中没有转移持有关系,而是分别增加或减少了持有者的数量。当持有者的数量降为零时,对象会被释放,相关内存会被回收。这种持有关系计数通常被正式成为引用计数。亲自管理时,被称为手动引用计数(MRC)。现如今的应用大都使用自动引用计数(ARC)

引用计数基本结构

NSString *message = @"a"; //引用计数为1
NSString *messageRetained = [message retain]; //引用计数为2
[messageRetained release]; //引用计数为1
[message release]; //引用计数为0
NSLog(@"%@", message); //此时值已经为未定义状态,但还是能取得相同的值,因为对应的内存还没被回收或重置。

方法中的引用计数

//一个Person类的部分
 - (NSString *)address {
    NSString *result = [[NSString alloc] initWithFormat:@"%@", self.city]; //首次创建为1
    return result;
}

 - (void)showPerson:(Persson *)p {
    NSString *paddress = [p address]; //引用计数仍未1
    NSLog(@"%@", paddress); //不变
    [paddress release]; //引用计数为0
}

自动释放对象

自动释放对象让你能够放弃一个对象的持有关系,但是延后对它的销毁。当方法中创建一个对象并需要将其返回时,自动释放就显得十分有用。

 - (NSString *)address{
    NSString *result = [[[NSString alloc] initWithFortmat:@"%@", self.city] autorelease];
    //[result release]; //3对象在返回之前释放,返回引用无效。
    return result;
}

规则:
- 持有的对象是alloc方法生成并返回的;
- 确保没有内存泄露,必须在失去引用之前放弃关系;
- 如果使用release,那么对象释放将发生在返回之前,因而方法将返回无效的引用;
- autorelease表明想放弃持有关系,但同时发放的调用者允许对象被释放之前使用对象。

当创建一个对象并将其从非alloc方法返回时,应使用autorelease。

自动释放池块(@autoreleasepool)

自动释放池块是允许你放弃对一个对象的持有关系、但可避免它立即被回收的一个工具。当从方法返回对象时,这个功能非常有用。
它(自动释放池块)能确保在块内创建的对象会在块完成时被回收(用完了就被回收)。这在创建了多个对象的场景中非常有用。本地的块可以用来今早的释放其中的对象,从而使内存用量保持在较低水平。
自动释放池块用@autoreleasepool表示。

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

块中收到autorelease消息的所有对象都会在autoreleasepool块结束时收到release消息。更加重要的是,每个autorelease调用都会发送一个release消息。这意味着如果一个对象收到了不知一次的autorelease消息,那它也会多次收到release消息。这样能保证对象引用技术下降到使用autoreleasepool块之前的值。如果计数为0,则对象将被回收,从而保持较低的内存使用率。

autoreleasepool可以嵌套。另外autorelease对象在autoreleasepool块内执行,能确保autorelease对象被释放,从而防止应用内存泄露的情况。
AppKit和UIKit框架将事件–循环的迭代放入autoreleasepool块中。因此,通常不需要自己在创建autoreleasepool块。
使用autoreleasepool块的场景:

  • 当创建了很多临时对象的循环时。循环中使用autoreleasepool块可以为每个迭代释放内存,可以大大降低内存的需求。
  • 当创建了一个线程时。主线程用自己的autoreleasepool块,然而对自定义线程,必须自己创建autoreleasepool。
-(void)threadStart {
    @autoreleasepool{
        //新线程的代码
    }
}
//其他地方
{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
    [thread start];
}

自动引用计数

出现的原因:持续跟踪retain、release和autorelease不容易。2011年提出的ARC。Objective-C同时支持ARC和MRC,Swift不支持MRC。
ARC是编译器特性。评估了对象在代码中的生命周期,并在编译时自动注入合适的内存管理调用(ARC内存管理调用发生在编译时)。编译器还会生成合适的dealloc方法。

优势:节约了开发时间,降低了开发难度,减少了代码量。
禁用ARC,需要进入 Targets->Build Phases->Compile Sources,选择必须禁用 ARC的文件,然后添加编译器标记 -fno-objc-arc。

ARC的规则

  • 不能实现或者调用retain、release、autorelease或retainCount方法。这一限制不仅针对对象,对选择器(@selector())同样有效。因此,[obj release]@selector(retain)是编译时的错误。
  • 可以实现dealloc方法,但是不能主动调用。不仅不能调用其他对象的dealloc方法,也不能调用其超类。如:[super dealloc]是编译时的错误。
  • 不能调用NSAllocateObject和NSDeallocateObject方法。应使用alloc方法创建对象,运行时负责回收对象。
  • 不能再C语言结构体内使用对象指针?
  • 不能再id类型和void*类型之间自动转换。需要做显示转换。
  • 不能使用NSAutoreleasePool,要替换使用autoreleasepool块。
  • 不能使用NSZone内存区域。
  • 属性的访问器名称不能以new开头,确保与MRC的互操作性。
  • ARC和MRC仍然可以混合使用。(new开头的属性会报错)
    arc属性名称不能以new开头,但开启访问器可以
//won't work
@property NSString *newTitle;
//Works
@property (getter=getNewTitle) NSString *newTitle;

引用类型

ARC带来了新的引用类型:弱引用。支持的类型包括:

  • 强引用。默认引用类型,被强引用指向不会被释放。强引用会对引用计数加1,从而扩展对象的生命周期。
  • 弱引用。不会增加引用计数,因而不会扩展对象的生命周期。

变量限定符

ARC为变量提供了四种生命周期限定符。

  • __strong,这是默认的限定符,无需显示引入。只要有强引用指向,对象就会长时间驻留在内存 中。可以将 __strong 理解为 retain 调用的 ARC 版本。
  • __weak,这表明引用不会保持被引用对象的存活。当没有强引用指向对象时,弱引用会被置为 nil。可将 __weak 看作是 assign 操作符的 ARC 版本,只是对象被回收时,__weak 具有安全性——指针将自动被设置为 nil。
  • __unsafe_unretained,与 __weak 类似,只是当没有强引用指向对象时,__unsafe_unretained 不会被置为 nil。 可将其看作 assign 操作符的 ARC 版本。
  • __autoreleasing,__autoreleasing用于由引用使用id *传递的消息参数。它预期了autorelease方法会 在传递参数的方法中被调用。
//TypeName * qualifier variable;
Person * __strong p1 = [[Person alloc] init]; //1
Person * __weak p2 = [[Person alloc] init]; //2
Person * __unsafe_unretained p3 = [[Person alloc] init]; //3
Person * __autoreleasing p4 = [[Person alloc] init]; //4

1.创建后引用计数为1,并且对象在p1引用期间不会被回收。
2.创建对象后饮用计数为0,对象会被立即释放,且p2将置为nil。
3.创建后计数变为1,对象会被立即释放,但p3不会被设置为nil。
4.创建后计数为1,当方法返回时对象会被立即释放。

属性限定符

属性声明有六个持有关系限定符:
- strong,默认符,指定了__strong关系;
- weak,指定了__weak关系;
- assign,在新版本中,assign的含义发生了变化。在ARC之前,assign是默认的持有关系限定符。在启用ARC之后,assign表示了__unsafe_unretained关系;
- copy,暗指了__strong关系,暗示了setter中的复制语句的常规行为;
- retain,指定了__strong关系;
- unsafe_unretained,指定了__unsafe_unretained关系。
因为assign和unsafe_unretained只进行值复制而没有任何实质性的检查。

你可能感兴趣的:(iOS)