@qidiandasheng
2022-08-23T09:28:02.000000Z
字数 6742
阅读 4058
性能优化
关于线下性能监控,苹果公司官方就有一个性能监控工具 Instruments。它是一款被集成在 Xcode 里,专门用来在线下进行性能分析的工具。
Instruments 的功能非常强大,比如说 Energy Log 就是用来监控耗电量的,Leaks 就是专门用来监控内存泄露问题的,Network 就是用来专门检查网络情况的,Time Profiler 就是通过时间采样来分析页面卡顿问题的。
如下图所示,就是 Instruments 的各种性能检测工具。
通常是拿来分析内存增加(不一定是内存泄漏)和app中各部分占用内存问题,当我们得知哪个内存占用比较多,我们直接进行优化即可减少内存占用问题。
这是我创建的一个ViewController
,我们后面的测试会通过不断的在主页push和pop这个controller来看一个内存的变化。
@implementation DSOneViewController
- (void)viewDidLoad {
[super viewDidLoad];
[UIColor redColor];
self.myNames = [NSMutableArray array];
for (int i=0; i<100000; i++) {
// 这个字符串对象占用48个字节
NSString *num = [NSString stringWithFormat:@"测试字符串:%d",i];
[self.myNames addObject:num];
}
}
@end
默认选择的是Statistics
,表示一个All Heap
& Anonymous VM
的内存使用量。如下图1部分表示一个总的内存,2部分表示单独某一类对象占用的内存:
App运行过程中在堆上分配的内存,几乎所有类实例,包括 UIViewController、UIView、UIImage、Foundation 和我们代码里的各种类/结构实例。一般和我们的代码直接相关。
都是由VM:开头的,无法由开发者直接控制,主要包含一些系统模块的内存占用。有些部分虽然看起来离我们的业务逻辑比较远,但其实是保证我们代码正常运行不可或缺的部分,也是我们常常忽视的部分。
例如图片之类的大内存,属于All Anonymous VM -> VM: ImageIO_IOSurface_Data,其他的还有IOAccelerator与IOSurface等跟GPU关系比较密切的。
CVPixelBuffer: An image buffer that holds pixels in main memory.
A Core Video pixel buffer is an image buffer that holds pixels in main memory. Applications generating frames, compressing or decompressing video, or using Core Image can all make use of Core Video pixel buffers.
主要是CVPixelBuffer,通常使用Pool来管理,交给系统自动释放。而释放的时机完全由系统决定,开发者无法控制。
VM: IOSurface
IOSurface是用于存储FBO、RBO等渲染数据的底层数据结构,是跨进程的,通常在CoreGraphics、OpenGLES、Metal之间传递纹理数据。该结构和硬件相关。提供CPU访问VRAM的方式,如创建IOSurface对象后,在CPU往对象里塞纹理数据,GPU就可以直接使用该纹理了。可以简单理解为IOSurface,为CPU和GPU直接搭建了一个传递纹理数据的桥梁。
Share hardware-accelerated buffer data (framebuffers and textures) across multiple processes. Manage image memory more efficiently.
The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries. It is commonly used to allow applications to move complex image decompression and draw logic into a separate process to enhance security.
VM: Stack
调用堆栈,一般不需要做啥。每个线程都需要500KB左右的栈空间,主线程1MB。
VM: CG raster data
光栅数据,即为UIImage的解码数据。SDWebImage将解码数据做了缓存,避免渲染时候在主线程解码而造成阻塞。
把列表展示类型切换成Call Trees
,能够非常清晰的看到调用树。采集的是分配内存相关的方法调用。
DSOneViewController
循环10万次创建String,那就是分配了10万次内存(被释放之后Count也会对应减少)注意:这里显示的方法表示占用的内存是在这个方法里申请的,不代表这个方法对应的实例未被释放。比如我在这个方法里创建了一个单例,当我们这个方法对应的实例释放的时候,我们还能看到列表中有这个实例对应的方法,这就是告诉你之前这个单例是在这个方法里创建的。
底部Call Tree
选择Invert Call Tree
和 Hide System Libraries
,用来筛选处理调用树,看起来更加简单明了。
首页还没进入DSOneViewController
的时候
接着push进DSOneViewController
,我们看到主要是新增了1和2两处的内存
双击1和2两处的方法,显示如下:
pop回首页,我们发现之前push进DSOneViewController
页时的两个方法还在,只是前面显示的内存占用变少了,说明有部分在这两个方法里创建的内存一直都在。
以[DSOneViewController viewDidLoad]
方法为例,我们看到[UIColor redColor]
占用的内存为64 Bytes,刚好跟外面显示的一样,这是因为我们第一次调用了这个系统方法,这个方法和类对象会在第一次调用的时候缓存到内存中供下次使用。
push进DSTwoViewController
,里面也有一个[UIColor blackColor]
的调用,我们发现Call Tree
列表中并没有DSTwoViewController
相关的内存创建,因为DSOneViewController
已经调用过[UIColor blackColor]
,所以不用再创建对象或方法了。只是首页的tableView: didSelectRowAtIndexPath:
方法我们能看到增加了一点内存,也就是我们创建DSTwoViewController
实例的内存,当pop回去的时候也会被释放。
@implementation DSTwoViewController
- (void)viewDidLoad {
[super viewDidLoad];
[UIColor blackColor];
}
@end
修改DSTwoViewController
代码,加入第一次调用的UIFont
方法,再次push进去看看。我们发现这次Call Tree
列表中出现了[DSTwoViewController viewDidLoad]
方法,并且里面的[UIFont systemFontOfSize:12]
申请了内存([UIFont systemFontOfSize:]也是第一次使用)。
@implementation DSTwoViewController
- (void)viewDidLoad {
[super viewDidLoad];
[UIColor blackColor];
[UIFont systemFontOfSize:12];
}
@end
注意:
同一个类的不同方法都会申请函数内存,比如[UIFont fontWithName:@"Arial-BoldMT" size:15];
和[UIFont systemFontOfSize:12];
。但[UIColor blackColor];
和[UIColor redColor];
却不会,估计是调用的同一个方法。
点击Mark Generation
时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。
比如我进入DSOneViewController
前点击了一下创建快照A,然后push进DSOneViewController
等字符串创建完毕之后又点击了一下创建快照B,如下图所示显示了这段时间内新分配的内存:
如图所示我们在快照B期间一共创建了100000个字符串,每个48字节,48*100000/1024/1024=4.577 M,四舍五入跟我们图中显示分配的内存一样。
记住这个快照是动态的,我们在pop回首页的时候,创建快照C,我们看到快照B内分配的一些内存被释放了:
使用场景:
我们可以不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。因此,我们在每回 push 之前跟 pop 之后,都 Mark Generation 一下,以此观察内存是不是无限制增长。这个方法在 WWDC 的视频里:Session 311 - Advanced Memory Analysis with Instruments,以及苹果的开发者文档:Finding Abandoned Memory 里有介绍。
用这种方法来发现内存泄露还是很不方便的:
选择Leaks,点击运行,可以看到默认分上下两部分,上面部分就是Allocations
,跟我们上面的一样。
选择下面的话就是Leaks内存泄漏检测,如果无法定位到具体代码,则修改Buidl Setting
->Debug Information Format
->Debug
里选择DWARF with dSYM File
。重新运行看到红叉出现,即是出现了内存泄漏。
下面的列表选择Call Trees
,选择Hide System Libiraries
即可显示内存泄漏具体的方法。
双击即可查看方法里面具体内存泄漏的地方:
第一次运行的时候我们能看到是可以的,当第二次直接点击暂停再重新运行的时候,虽然我们能看到内存泄漏的红叉,但无法定位到具体的方法,提示如下,大致意思就是之前已经定位过了,无法追踪调用栈,估计是有缓存,但我不知道在哪清。
暴力解决方法把instruments
关了重新打开即可。
代码运行并调用UIImage *image = [UIImage imageNamed:@"apic"];
读取本地图片之后,如下图选择Debug Memory Graph
:
然后可以点击 File->Export Memory Graph
将其导出为 memgraph
文件。
Image IO部分内存:
我上面的测试图片大小为:650*1015*4/1000=2639kb
。使用vmmap查看上面生成的memgraph
文件。
vmmap --summary DSImageBitmaps.memgraph
我们可以看到Image IO
刚好就是差不多2604K。也可以使用以下方法进行筛选:
vmmap --summary DSImageBitmaps.memgraph | grep 'Image IO'
总内存:
app的内存占用主要是Dirty Size
+Swapped Size
,我们看到这里差不多是22.1M,但是我们查看xcode的Debug Navigator
时是18.9M。
主要原因是如果你的App使用了别的进程创建的共享内存,那么Debug Navigator
是不会将它计入你自己的内存总量的,不过vmmap会将它加入TOTAL中,所以可能会导致vmmap
计算的内存量会大于Debug Navigator
统计内存量。
Facebook工程师们开源了一些自动化工具来解决监测内存泄露问题:FBRetainCycleDetector
、FBAllocationTracker
、FBMemoryProfiler
。
原文介绍:Automatic memory leak detection on iOS
中文翻译:在iOS上自动检测内存泄露
这里有一个微信读书团队开源的工具MLeaksFinder,它可以在你程序运行期间,如果有内存泄漏就会弹出提示告诉你泄漏的地方。
具体原理如下:
我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。
具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。
- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”);
}
这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。