APP 内存泄露优化

news/2024/12/12 21:51:56/文章来源:https://www.cnblogs.com/liweihang/p/18603202

原理

https://juejin.cn/post/6864492188404088846

分析

我的APP主要的VC路径如下:

如果没有内存泄露的话,我们从一个VC_A开始push一个VC_B,无论在VC_B操作了什么,pop回到VC_A,这个时候的内存大小应该和VC_A在puhs VC_B的时候是一样大的。
如图:

页面结构:曲谱列表 push 曲谱详情 ->....->  pop 曲谱列表
内存大小: x1            x2                    Y

下面我们用第一个工具开始大概检测一下是否存在问题:

Memory Report

运行程序,打开入口:
image
Memory Report 是可以实时查看整个应用当前应用内存使用情况的工具,但是它只能用于初略得定位哪些页面有可能有内存泄漏,或者哪个时间段有内存抖动问题。具体的定位还是需要Allocations工具。Memory Report工具不是很准,我们后面会提到。

开始测试

目前来看:在曲谱列表里面内存是40Mb,我们打开曲谱编辑VC退出看下内存变化:
image
我们可以看出来,内存大小没有恢复push前的,说明可能存在内存泄露,不过我们前面说了,这个工具检测可能不准,我们用Allocations来确认。

Allocations

入口:
打开Instruments
image
创建一个空的。
image
添加Allocations
image
左上角选好设备和应用,开始运行
image
pop后的内存变化不大。说明编辑曲谱VC的内存没有得到释放。
这里简单介绍一下Allocations的用法
image

All Heap & Anonymous VM: 所有堆内存和虚拟内存
All Heap Allocations: 所有堆内存,堆上malloc分配的内存,不包过虚拟内存区域
All Anonymous VM: 所有虚拟内存,就是Allocations不知道是你哪些代码创建的内存,也就是说这里的内存你无法直接控制。像memory mapped file,CALayer back store等都会出现在这里。这里的内存有些是你需要优化的,有些不是。

每行都包含如下几个重要的列:
image

Persistent :未释放的对象个数
Persistent Byte :未释放的字节数
Transient       :已释放的临时对象个数
Total Byte      :总使用字节数
Total           :所有对象个数
Persistent/Total Bytes : 已经使用的内存对象占全部的百分比

不同的数据视图:
image

Statistics:显示程序运行期间的统计数据。包括分配的对象数量、总内存使用量、内存峰值等。
Call Trees:显示函数调用栈的层级结构,按线程或调用关系组织。包括每个函数的调用次数、占用的时间等。
Allocations List:列出程序中所有内存分配的对象详细信息。包括对象的类型、大小、分配时间、释放状态等。
Generations:通过“代”的概念,跟踪不同时间点内存分配的对象。显示每一代中创建的对象及其生命周期。场景总结
Statistics:用于获取总体内存使用的概览。
Call Trees:用于分析代码的性能瓶颈。
Allocations List:用于详细检查内存分配的对象。
Generations:用于分析内存泄漏及对象生命周期。

我们这里如果想看在push的时候是代码是在什么地方申请了内存。
image
第1步,我们切换到call Tree视图。
第2步,我们框出这个push的时间段,可以明显看出有内存占用升高。

这个时候显示如下:
image

非常不好观察:(1)有系统调用, (2)函数是从栈底显示的就是从main开始一层一层到真正申请的内存的函数。
我们打开2个开关:
image

