ForgetSou | Blog

❤ 武统台湾 刻不容缓 ❤

0%

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

image.png

一. 简述

​ 手机的内存资源是非常有限的,如果一个应用的内存使用量超过了单个进程的上限,就会被操作系统终止使用,正式这个原因,内存管理在iOS中扮演着核心的角色。
​ 与(基于垃圾回收的)Java 运行时不同,Objective-C 和 Swift 的 iOS 运行时使用引用计数。 使用引用计数的负面影响在于,如果开发人员不够小心,那么可能会出现重复的内存释放 和循环引用的情况。

1.内存消耗

​ 内存消耗指的是应用消耗的 RAM。应用中的内存消耗分为两部分:栈大小和堆大小。

1.1 栈大小

​ 可被递归调用的最大方法数
​ 每个方法都有其自己的栈帧,并会消耗整体的栈空间。如果你调 用 main,那么 main 将调用 method1,而 method1 又将调用 method2,这就存在三个栈帧 了,且每个栈帧都会消耗一定字节的内存。

​ 一个方法中最多可以使用的变量个数
​ 视图层级中可以嵌入的最大视图深度
​ 渲染复合视图将在整个视图层级树中递归地调用 layoutSubViews 和 drawRect 方法。如 果层级过深,可能会导致栈溢出。

1.2 堆大小

​ 每个进程的所有线程共享同一个堆。一个应用可以使用的堆大小通常远远小于设备的 RAM 值。例如,iPhone 5S 拥有大约 1GB 的 RAM,但分配给一个应用的堆大小最多不到 512MB。应用并不能控制分配给它的堆。只有操作系统才能管理堆。
​ 使用 NSString、载入图片、创建或使用 JSON/XML 数据、使用视图等都会消耗大量的堆 内存。如果你的应用大量使用图片(与 Flickr 和 Instagram 应用类似),那么你需要格外关 注平均值和峰值内存使用的最小化。
​ didReceiveMemoryWarning
​ 建议使用量不要超过 80%~85%,要给操作系统的核 心服务留下足够多的内存。

2.内存管理模型

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

3.自动释放对象

​ 自动释放对象让你能够放弃对一个对象的持有关系,但延后对它的销毁。当在方法中创建 一个对象并需要将其返回时,自动释放就显得非常有用。自动释放可以帮助在 MRC 中管 理对象的生命周期。

-(NSString *) address
{
NSString *result = [[[NSString alloc]
initWithFormat:@"%@\n%@\n%@, %@",
self.line1, self.line2, self.city, self.state]
autorelease]; return result;
}
(1) 持有的对象(在上述示例中是 NSString)是 alloc 方法返回的。
(2) 确保没有内存泄漏,你必须在失去引用之前放弃持有关系。
(3) 但是,如果使用了 release,那么对象的释放将发生在返回之前,因而方法将返回一个
无效的引用。
(4) autorelease 表明你想要放弃持有关系,同时允许方法的调用者在对象被释放之前使用
对象。

​ 当创建一个对象并将其从非 alloc 方法返回时,应使用 autorelease。这样 可以确保对象将被释放,并尽量在调用方法执行完成时立即释放。

4.自动释放池

​ 自动释放池块是允许你放弃对一个对象的持有关系、但可避免它立即被回收的一个工具。当从方法返回对象时,这种功能非常有用。

​ 它还能确保在块内创建的对象会在块完成时被回收。这在创建了多个对象的场景中非常有 用。本地的块可以用来尽早地释放其中的对象,从而使内存用量保持在较低的水平。

​ 自动释放池块用 @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,则对象将被回收,从而保持较低的内存使用率。

​ 看了 main 方法的代码后,你会发现整个应用都在一个 autoreleasepool 块中,这意味着所 有的 autorelease 对象最后都会被回收,不会导致内存泄漏。

​ Cocoa 框架希望代码能在 autoreleasepool 块内执行,否则 autorelease 对象将无法被 释放,从而导致应用发生内存泄漏。

AppKit 和 UIKit 框架将事件 - 循环的迭代放入了 autoreleasepool 块中。因此,通常 不需要你自己再创建 autoreleasepool 块了。

