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

news2024/11/26 18:43:18

目录

    • 为什么要检测内存泄漏?
    • 什么是内存泄漏?
    • 内存泄漏排查方法
      • 1. 使用Zombie Objects
      • 2. 静态分析
      • 3. 动态分析方法
        • 定位修改
        • Leaks界面分析
        • Call Tree的四个选项:
    • 内存泄漏原因分析
      • 1. Leaked Memory:应用程序未引用的、不能再次使用或释放的内存。
      • 2. Abandoned Memory: 内存仍被应用程序所引用,没有任何有用的用途。
      • 3. Cached Memory:内存仍然由应用程序引用,可以再次使用以获得更好的性能。

前言

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

为什么要检测内存泄漏?

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

什么是内存泄漏?

内存泄漏指的是一块内存被分配后不再使用,但是没有被程序正确释放回系统,从而导致该内存继续占用在程序中,无法被其它任务使用。
这通常发生在使用了动态内存分配但未及时或正确释放,或者由于编程逻辑错误导致。如果不加以管理,会导致程序消耗过多的内存,甚至导致应用程序崩溃。

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

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:

请添加图片描述

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

测试代码:

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

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

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

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

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

  • OC对象循环引用:

    @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循环引用:

    @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循环引用:

    @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引起的循环引用:

    @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:内存仍然由应用程序引用,可以再次使用以获得更好的性能。

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

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