1. Separate by Category
功能:
按类别分离调用树。
将调用分组到不同的类别中,例如系统库、用户代码、动态库等。
用途:
帮助开发者快速区分调用源头(系统库或用户代码)。
对于大型项目,可以清晰地查看代码的调用类别。2. Separate by Thread
功能:
按线程分离调用树。
将调用树分组到不同的线程上,显示每个线程的调用栈。
用途:
帮助分析多线程程序中不同线程的性能瓶颈。
定位哪一个线程占用最多的资源或导致了问题。3. Invert Call Tree
功能:
颠倒调用树,将调用树的叶子节点(最底层函数)显示在顶层。
用途:
快速查看哪个底层函数耗时最多。
对于复杂的调用链,可以直接定位到性能瓶颈的根本原因,而不用从顶部逐层展开。4. Hide System Libraries
功能:
隐藏系统库的调用栈。
仅显示用户代码相关的调用。
用途:
隐藏与用户代码无关的系统库调用栈。
减少干扰,更专注于优化用户代码。5. Flatten Recursion
功能:
将递归调用压平,显示为一层。
即使某些函数递归调用了多次,也只显示一次。
用途:
简化递归调用的展示,避免递归调用链太深导致难以阅读。
快速了解递归调用的总体性能影响。
选项适用场景
时间和性能分析如果你正在分析代码性能瓶颈,以下选项非常有用:
Invert Call Tree:快速找到最耗时的底层函数。
Hide System Libraries:只关注用户代码,忽略系统调用。多线程优化
在多线程程序中,以下选项可帮助分析线程间问题:
Separate by Thread:区分不同线程的调用栈,发现某些线程可能占用过多资源。
代码复杂度管理对于递归算法或复杂代码:
Flatten Recursion:清晰了解递归函数的整体影响。
Separate by Category:区分系统库和用户代码调用链。推荐使用方式
初步分析:
开启 Hide System Libraries,专注于用户代码。
启用 Invert Call Tree,从底层函数开始找耗时热点多线程项目:
开启 Separate by Thread,观察不同线程的资源占用。递归代码:
启用 Flatten Recursion,避免调用树过于复杂。
通过调整这些选项,可以快速定位性能问题并优化代码。

现在我们再看,就可以知道是我的collectionView创建了大量collectionViewcell,collectionViewcell又创建了大量的ChordItemButtonMin,导致内存占用升高。
image

Leaks 内存泄漏检测工具

我们检查一下是否有内存泄露
image

运行操作刚才的步骤,发现没有检测到内存泄露。
image

感觉Leaks对野指针更有效,对于循环引用无法判断是否是程序员自己的逻辑,还是bug导致。

Debug Memory Graph 图形化内存表

Debug Memory Graph 是Xcode8中增加的调试技能,在App运行调试过程中,点击即可实时看到内存的分配情况以及引用情况,可用于发现部分循环引用问题,为了能看到内存详细信息,需要打开Edit Scheme–>Diagnostics, 勾选 Malloc Scribble 和 Malloc Stack。同时在 Malloc Stack 中选择 Live Allocations Only:
image
image

开始调试,我们运行APP,然后从

曲谱列表 push 曲谱详情 push 编辑曲谱,然后pop2次回到曲谱列表。

image

优化block循环引用

这个搜索(编辑曲谱VC)MusicViewController,发现还有一个实例没有释放。
我们看是谁强引用了它:
image
是MusicCollectionViewCell的block强引了。我们想看代码在什么地方进行了强引用:
image
image

这里MusicViewController被block强引用,形成循环引用,所以pop后MusicViewController无法释放。
image

修改代码:

@weakify(self);  // 将 self 弱引用化,生成 weak_self
cell.buttonActionBlock = ^{@strongify(self); // 将 weak_self 转为强引用,恢复为 self// 将新的模型插入到数据数组的倒数第二个位置[[VibrationManager sharedManager] vibrateWithCurrentSetting];NSUInteger insertIndex = self.MusicModel.data.count >= 1 ? self.MusicModel.data.count - 1 : self.MusicModel.data.count;[self addCellModel:insertIndex];
};@weakify(self);和@strongify(self);是我自定义包装,可以用下面的代码,效果是一样的。__weak typeof(self) weakSelf = self; // 将 self 转为弱引用,避免循环引用
cell.buttonActionBlock = ^{__strong typeof(weakSelf) strongSelf = weakSelf; // 在 block 内部将弱引用提升为强引用if (!strongSelf) return; // 如果 strongSelf 为 nil,直接返回// 将新的模型插入到数据数组的倒数第二个位置[[VibrationManager sharedManager] vibrateWithCurrentSetting];NSUInteger insertIndex = strongSelf.MusicModel.data.count >= 1 ? strongSelf.MusicModel.data.count - 1 : strongSelf.MusicModel.data.count;[strongSelf addCellModel:insertIndex];
};

现在的引用图:
image

我们再次运行,通过Allocations来检测:
image
每次回到曲谱列表页面,内存占用大小也恢复到之前的大小。说明MusicViewController在pop得到释放。我们可以在去Debug Memory Graph验证:

我们再执行一次,打开Debug Memory Graph,搜索MusicViewController:
image
发现在内存中没有了。

