前言
在项目中,直接面向用户的客户端往往是一个项目的门面。因此,在项目开发建设的过程中,为了交付用户体验较佳的客户端App,保障产品交付质量。往往需要我们开发者关注客户端软件的性能指标问题。因此,我们要对应用的性能优化专题有所研究!!
我们通常关注的性能指标有:
页面卡顿耗电、发热网络优化应用启动安装包瘦身等
我们在开发建设项目过程中,可以粗略划分为几个阶段:开发阶段、测试阶段、维护阶段:
- 在
开发阶段,我们要掌握性能调试、性能监测的手段,从而保障,在当前稳定版本的客户端软件,有一个比较合理的性能保障; - 在
测试阶段,测试团队等若干同事往往会给我们提出一些用户体验上的反馈和建议,因此,我们需要掌握性能调试的手段,从而改造出比较符合团队要求的产品; - 在上线
维护阶段,针对已经上线的应用,我们的开发团队要有线上性能监控的能力,从而及时收集不满足性能指标要求的业务交互场景和步骤,捕获具体问题进行分析,从而以此为依据作为有效迭代优化我们客户端的有力助力。
为此,我们本次将会用几篇文章,围绕一些常见的性能指标,去关注 如何调试、如何监测、如何改进处理问题:
- Instruments
- 其它性能指标的关注
一、概述
本文主要是针对 开发阶段 、测试阶段 这两个线下场景,围绕常见的几个性能指标要点:页面卡顿、离屏渲染、耗电优化、内存泄露、App启动优化,展开来陈述如何利用Instruments工具进行性能调试的。关于相关的同一主题的其它要点,我们会在其它文章,用新的篇幅进行讨论。
二、 Instruments工具
我们前面通过一篇文章简答介绍了Instruments这个苹果官方自带的调试工具,若本篇文章是您阅读我的第一篇文章,且您对Instruments了解甚少,可以先阅读我的这篇文章先对该工具有基本的认识:Instrument简单介绍
我们通常可以右击Xcode打开Instruments工具: Xcode->Open Developer Tool->Instruments
我们还可以在Xcode打开项目的前提下,通过以下两个方式打开
Instruments:
- 按下两个键:
Command + I打开Instrument; - 或者点击:Xcode->product->profile;
三、Allocations检测工具
四、Allocations设置
右下角面板,您可在这里修改您想要查看的分配类型的相关设置。除了确保 Created & Persistent 气泡已被选中之外,没有其他什么需要提前做的事。
五、运行Allocations检测工具
该instrument工具捕获以下信息:
-
Category(类别)
All Heap & Anonymous VM堆内存+虚拟内存All Heap Allocations堆内存All Anonymous VM虚拟内存- 通常这些内存来自于一个 Core Foundation 对象、Objective-C 类、或原始 内存块(block)。
-
Persistent Bytes(净分配字节数)
- 当前已经分配内存但是仍然没有被释放的字节的总数。
- 3.
#Persistent(净分配数)- 表示正在使用内存的object的数量
-
#Transient(临时分配数)
- 表示存在过但是目前已被销毁的 object 的数量,其占用的内存已被释放。
-
Total Bytes(总分配字节数)
- 所有已经分配内存,而且包括已经被释放了的 字节的总数。
-
#Total(总分配数)
- 所有当前已经分配内存,包括已经被释放了的对象或内存 块的总数。
-
Transient(临时分配数)
- 当前和全部分配数的直方图。如上图所示当比例变化时,直方条会变颜色,Instruments 应用通常给它们标示不同的颜色来指出分配模式以便进行进一步的研 究。
六、Allocations排查内存释放不及时的问题
使用ARC的过程中,由于循环引用或者其他使用不当,会造成对象的内存没有及时释放,而这些内存在页面关闭的时候又会随着页面的生命周期一起释放,这就给我们排查“内存泄漏”带来了麻烦;
这种内存问题用Leaks是找不出问题的,因为这些内存其实只是释放不及时,而不是没有释放。
使用Instruments Allocations的Mark Generation功能能帮助我们排查出这种问题。
1. 一个典型案例
假定我们有一个页面MainViewController,打开这个页面后会开始进行视频直播,在直播的过程中会出现内存没有及时释放,比如10分钟后应用的内存占用已经达到了200MB,但是页面一关闭内存又都正确释放了。
针对这样的一个问题我们应该怎么来排查呢?
1.1 排查步骤
1、Xcode中启动Profile工具
2、选择Allocations工具,点击红色按钮开始“录制”,这时Instruments会启动我们的应用
3、打开有内存释放问题的页面,稍微等一下,等页面的常规初始化完成,比如等个10秒钟
4、从Instruments右下角的区域切换到“Display Settings” Tab页,并点击“Mark Generation”生成第一个Generation:Generation A; 这个相当于是我们判断内存没有及时释放的起始点
5、为了让没有及时释放的内存泄漏问题,累积的更多一点,第二次Mark的时间可以稍微多等久一点(等多久这个取决于你的内存问题出现所需要的时间,比如2分钟),然后再次点击“Mark Generation”,这个时候你就会发现两次Generation之间的内存增长,而这里面的增长其实就是我们所寻找的没有及时释放的内存:
6、点击小箭头查看更详细的信息,根据内存占用的比例,找到占用比例最高的,并且不断展开,直到找到我们自己的代码,这就是出问题的地方了。
2. 分析内存情况
All Heap Allocations(真实内存)只有23.02,而All Anonymous VM(虚拟内存:为程序分配的虚拟内存,当程序有需要的时候,能够及时为程序提供足够的内存空间,而不会现用现创建)高达91.06,所以手机分配给我们的内存是114.08;我们现在不检测内存泄漏(是另外一个工具),所以我们尽量优化VM(因为不是app真实占用的内存,只是系统分配的),而VM主要由以下三部分组成(我会把三部分都优化完了后再运行截图)
2.1 VM:ImageIO_PNG_Data
关于这个问题我在google中找到了解释,说到
Replace:
background.image = [UIImage imageNamed:@"*.png"];
With:
background.image = [UIImage imageWithContentsOfFile:[[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/*.png"]];
and now the ImageIO_PNG_Data's will be released when the view controller is dismissed.
我后面在我工程中搜索了一下imageNamed,确实有很多地方使用了,所以我们加载图片正确的思路应该是这样
1: 对于大的图片且偶尔需要显示的应放到工程目录下,不要放到Assets.xcassets中,并使用imageWithContentsOfFile加载不让系统缓存;
2: 对于经常需要展示的小图片放到Assets.xcassets中让系统缓存,使用imageNamed加载;
所以我改了一些地方,比如
imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:nameArr[index] ofType:@"png"]];
//imageView.image = [UIImage imageNamed:nameArr[index]];
两者的区别:
- imageNamed默认加载图片成功后会内存中缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象,如果缓存中没有找到相应的图片对象,则从指定地方加载图片然后缓存对象,并返回这个图片对象.
- 而imageWithContentsOfFile则仅只加载图片,不缓存.
2.2 使用场景
- 大图,只加载一次,也不太需要缓存,选择imageWithContentsOfFile
- 当应用程序需要加载一张比较大的图片并且使用一次性,那么其实是没有必要去缓存这个图片的,用imageWithContentsOfFile是最为经济的方式
- 在不大影响性能的情况下,选择 imageName:
- 因为大量使用imageNamed方式会增加开销CPU的时间来做这件事,所以我们需要根据特定场景慎重选择
- UIimage虽小,但使用元素较多问题会有所凸显.
VM:CG raster data
关于这个问题我在google中找到了解释,这是SDWebImage的问题
* Decompressing images that are downloaded and cached can improve peformance but can consume lot of memory.
* Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
所以我们需要在Appdelegate中设置一下
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];
VM:CoreAnimation
这个问题在google搜索的时候没有明显的答案,我们先查看一下详细列表
我们可以看到很多的地方都有这个VM问题,那我们具体看一下是哪些函数
我们可以发现都是系统的一些方法,我们好像无从下手;这时候我们可以google看看发现有很多原因都会变成这个样子,其中一个是这里
Found out that animation caused by the inner pages.
Inside the pageViewController(viewController that added to the scrollView as a page) on viewWillDisappear:(BOOL)animated method I added this
for (CALayer* layer in [self.view.layer sublayers]) {
[layer removeAllAnimations];
}
it resolved the problem
其中的Found out that animation caused by the inner pages我们还是需要在扩展信息中查看能不能定位到某个页面(我不知道怎么定位,如果你知道请一定回复我),所以我在我刚才进入过的界面全部实现了如上方法
七、看看优化结果
这时候我们重新启动Allocations,然后重复上面随意的在自己app中点击,跳转等操作,然后截图如下
我们发现一个好的现象VM:CG raster data明显减少了;同时我们得到一个坏现象,其他两项并没有明显变化,那我们继续分析什么原因,不过不要着急,我们先来看看右下方都有哪些功能
1. Record Settings
1.1 Launch Configuration for All Allocations
所有的Allocations启动如下配置: Discard unrecorded data upon stop:当用户点击停止的时候丢弃没有记录的数据,勾不勾无所谓 Discard events for freed memory:当内存被释放的时候丢弃事件(也就是列表中不显示已被释放内存所关联的事件) Only track VM allocations:只捕获虚拟内存的项,不勾,因为我们还是需要看看真实内存占用的 这里我们需要把第一、二个勾上
1.2 Launch Configuration for Heap Allocations
对所有的真实内存Allocations启动如下配置(如果你钩中了Only track VM allocations,这一栏是没办法操作的):
- Record reference counts:记录引用计数
- Identity virtual C++ objects:标记虚拟c++对象,这个钩上可以检测openGL等库
- Enable NSZombie detection:检测僵尸对象,钩上可以发现有没有对已释放的对象发送消息
1.3Recorded Types
这个我就不需要解释了,你需要捕获什么类型的事件就钩上,提供了简单的正则
2. Dispalay Settings
2.1 Track Display
决定上面将要显示什么内容
Current Bytes:显示字节数量 Allocation Density:Allocation数量 Active Allocation Distribution:新激活的Allocation数量
2.2 Generation Analysis
这个功能是非常有用的,一般是这样用的:进入一个页面前mark一下,在退出这个页面的时候再mark一下可以比较哪些内容增加了,就可以具体分析哪些内存没有被释放;比如我们要进入日程界面的时候我点了一下mark
显示了Growth(相比上一次增加的量)为27.48,也就是第一次真实内存和虚拟内存之和;我们在日程界面操作一阵子之后我们再点击mark截图
所以我们知道了我们退出日程界面内存依然还是增加了2.85,你可以点击查看具体是哪些增加了
2.3 Allocation Lifespan
需要记录哪些Allocation All Allocations:所有的 Created & Persistent:创建且存活的 Created & Destroyed:创建且被销毁的 我们目前只关心存活的,所以我们钩上第二个
2.4 Allocation Type
记录的Allocation类型 All Heap & Anonymous VM:所有真实内存和虚拟内存,我通常选这个分析 All Heap Allocations:所有真实内存 All VM Regions:所有分配过的虚拟内存 这里的选择将会影响到列表,比如我选择All VM Regions
2.5 Call Tree
这里的功能需要我们把列表展示类型切换成Call Trees,能够非常清晰的看到调用树
不过一般我们需要勾选一些选项,因为默认的实在看不出什么东西
Separate by Category:按照类别隔开,我们钩上看看效果
瞬间好看多了,我们能够清楚的看出来是哪些类别的VM
Separate by Thread:按照线程划分,我个人不是很喜欢这种划分,因为我不是很关心线程\
Invert Call Tree:反转调用,我们给一张对比图就不需要解释了
我习惯钩上,因为我能够一眼看到具体哪个方法出现了问题 Hide System Libraries:这个似乎是必钩的,因为我们目前只关心自己的方法,不关心系统的 Flatten Recursion:扁平化递归,我暂时还不知道是干嘛的,网上也没有搜到,不过我还是钩上了,听着是一个不错的功能
2.6 Call Tree Constraints
这个我就不需要讲了,是对列表中的数据进行过滤,可以是数量和大小;比如我只关心100以内的数据
2.7 Data Mining
数据挖掘,这是一个很具有噱头的功能;官网给了这么一个解释
Allows you to filter through the collected data for specific symbols and libraries.
就是可以过滤掉你不看的库、符号调用
点击Symbol、Library会自动把你选中的行的符号、库加到小框中
符号和库有两个选项,就是是否过滤改行;点击Restore会去掉小框中的选中行,比如我们把帮帮管理助手去掉
我觉得我暂时用不到数据挖掘,所以我后面熟悉了的时候再来补充这部分
3. Extended Detail
这个我其实已经介绍过了,对于Allocations来说是看某一条数据的调用栈等信息
到现在我们只有一种列表展示没有介绍了,我们来看一眼
相对于Statistics来说多了调用库和方法的展示
七、继续优化
刚才说到了,我们只对VM:CG raster data的优化表示满意,接下来我们继续优化VM:CoreAnimation和VM:ImageIO_PNG_Data
1. VM:ImageIO_PNG_Data优化
前面说到我们把加载大图片的方法换成了imageWithContentsOfFile但是效果不明显,现在我们就来看看具体是哪个方法产生了大量的虚拟内存,下面是步骤
查看列表
找到调用函数.png
在谷歌借鉴了解决方法
//_pageScrollView是一个滚动视图,用来加载本地图片
...
//加入图片
for (int index = 0; index < nameArr.count; index ++) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(index * pageScrollImagewidth, 0, pageScrollImagewidth, pageScrollImageheight)];
NSString *imageFile = [NSString stringWithFormat:@"%@/%@.png",[[NSBundle mainBundle] resourcePath],nameArr[index]];
@autoreleasepool {
imageView.image = [UIImage imageWithContentsOfFile:imageFile];
}
// imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:nameArr[index] ofType:@"png"]];
// imageView.image = [UIImage imageNamed:nameArr[index]];
[_pageScrollView addSubview:imageView];
}
...
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
for (UIView *viewView in _pageScrollView.subviews) {
[viewView removeFromSuperview];
}
_pageScrollView = nil;
}
按照这个方法后,效果还是很明显的,之前是33.95(看文章前面的图)
2. 优化VM:CoreAnimation
我们同样用Generations检测出了这个问题
前面说到我们谷歌到可以采用下面的方法减少
for (CALayer *layer in self.view.layer.sublayers) {
[layer removeAllAnimations];
}
但是证明几乎没用,那我们只好继续谷歌看看,按照上面的方法,我们在谷歌输入关键字CA::Render::Shmem::new_shmem(unsigned long)进行搜索,我们从搜索的结果可以得知这是一个很多原因都会导致的结果;其中在这篇文章有两段描述
the solution is to reduce that space to an absolute minimum.
//把控件的范围设置到最小
...
I am having the same issue. I will try what you suggested with the size.
But, maybe changing the background color from clearColor could
correct the issue as well.
//改变视图的背景颜色
我试了改变背景颜色,没什么作用
[[UIView appearance] setBackgroundColor:[UIColor whiteColor]];
[[UIView appearance] setBackgroundColor:[UIColor clearColor]];
所以我们就暂时不解决这个了,我后面解决了会在这里进行补充,VM:Animation优化是一个大问题,可能需要一篇文章单独讲
八、看另一部分
接下来我们来看看这个视图是干什么用的,使用的时候需要手动捕获(或者你钩上Automatic Snapshotting自动定时捕获)一次左下角列表才有数据
默认会显示三种数据
Dirty Size:脏数据大小(没办法被重复使用) Swapped Size:交换空间大小 Resident Size:固定数据大小 关于这三个名字的解释,你可以看看这里,所以我们得知这条数据是有异常的
因为产生了31.54的垃圾数据,我们在这里找到了想要的答案,结果证明无效 又从这里得知我们不需要管这个问题,因为这时XCode工具自身的,不是我们程序的 所以我们来看下一个问题
我还特别重新启动了一次,证明我确实没有做什么其他操作,结果垃圾数据还是很多
这是为什么呢?答案在这里,所以这一部分我们也不需要管,所以我们的程序没有出现大量垃圾数据情况,问题还是出在CoreAnimation
我们把这个问题留着, 因为和上面优化VM:CoreAnimation是一类问题,等我熟悉了我再补充
1. Regoins Map
这个视图,主要是把每一条数据的地址段和调用路径给你显示出来了,我很少用这个视图,不过能看到path还是不错的
九、Allocations一般来做什么
其实文本已经大致讲了一下,Allocations对app优化非常有用,通常是拿来分析内存增加(不一定是内存泄漏)和app中各部分占用内存问题,当我们得知哪个内存占用比较多,我们直接进行优化即可减少内存占用问题 本文一直在讨论如何减少VM的占用,你接下来可以分析Heap占用
总结
本文 简单介绍了 项目 性能调试工具 Instruments 的基本使用,目前只对Allocations调试这一块 做了简单介绍 。
接下来会用几篇文章,围绕几个常见的性能问题,展开对 性能调试工具 Instrument 的 其它模块的使用介绍
参考
相关系列文章
Instruments