​ 在一些特定情况下,你很可能想创建自己的 autoreleasepool 块,例如以下这些情况。

  • 当你有一个创建了很多临时对象的循环时
    在循环中使用 autoreleasepool 块可以为每个迭代释放内存。虽然迭代前后最终的内存 使用相同,但你的应用的最大内存需求可以大大降低。

    {
    @autoreleasepool {
    NSUInteger *userCount = userDatabase.userCount;
    for(NSUInteger *i = 0; i < userCount; i++) {
    @autoreleasepool {
    Person *p = [userDatabase userAtIndex:i];
    NSString *fname = p.fname;
    if(fname == nil) {
    fname = [self askUserForFirstName];
    }
    NSString *lname = p.lname;
    if(lname == nil) {
    lname = [self askUserForLastName];
    }
    //...
    [userDatabase updateUser:p];
    }
    }
    }
    }
  • 当你创建一个线程时

    每个线程都将有它自己的 autoreleasepool 块栈。主线程用自己的 autoreleasepool 启 动,因为它来自统一生成的代码。然而,对于任何自定义的线程,你必须创建自己的 autoreleasepool。

    -(void)myThreadStart:(id)obj { 
    @autoreleasepool {
    //新线程的代码
    }
    }
    //其他地方
    {
    NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadStart:) object:nil];
    [myThread start];
    }

    5. 自动引用计数

    image-20210922100020465

    • 不能实现或调用 retain、release、autorelease 或 retainCount 方法。这一限制不仅针 对对象,对选择器同样有效。因此,[obj release]或@selector(retain)是编译时的错误。

    • 可以实现 dealloc 方法,但不能调用它们。不仅不能调用其他对象的 dealloc 方法,也 不能调用超类。[super dealloc] 是编译时的错误。
      但你仍然可以对 Core Foundation 类型的对象调用 CFRetain、CFRelease 等相关方法。

    • 不能调用 NSAllocateObject 和 NSDeallocateObject 方法。应使用 alloc 方法创建对象, 运行时负责回收对象。

    • 不能在 C 语言的结构体内使用对象指针。

    • 不能在 id 类型和 void * 类型之间自动转换。如果需要,那么你必须做显示转换。

    • 不能使用 NSAutoreleasePool,要替换使用 autoreleasepool 块。

    • 不能使用 NSZone 内存区域。

    • 属性的访问器名称不能以 new 开头,以确保与 MRC 的互操作性。

    • 虽然总的来说需要避免许多事情,但仍然可以混合使用 ARC 和 MRC 代码。

    6. 引用类型

    • 强引用

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

    • 弱引用

      弱引用是一种特殊的引用类型。它不会增加引用计数,因而不会扩展对象的生命周期。 在启用了 ARC 的 Objective-C 编程中,弱引用格外重要。

    6.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方法会 在传递参数的方法中被调用。

      // 创建对象后引用计数为 1,并且对象在 p1 引用期间不会被回收。
      Person * __strong p1 = [[Person alloc] init];
      // 创建对象后引用计数为 0,对象会被立即释放,且 p2 将被设置为 nil。
      Person * __weak p2 = [[Person alloc] init];
      // 创建对象后引用计数为 1,对象会被立即释放,但 p3 不会被设置为 nil。
      Person * __unsafe_unretained p3 = [[Person alloc] init];
      // 创建对象后引用计数为 1,当方法返回时对象会被立即释放。
      Person * __autoreleasing p4 = [[Person alloc] init];

      6.2 属性限定符

      • strong

        默认符,指定了__strong关系

      • weak

        指定了__weak关系

      • assign

      • copy

        暗指了 __strong 关系。此外,它还暗示了 setter 中的复制语义

      • retain

        指定了 __strong 关系。

      • unsafe_unretained

        指定了 __unsafe_unretained 关系。

      7. 僵尸对象

      ​ 僵尸对象是用于捕捉内存错误的调试功能。

      ​ 通常情况下,当引用计数降为 0 时对象会立即被释放,但这使得调试变得困难。如果开启 了僵尸对象,那么对象就不会立即释放内存,而是被标记为僵尸。任何试图对其进行访 问的行为都会被日志记录,因而你可以在对象的生命周期中跟踪对象在代码中被使用的 位置。

      ​ NSZombieEnabled 是一个环境变量,可以控制 Core Foundation 的运行时是否将使用僵尸对 象。不应长期保留 NSZombieEnabled,因为默认情况下不会有对象被真正析构,这会导致应 用使用大量的内存。特别说明一点,在发布的构建包中一定要禁用 NSZombieEnabled。

      ​ 要想设置 NSZombieEnabled 环境变量,需要进入 Product → Scheme → Edit Scheme。选择 左侧的 Run,然后在右侧选取 Diagnostics 标签页。选中 Enable Zombie Objects 选项。


个人博客: 🏡 ForgetSou


-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道