@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"];
 ```
示例中,图片被缓存以便快速访问,从而提高性能。缓存图片使用的内存不是泄漏,因为这戏内存是有意保留以供将来使用的。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1805515.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CTFHUB-SQL注入-字符型注入

目录 查询数据库名 查询数据库中的表名 查询表中数据 总结 此题目和上一题相似&#xff0c;一个是整数型注入&#xff0c;一个是字符型注入。字符型注入就是注入字符串参数&#xff0c;判断回显是否存在注入漏洞。因为上一题使用手工注入查看题目 flag &#xff0c;这里就不…

基于微信小程序的社区志愿者服务平台 _8xh87【已测试】

前言&#xff1a;&#x1f469;‍&#x1f4bb; 计算机行业的同仁们&#xff0c;大家好&#xff01;作为专注于Java领域多年的开发者&#xff0c;我非常理解实践案例的重要性。以下是一些我认为有助于提升你们技能的资源&#xff1a; &#x1f469;‍&#x1f4bb; SpringBoot…

轻松掌握系统概况,提升工作效率

作为 Linux 系统管理员,我们经常需要了解系统的基本状况,比如当前时间、系统版本、内核信息、CPU 型号、内存使用等等。但是每次手动执行各种命令来获取这些信息,无疑是一件非常繁琐的事情。 幸运的是,我们可以通过编写一个简单的 shell 脚本来一键获取这些系统信息。让我们一…

Linux用户和用户组的管理

目录 前言一、系统环境二、Linux用户组的管理2.1 新增用户组2.2 删除用户组2.3 修改用户组2.4 查看用户组 三、Linux用户的管理3.1 新增用户3.2 删除用户3.3 修改用户3.4 查看用户3.5 用户口令&#xff08;密码&#xff09;的管理 总结 前言 本篇文章介绍如何在Linux系统上实现…

Aethir: 破局算力瓶颈,构建AI时代去中心化云基础设施

科技的每一次飞跃都在重新塑造世界&#xff0c;而近年来&#xff0c;跨越式的技术革新再次引发了深刻的变革&#xff0c;那就是人工智能&#xff08;AI&#xff09;。 人工智能已然超越了此前的所有技术概念&#xff0c;成为了继互联网之后的下一个巨大浪潮。从自动驾驶汽车到…

ipv6有状态分配地址

RA报文M/O标志位 设备在获取IPv6地址等信息时&#xff0c;会先发送RS报文请求链路上的路由设备&#xff0c;路由设备受到RS报文后会发送相应的RA报文来表示自身能够提供的IPv6服务类型。 对于RA报文&#xff0c;根据其M字段和O字段确定其获取IPv6地址的模式&#xff1a; M/O都…

python后端结合uniapp与uview组件tabs,实现自定义导航按钮与小标签颜色控制

实现效果&#xff08;红框内&#xff09;&#xff1a; 后端api如下&#xff1a; task_api.route(/user/task/states_list, methods[POST, GET]) visitor_token_required def task_states(user):name_list [待接单, 设计中, 交付中, 已完成, 全部]data []color [#F04864, …

电脑缺失msvcp110.dll文件的解决方法,总结5种靠谱的方法

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“找不到msvcp110.dll”。这个错误提示通常出现在运行某些软件时&#xff0c;那么&#xff0c;它究竟会造成哪些问题呢&#xff1f; 一&#xff0c;msvcp110.dll文件概述 msvcp110.dll是Mic…

各种空气能热泵安装图

空气能热泵安装图 循环式空气能热泵安装图 直热循环式空气能热泵安装图 泳池空气能热泵安装图 循环式水源热泵热安装系统原理图 直热循环式水源热泵安装系统图 空气水源热泵安装图

sqli-labs 靶场 less-8、9、10 第八关到第十关详解:布尔注入,时间注入

SQLi-Labs是一个用于学习和练习SQL注入漏洞的开源应用程序。通过它&#xff0c;我们可以学习如何识别和利用不同类型的SQL注入漏洞&#xff0c;并了解如何修复和防范这些漏洞。Less 8 SQLI DUMB SERIES-8判断注入点 当输入id为1时正常显示&#xff1a; 加上单引号就报错了 …

2024年【天津市安全员C证】免费试题及天津市安全员C证试题及解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 天津市安全员C证免费试题是安全生产模拟考试一点通生成的&#xff0c;天津市安全员C证证模拟考试题库是根据天津市安全员C证最新版教材汇编出天津市安全员C证仿真模拟考试。2024年【天津市安全员C证】免费试题及天津市…

【云原生】基于windows环境搭建Docker

目录 一、Docker Desktop搭建 二、前置准备 2.1开启 Hyper-V 2.2 Hyper-V选项看不到问题解决 2.3 开启或升级wsl 三、安装过程 3.1 下载安装包 3.2 安装 Docker Desktop 3.2.1 Docker 图标一直处于starting状态问题解决 3.3 配置仓库与镜像 3.4 docker功能测试 四、…

coap-emqx:使用libcoap与emqx通信

# emqx开启CoAP网关 请参考【https://blog.csdn.net/chenhz2284/article/details/139562749?spm1001.2014.3001.5502】 # 写一个emqx的客户端程序&#xff0c;不断地往topic【server/1】发消息 【pom.xml】 <dependency><groupId>org.springframework.boot<…

配置免密登录秘钥报错

移除秘钥&#xff0c;执行 ssh-keygen -R cdh2即可 参考&#xff1a;ECDSA主机密钥已更改,您已请求严格检查。 - 简书

【C语言】宏详解(上卷)

前言 紧接着预处理详解&#xff08;上卷&#xff09;&#xff0c;接下来我们来讲宏&#xff08;隶属于预处理详解系列&#xff09;。 #define定义宏 #define机制包括了一个规定&#xff0c;允许把参数替换到文本中&#xff0c;这种实现通常称为宏&#xff08;macro&#xff…

步态控制之足旋转点(Foot Rotation Indicator, FRI)

足旋转点(Foot Rotation Indicator, FRI) 足旋转点是人形机器人步态规划中的一个关键概念,用于描述步态过程中机器人脚部的旋转和稳定性。FRI 可以帮助确定机器人在行走时是否稳定,以及如何调整步态以保持稳定。下面详细介绍FRI的原理,并举例说明其应用。 足旋转点(FRI…

Word多级标题编号不连续、一级标题用大写数字二级以下用阿拉伯数字

Word多级标题编号不连续 &#xff1a; 一级标题用大写数字二级以下用阿拉伯数字&#xff1a;

Qt Designer 生成的 .ui 文件转为 .py 文件并运行

1. 使用使用 PyUIC将 .ui 转 .py &#xff08;1&#xff09;打开命令行终端&#xff08;可以用cmd&#xff0c;或pycharm 下面的 Terminal&#xff09;。 &#xff08;2&#xff09;导航到包含.ui文件的目录。 cd 你的ui文件路径 &#xff08;3&#xff09;运行以下命令来…

Linux -- 了解 vim

目录 vim Linux 怎么编写代码&#xff1f; 了解 vim 的模式 什么是命令模式&#xff1f; 命令模式下 vim 的快捷键&#xff1a; 光标定位&#xff1a; 复制粘贴&#xff1a; 删除及撤销&#xff1a; 注释代码&#xff1a; 什么是底行模式&#xff1f; ​编辑 ​编辑…

Java:110-SpringMVC的底层原理(上篇)

SpringMVC的底层原理 在前面我们学习了SpringMVC的使用&#xff08;67章博客开始&#xff09;&#xff0c;现在开始说明他的原理&#xff08;实际上更多的细节只存在67章博客中&#xff0c;这篇博客只是讲一点深度&#xff0c;重复的东西尽量少说明点&#xff09; MVC 体系结…