优化定时器循环引用

image
我们刚才确定了前3个VC没有内存泄露,现在我们在检测第4个VC:曲谱播放VC
还是一样的步骤,先用Allocations来检测。
我们从详情页push2次又pop2次,内存都回到原来的状态。

发现是正常的。

现在我为了模拟定时器循环引用,我改了一下代码
再次检测,我们从详情页push2次又pop2次。发现内存没有回到之前的状态。说明曲谱播放VC(PlayMusicViewController)也发生内存泄露。并且很可能是循环引用
image
我们打开Debug Memory Graph验证:
image
发现了是定时器强引用了PlayMusicViewController
image
image

解决办法是方法1:

在退出的时候,显式调用[self.displayLink invalidate];
这个会主动让displayLink释放对Target的强引用。优点:写法简单
缺点:必须在所有pop路径添加这个调用,不然就会循环引用。

我们看方法:
考虑添加一个中间对象作为Target,通过消息转发把定时器的回调转发到PlayMusicViewController
image
这样就算没有在pop的时候调用[self.displayLink invalidate];也不会循环引用。

// MJProxy.h
#import <Foundation/Foundation.h>@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (nonatomic, weak) id target; // 弱引用目标对象,避免循环引用
@end// MJProxy.m
#import "MJProxy.h"@implementation MJProxy+ (instancetype)proxyWithTarget:(id)target {MJProxy *proxy = [MJProxy alloc]; // NSProxy 没有 init 方法,直接 allocproxy.target = target;return proxy;
}// 消息转发目标
//- (id)forwardingTargetForSelector:(SEL)aSelector {//return self.target; // 直接转发给目标对象
//}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {return [self.target methodSignatureForSelector:aSelector];
}- (void)forwardInvocation:(NSInvocation *)invocation {// NSLog(@"拦截到未实现的方法:%@", NSStringFromSelector(invocation.selector));[invocation invokeWithTarget:self.target];
}@end

使用的地方修改:

// 第一次播放
- (void)playMusic {// 滚动 tableView 使目标行可见[self scrollToCellAtIndex:self.playbackProgress.cellIndex -1];[self.currentCellView updateBeatBackView:self.playbackProgress.beatIndex];// 使用 MJProxy 代理 self,避免 CADisplayLink 对 self 的强引用self.displayLink = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self]selector:@selector(update:)];self.displayLink.preferredFramesPerSecond = 60; // 设置刷新率[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

为什么用NSProxy子类作为中间对象?
答:NSProxy专门用来信息转发,objc_magSend不用走信息发送方法解析直接走消息转发

为什么MJProxy不通过forwardingTargetForSelector快速转发?
答:有的方法不通过forwardingTargetForSelector,例如:
isClassisMemberOfClass

- (BOOL)isMemberOfClass:(Class)aClass {// 声明方法签名和调用对象NSMethodSignature *sig;NSInvocation *inv;BOOL ret;// 获取当前方法的签名sig = [self methodSignatureForSelector:_cmd];// 创建 NSInvocation 对象inv = [NSInvocation invocationWithMethodSignature:sig];// 设置选择器 (即 isMemberOfClass:)[inv setSelector:_cmd];// 设置参数[inv setArgument:&aClass atIndex:2];// 转发消息[self forwardInvocation:inv];// 获取返回值[inv getReturnValue:&ret];// 返回结果return ret;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VMware ESXi 8.0U3c macOS Unlocker OEM BIOS 2.7 集成网卡驱动和 NVMe 驱动 (集成驱动版)

VMware ESXi 8.0U3c macOS Unlocker & OEM BIOS 2.7 集成网卡驱动和 NVMe 驱动 (集成驱动版)VMware ESXi 8.0U3c macOS Unlocker & OEM BIOS 2.7 集成网卡驱动和 NVMe 驱动 (集成驱动版) 发布 ESXi 8.0U3 集成驱动版,在个人电脑上运行企业级工作负载 请访问原文链接:…

12.12实验八:随机森林算法实现与测试

实验八:随机森林算法实现与测试 一、实验目的 深入理解随机森林的算法原理,进而理解集成学习的意义,能够使用 Python 语言实现随机森林算法的训练与测试,并且使用五折交叉验证算法进行模型训练与评估。二、实验内容 (1)从 scikit-learn 库中加载 iris 数据集,使用留出法…

VMware ESXi 8.0U3c 发布 - 领先的裸机 Hypervisor

VMware ESXi 8.0U3c 发布 - 领先的裸机 HypervisorVMware ESXi 8.0U3c 发布 - 领先的裸机 Hypervisor 同步发布 Dell (戴尔)、HPE (慧与)、Lenovo (联想)、IEIT SYSTEMS (浪潮信息)、Cisco (思科)、Fujitsu (富士通)、Hitachi (日立)、NEC (日电)、Huawei (华为)、xFusion (超聚…

VMware ESXi 8.0U3c macOS Unlocker OEM BIOS 2.7 标准版和厂商定制版

VMware ESXi 8.0U3c macOS Unlocker & OEM BIOS 2.7 标准版和厂商定制版VMware ESXi 8.0U3c macOS Unlocker & OEM BIOS 2.7 标准版和厂商定制版 ESXi 8.0U3 标准版,Dell (戴尔)、HPE (慧与)、Lenovo (联想)、IEIT SYSTEMS (浪潮信息)、Cisco (思科)、Fujitsu (富士通…

使用Flex布局的几个小技巧

前情 Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性,任何一个容器都可以指定为 Flex 布局,如果说目前我开发中离不开的布局方式那就非Flex莫属了,而且小程序就是推荐使用Flex布局的,对 Grid布局的支持不太理想。 在一次次使用fl…

C#学习笔记(一) Array学习笔记

之前一直学习各种基础知识,光学习,没有总结,趁着有时间,总结总结C#有关知识 Array类是最基础的数组类,官方文档截图如下:Array是一个抽象类,不能实例化,只能使用里面的方法,属性。 Array 类不是 System.Collections 命名空间的一部分。 但是,它仍被视为集合,因为它基…

VMware ESXi 7.0U3r macOS Unlocker OEM BIOS 2.7 标准版和厂商定制版

VMware ESXi 7.0U3r macOS Unlocker & OEM BIOS 2.7 标准版和厂商定制版VMware ESXi 7.0U3r macOS Unlocker & OEM BIOS 2.7 标准版和厂商定制版 ESXi 7.0U3 标准版,Dell (戴尔)、HPE (慧与)、Lenovo (联想)、Inspur (浪潮)、Cisco (思科)、Fujitsu (富士通)、Hitachi…

Ubuntu 20.04虚拟机无法上网,无法访问主机

查版本 Ubuntu基于Debian发行版和GNOME桌面环境 Xubuntu(发音为ZOO-bun-too)是一个Ubuntu Linux的官方派生版,它基于桌面环境Xfce,主要运行基于GTK+的程序,面向旧式电脑的用户和寻求更快捷的桌面环境的用户。它与Ubuntu不完全相同,但使用Ubuntu的高质量软件源。 /etc/net…

帮助用户与 AI 实时练习口语,Speak 为何能估值 10 亿美元?丨Voice Agent 学习笔记

👋活动招募:如果你正在关注 Voice Agent 的开发和创业,欢迎线上参与本周日(15 日)的 Voice Agent 开发者分享会。AI 语言学习公司 Speak 近日宣布完成 7800 万美元 C 轮融资,由 Accel 领投,OpenAI 创业基金、Khosla Ventures、Y Combinator 等顶级投资机构参投。此轮融…

12C++循环结构-for循环(2)——教学

1、循环变量为字符型; 2、打擂台-for语句的另一种形式; 3、break语句; 4、数位分离一、循环变量为字符型 试编一程序,按字典顺序输出26个字母。 流程图:思考:先顺序输出26个小写英文字母,再逆序输出26个大写英文字母。 循环可以是递增型循环,也可以是递减型循环。二、打…

沈阳在线教育系统官网

针对教育和软件行业的关注,在不断发展的市场环境下,我们探讨一种新兴的趋势,即在线教育领域如何更高效、更个性化地提供优质的教育服务,尤其是利用知识付费在线教育系统的创新解决方案。图源 凸知@www.tuzhi.ltd在当前社会背景下,随着信息技术的突飞猛进,以及人们对于优质…

【PHP样本】反序列化ctf题

题目: <?phphighlight_file(__FILE__);class ease{private $method;private $args;function __construct($method, $args) {$this->method = $method;$this->args = $args;}function __destruct(){if (in_array($this->method, array("ping"))) {call_…