【iOS】内存泄漏检查及原因分析

目录

    • 为什么要检测内存泄漏?
    • 什么是内存泄漏?
    • 内存泄漏排查方法
      • [1. 使用Zombie Objects](#1. 使用Zombie Objects)
      • [2. 静态分析](#2. 静态分析)
      • [3. 动态分析方法](#3. 动态分析方法)
    • 内存泄漏原因分析
      • [1. Leaked Memory:应用程序未引用的、不能再次使用或释放的内存。](#1. Leaked Memory:应用程序未引用的、不能再次使用或释放的内存。)
      • [2. Abandoned Memory: 内存仍被应用程序所引用,没有任何有用的用途。](#2. Abandoned Memory: 内存仍被应用程序所引用,没有任何有用的用途。)
      • [3. Cached Memory:内存仍然由应用程序引用,可以再次使用以获得更好的性能。](#3. Cached Memory:内存仍然由应用程序引用,可以再次使用以获得更好的性能。)

前言

  • 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
  • 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间 ,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

为什么要检测内存泄漏?

迅速膨胀的内存可以很快让程序毙命,所以要多加防范。即使有 ARC(自动引用计数)内存管理机制,但在现实中对象之间引用复杂,循环引用导致的内存泄漏仍然难以避免,所以关键时刻还要自力更生。分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。

什么是内存泄漏?

内存泄漏 指的是一块内存被分配后不再使用,但是没有被程序正确释放回系统,从而导致该内存继续占用在程序中,无法被其它任务使用。

这通常发生在使用了动态内存分配但未及时或正确释放 ,或者由于编程逻辑错误导致。如果不加以管理,会导致程序消耗过多的内存,甚至导致应用程序崩溃。

比如下MRC中如下代码会造成泄漏:

objectivec 复制代码
NSString* string = [[NSString alloc] init];
...
// [string release];  //ARC下,编译器自动添加此代码

但由于ARC机制,编译器会在适当的时机帮我们加上release代码,避免了内存泄漏。不过即使在ARC中也有肯能因对象不释放而引起内存泄漏 ,比如使用CF框架下的对象而没有做CFRelease操作。

虽然string所占的内存很小可以忽略不计,但也是有安全隐患的,就像前言所述,代码中这里泄漏一点内存,那里又泄漏一点内存,反反复复,内存总会有用尽的那一刻。

毕竟系统本身内存有限,分配给每个App的内存更加有限,当系统内存慢慢不足时,我们的App会变得越来越卡顿。

当系统内存告急时,App中首先会收到didRecieveWarning提醒,如果我们不第一时间采取措施释放内存,那么系统就会把我们的App Kill掉,所以我们应该重视内存泄漏问题。

didRecieveWarning调用流程看这篇文章:【iOS】didReceiveMemoryWarning实例方法

内存泄漏排查方法

1. 使用Zombie Objects

有时候我们会收到EXC_BASD_ACCESS错误提示,但没能跳到具体的出错代码行,此时可以启用Zombie Objects功能,来寻找那些已被释放的对象。

进入edit Scheme:

选中僵尸对象选项:

按照以上步骤开启Zombies Objects,而后Memory查看器变为disable:

系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤,即把对象转化为僵尸对象,而不彻底回收。

测试代码:

objectivec 复制代码
void PrintClassInfo(id obj) {
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===", class_getName(cls), class_getName(superCls));
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    UIView* view = [[UIView alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(view);
    
    [view release];
    
    NSLog(@"After release:");
    PrintClassInfo(view);
}

2. 静态分析

打开Xcode项目,并点击Product->Analyze:

静态内存泄漏分析如下:


静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。

3. 动态分析方法

打开Xcode项目,点击Product->Profile:

选择Leaks,这时项目也在模拟器或真机上运行起来了:

或者直接在自己的项目中运行程序,选中Memory点击右上角的Profile in instruments:

都可以进入下面的页面:

由于 Leaks 是动态监测,所以我们需要手动操作 APP,进行测试,一边操作 APP,一边观察 Leaks 的变化,在 暂停按钮 的右边 我们可以选择正在 运行的程序 & 选择设备 & App, 之后点击 红点 Record(红色圆圈按钮)运行。

观察,如果发现在 Leaks 里面有一个 红色X,这说明了我们的 APP 存在内存泄露。

就像这样

点击暂停,点击其中一个,然后我们开始分析。

定位修改

此时选中有红色叉的 Leaks,下面有个Leaks 字方格,点开,选中 Call Tree。

接着就是最关键的一步,在这个界面的右下角有若干选框,选中Invert Call Tree(从上到下跟踪堆栈信息) 和 Hide System Libraries(表示隐藏系统的函数)

在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。

Leaks界面分析

Leaks 启动后会开始录制,随着对模拟器运行的 App 的操作,可以在 Leaks 中查看内存占用的情况。

Leaks顶部分为两栏:Allocations 和 Leaks,右侧的曲线代表内存分配和内存泄漏曲线。

Call Tree的四个选项:
  • Separate By Thread: 线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
  • Invert Call Tree: 从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中话费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
  • 表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
  • 递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

内存泄漏原因分析

在目前主要以ARC进行内存管理的开发模式,导致内存泄漏的根本原因是代码总存在循环引用,从而导致一些内存无法释放,这就会导致dealloc方法无法被调用。

开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject

使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的。

引起内存泄漏的几种原因:

1. Leaked Memory:应用程序未引用的、不能再次使用或释放的内存。

  • 在OC的ARC机制下,使用CF或CG对象时,忘记手动调用CFReleaseCGRelease

    objectivec 复制代码
        CGImageRef* imageRef = CGImageCreateWithImageInRect(someImage, someRect);
    // ...
    //    CGImageRelease(imageRef); //  释放内存
  • for循环中超多次加载比较占内存的对象:频繁创建大量占用内存的对象,如果不使用@autorelease,会导致内存无法及时释放。

    objectivec 复制代码
    for (int i = 0; i < 1000; ++i) {
       @autoreleasepool {
           UIImage* image = [UIImage imageNamed: @"largeImage"];
           // ...
       }
    }

2. Abandoned Memory: 内存仍被应用程序所引用,没有任何有用的用途。

大多是OC对象、block、timer、delegate等循环引用问题,造成引用计数一直不为零。

  • OC对象循环引用:

    objectivec 复制代码
    @interface Person : NSObject
    @property (nonatomic, strong)Person* friends;
    @end
    
    @implementation Person
    - (void)dealloc {
        NSLog(@"Person 对象被释放");
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person* person1 = [[Person alloc] init];
            Person* person2 = [[Person alloc] init];
            person1.friends = person2;
            person2.friends = person1;
        }
        return 0;
    }

    person1和person2互相引用,形成了循环(强)引用,这两个对象的引用计数不会降为0,dealloc方法并没有被执行。

    解决办法就是使用弱引用 打破循环:@property (nonatomic, weak)Person* friends

    • Block循环引用:
    objectivec 复制代码
    @interface MyClass : NSObject
    @property (nonatomic, strong) void (^myBlock)(void);
    - (void)setupBlock;
    @end
    
    @implementation MyClass
    
    - (void)setupBlock {
        //  __weak typeof(self) weakSelf = self;
        self.myBlock = ^{
            //  __strong typeof(weakSelf) strongSelf = weakSelf;
            NSLog(@"%@", self); // 造成强引用循环
            //  if (strongSelf) {
            //      NSLog(@"%@", strongSelf);
            //  }
        };
    }
    
    @end
    
    MyClass *obj = [[MyClass alloc] init];
    [obj setupBlock];

    如果没有注释掉的那段代码,self.myBlockself产生了强引用,导致self的引用计数永远不会为零,从而引起循环引用。

  • Timer循环引用:

    objectivec 复制代码
    @interface MyClass : NSObject
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation MyClass
    - (void)startTimer {
        // __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
    }
    
    - (void)timerFired {
        // Timer fired actions
    }
    @end

    如果没有注释掉的那段代码,NSTimerself保持强引用(target: self增加了ViewController的引用计数,如果不进行[timer invalidate];,就别想调用dealloc了),而selfNSTimer也保持强引用,形成循环引用。
    timer属性也最好设置为弱引用(weak)。

  • Delegate引起的循环引用:

    objectivec 复制代码
    @interface MyObject : NSObject
    @property (nonatomic, strong)id<MyDelegate> delegate;
    @end
    
    @interface MyViewController : UIViewController <MyDelegate>
    @property (nonatomic, strong) MyObject *myObject;
    @end
    
    @implementation MyViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myObject.delegate = self;
    }
    @end

    与Timer同理,myObjectdelegate使用强引用,delegate又对myObject保持强引用,形成循环引用。

    解决方案是使用弱引用:@property (nonatomic, weak)id<MyDelegate> delegate;

  • ViewController的子视图对self的持有
    我们有时候需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,这样跳转之后的back键才能直接返回该页面,同时也不销毁当前ViewController。此时,你就要注意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法释放。

3. Cached Memory:内存仍然由应用程序引用,可以再次使用以获得更好的性能。

为了快速访问而存储起来的对象。

以缓存图片提高性能为例:

objectivec 复制代码
@class UIImage;
@interface ImageCache : NSObject
@property (nonatomic, strong) NSMutableDictionary* cache;
+ (instancetype)sharedInstance;
- (UIImage *)imageForKey:(NSString *)key;
- (void)setImage:(UIImage *)image forKey:(NSString *)key;
@end

//  ImageCache.m
#import "ImageCache.h"
#import "UIKit/UIImage.h"

@implementation ImageCache

+ (nonnull instancetype)sharedInstance {
    static ImageCache *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
        sharedInstance.cache = [NSMutableDictionary dictionary];
    });
    return sharedInstance;
}

- (void)setImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key {
    self.cache[key] = image;
}

- (nonnull UIImage *)imageForKey:(nonnull NSString *)key {
    return self.cache[key];
}

@end

// 使用缓存
UIImage* image = [UIImage imageNamed: @"example.png"];
[[ImageCache sharedInstance] setImage: image forKey: @"example"];

// 之后访问缓存的图片
UIImage* cachedImage = [[ImageCache sharedInstance] imageForKey: @"example"];
 ```
示例中,图片被缓存以便快速访问,从而提高性能。缓存图片使用的内存不是泄漏,因为这戏内存是有意保留以供将来使用的。
相关推荐
SoraLuna8 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
阿7_QuQ8 小时前
怎么在Windows上远程控制Mac电脑?
macos
小路恢弘14 小时前
使用Mac自带共享实现远程操作
macos
恋猫de小郭14 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨18 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题20 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
阿髙1 天前
macos 隐藏、加密磁盘、文件
macos
minos.cpp1 天前
Mac上Stable Diffusion的环境搭建(还算比较简单)
macos·ai作画·stable diffusion·aigc
追光天使2 天前
Mac/Linux 快速部署TiDB
linux·macos·tidb
wzkttt2 天前
Mac gfortran编译fortran出错
macos·gfortran