实践App内存优化:如何有序地做内存分析与优化

由于项目里之前线上版本出现过一定比例的OOM,虽然比例并不大,但是还是暴露了一定的问题,所以打算对我们App分为几个步骤进行内存分析和优化,当然内存的优化是个长期的过程,不是一两个版本的事,每个版本都需要收集线上内存数据进行监控以及分析。

版本迭代过程中,内存增长过快,不仅会导致一定概率的OOM,运行时若出现内存抖动,导致频繁GC,则会对App的流畅度以及用户体验造成很大影响。

本文主要会根据实际项目中优化步骤分为以下几部分:

  1. Android内存分析基础
  2. 内存泄漏
  3. 静态内存分析优化
  4. 运行时内存分析优化
  5. 监控

1.Android内存分析基础

这部分主要先介绍一些进行内存分析的基础方法以及工具,对这部分比较熟悉的同学可以先跳过哈。

一.App的内存使用情况概览

每个App进程可以分配到的最大内存是有限的,当然不同手机每个App进程可以分配到的最大内存有可能不一样,可以通过以下命令进行查看:

//dvm最大可用内存:
adb shell getprop | grep dalvik.vm.heapsize
//单个程序限制最大可用内存:
adb shell getprop|grep heapgrowthlimit

超过单个程序限制最大内存则OOM,如果设置了开启largeHeap,则可提高到dvm最大内存才OOM。

我们可以输出我们App的内存使用情况概览:

adb shell dumpsys meminfo 包名

我们就可以看到:

内存概览

Pss: 该进程独占的内存+与其他进程共享的内存(按比例分配,比如与其他3个进程共享9K内存,则这部分为3K)

Privete Dirty:该进程独享内存

Heap Size:分配的内存

Heap Alloc:已使用的内存

Heap Free:空闲内存

二、Android Profiler

AndroidStduio3.0后Android Profiler变得比之前更强大,内存分析页变得更加直观更加方便,下面是截图:

Android profiler

  • 进程占用总内存
  • javaHeap:这部分内存大小是有限制的,溢出则会OOM,这部分内存也是我们分析优化的重点
  • NativeHeap:native层的 so 中调用malloc或new创建的内存,对于单个进程来说大小没有限制,所以可以利用在native层分配内存来缓解javaHeap的压力(比如2.3.3之前Android Bitmap的内存分配就是在native层,之后移到javaHeap, 8.0又回到native)
  • Graphics:这部分一般游戏app中用的较多,OpenGL和SurfaceFlinger相关的内存,若没有直接调用到OpenGL,则一般不会涉及到这块内存
  • Stack:栈,了解jvm内存模型的应该都知道
  • Code: 代码,主要是dex以及so等占用的内存
  • Others:就是others啦

所以我们可以看到事实上我们可以优化的点有:JavaHeap、NativeHeap、Stack、Code所占用的内存

三、强大的MAT

MAT是做比较细致的内存分析的利器了,功能十分强大,其中的:

Hisogram:Lists number of instances per class

Dominator Tree:List the biggest objects and what they keep alive.

可以非常方便的排序查看当前内存中最占内存的class或者实体对象,而且有一条非常清晰的引用链来查看该对象的持有者,这对内存的分析以及内存泄漏的分析都是非常友好的。

同时MAT支持compare对比功能,将两个.hprof文件导入,都Add to Compare Basket之后即可进行对比,这对于对比某个页面相较与前一页面的内存增量来说是非常有意义的。

有一点比较不友好的是,MAT需要标准的.hprof文件,所以在AndroidStduio的Profiler中GC后dump出的内存快照还要自己手动利用android sdk platform-tools下的hprof-conv进行转换一下才能被MAT打开。
当然如果觉得麻烦的话也可以自己写个脚本执行几条命令来直接完成GC->dump java heap->转换.hprof文件 这个流程:

//adb and hprof-conv
ADB=${ANDROID_HOME}/platform-tools/adb
HPROF_CONV=${ANDROID_HOME}/platform-tools/hprof-conv
//GC
${ADB} shell pkill -l 10 $(PACKAGE_NAME)
//dump java heap
${ADB} shell "am dumpheap $(PACKAGE_NAME) $(OUT_PATH)"
//conv hprof
${HPROF_CONV} -z ${FILE_NAME} droid-${FILE_NAME}

2.内存泄漏

根据以往经验,其实做内存优化最先要搞定的应该是内存中的大头,这类大头对内存的占用很大,也是内存问题的主要祸首,相对来说比较容易定位问题,且优化后效果也非常明显,性价比非常高。

事实上很多优化都是这样,比如减包大小的优化,也是要先分析出主要大头祸首,比如可能你的包里包含了一张3M大小的无用图片,如果你没找到这种祸首,可能你做了大量的工作去想办法减少无用代码等,最终可能只有几百K的收益。

相对内存来说,这个大头就是:

  • 内存泄漏
  • 图片

所以首先你要确保你的应用里没有存在内存泄漏,然后再去做其他的内存优化。

内存泄漏检测

现在内存泄漏的检测已经变得非常简便了,使用App后在Android Profiler中先触发GC然后dump内存快照,之后点击按package分类,就可以迅速查看到你的App目前在内存中残留的class,点击class即可在右边查看到对应的实例以及引用对象。

当然你也可以在debug下集成LeakCanary做内存泄漏监控警告

排除内存泄漏后,图片就是另一个占用内存大头的对象了。

图片

对于图片来说一个是颜色模式,检查一下项目里的图片的颜色模式,是否可以降低,比如从RGB_8888降到RGB_565,则每张图片可以节省1/2的内存,如果没有使用到透明通道等的话基本上肉眼看不出差别。

还有一个是降低图片的大小,可能你的ImageView只有你图片的一半大,则这部分内存就大大浪费了,我们项目服务端会根据前端的参数做动态切图。

前端也可以通过降低采样率(inSampleSize)来达到降低图片占用内存大小的目的,但是这个采样率InSampleSize只能是整数(甚至只能是2的次方),如果inSampleSize=2,则最终内存占用就会是原来的1/4,适用于图片过大很多的情况,对于只是想做小幅度压缩的话,基本没用。

ok,接下来开始做具体的内存分析与稍微细致一点的内存优化。

3.静态内存分析优化

这边说的静态内存指的是在伴随着App的整个生命周期一直存在的那部分内存,也就是打底的,具体获取这部分内存快照的方式是:
打开App开始重度使用App,基本打开每一个主要页面主要功能,然后回到首页,进开发者选项打开"不保留后台活动",然后将我们的app退到后台。最后GC,dump出内存快照。
下面是我们app dump出的内存快照,进行分析后制图如下:

通过对静态内存数据的分析,主要发现了以下几个问题:

问题1: App首页的主图有两张(一张是保底图,一张是动态加载的图),都比较大,而且动态加载的图回来后,保底图并没有及时被释放

优化:首先是对首页的主图进行颜色通道的改变以及压缩,可以大大降低这两张图所占的内存,然后在动态加载图回来后及时释放掉保底图 -5M

问题2: 首页底部的轮播背景图占用内存1.6M,且在图片加载回来后,背景图一直没有置空

优化:首先一般来说对背景图的质量并没有很高的要求,所以这张背景图是可以被成倍压缩的,并且在图片加载回来后,背景图要及时的释放掉。同时首页的多张轮播图以及其他图片都可以进行颜色模式的改变以及质量压缩。 -1.6M -4M

问题3: 项目会在App启动时拉一个接口获取一些实验配置,放进单例,在内存分析时发现,这些实验配置竟然接近1M

优化:排查后发现,接口拉的是整个公司所有部门的实验配置,上千个,这也给遍历拿一个实验配置带来一定的性能损耗,推动接口去改进,只获取当前部门业务需要的实验配置,可节省内存90%以上 -700K

问题4: 发现几个lottie动画一直没有被回收,并且同一个lottie动画会有几个不同的实例存在,总共占用内存450K

优化:首先要确定几个lottie动画为什么在页面退出后没有被回收,并且同一个动画有几个不同的实例,很容易就联想到内存泄漏,由于页面没有被销毁,所以导致几个lottie动画也没有被回收,排查下来是项目里的RN页面存在内存泄漏,解决后大概可以 节省3-5M内存

问题5: SharePreference在内存里占用了700K的内存

优化:由于SP中的东西是会一次性加载到内存里并且保存为静态的,直到App进程结束才会被销毁,所以SP中千万别放大的对象,别图一时方便把对象序列化成json后保存到SP里,优化点就是把已经保存在SP中的一些较大的json字符串或者对象迁移到文件或者数据库缓存。 -400K

问题6: 埋点数据

优化:产品或者运营为了统计数据会在每个版本不断的增加新埋点,但是也需要定期去清理掉一些过时的不需要的埋点,来适当优化内存以及CPU的压力。

问题7: 还有就是一些App里的单例以及一些静态缓存

优化:整个看下来在我们项目中这部分占整体的静态内存其实较小,综合考虑内存情况以及使用的高效性可以进行一定程度的优化,不过这部分内存在App内存紧张时可以选择清理掉他们

我们可以选择在App退到后台后内存紧张即将被Kill掉时选择释放掉一些内存,如图片的缓存,静态缓存等来自保,具体做法是在Activity中重写onTrimMemory()方法(4.0之前是onLowMemory()),在这里面来做内存的释放。

静态内存优化:约15M

4.运行时内存分析优化

接下来做一下每个页面的运行时内存分析优化,这一部分就是随着App运行过程增长以及回收的内存,这部分工作十分繁琐,需要耐得住寂寞啊。

分析和优化运行时内存主要是通过以下两个核心方式:

  • 从首页开始用脚本dump出每个页面的内存快照文件,然后利用MAT的对比功能,找出每个页面相对于上个页面内存里主要增加了哪些东西,做针对性优化
  • 利用Android Profiler实时观察进入每个页面后的内存变化情况,对产生的内存较大波峰做分析

首先介绍一下我们App中我们产线的主要核心页面流程:搜索页-->列表页-->详情页-->信息页-->支付,这里重点对列表页和详情页做运行时内存分析优化。

(1)列表页内存优化

下面是列表页的内存快照与搜索页的对比:
内存对比

可以看到,绝大部分的内存增加还是图片,当然还有一些静态缓存:

问题1:列表item被回收时还持有图片的引用

优化:应该在item被回收不可见时释放掉对图片的引用,这里注意RecyclerView与ListView的区别,如果是ListView,因为每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetchFromWindow的时候释放掉图片引用即可。而对于RecyclerView来说,因为被回收不可见时第一选择是放进mCacheView中,而这里面的item被复用时并不会执行bindViewHolder来重新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,所以如果是RecyclerView,我们释放图片引用的时机应该是item被回收进RecyclePool的时候,只要重写Adapter中的onViewRecycled方法即可:

@Override
public void onViewRecycled(@Nullable VH holder) {super.onViewRecycled(holder);if (holder != null) {//做释放图片引用的操作}
}

问题2:图片大小有优化空间

优化:这个因为我司在服务端会对图片进行动态切图,所以最简单的方法就是根据实际情况来改变动态切图的大小达到节省内存的作用,当然如果从服务端请求回来的图片实在大(一般不要比装载的ImageView要大),前端就可以采用降低采样率的方式来进行压缩,当然这个上面说了采样率(inSampleSize)只支持2的次方,所以对图片占用内存大小的压缩是非常大的,如果你只是想小幅度的压缩,基本上这个是没用的。

问题3:对ImageLoader图片缓存策略的思考

①对于UIL这个图片框架,他的缓存策略是内存缓存+磁盘缓存,内存缓存默认的数据结构是LruMemoryCache,对图片是强引用,默认最大Size是内存的1/8,满后会按照LRU算法对最近最不常用的图片进行移除,看起来比较合理,但是会有一个问题,就是当图片缓存达到1/8后则图片所占的内存一直会保持在接近1/8,它没有自我清理的能力,可能长时间过去了这1/8内存里的有些图片都不再需要了,它也依然会保留在内存里不会被清除,所以我们可以考虑对缓存的图片做一个有效期的管理,图片过期后则自动清理一波,这样可以优化很大一部分内存空间。

②由于UIL对于内存缓存图片是以“url+targetWidth+targetHeight”作为key,如果我们加载图片的时候没有设置targetSize,则框架里默认会以ImageView的大小作为targetSize,那么就会出现一种情况,同一张图片,由于放在大小有轻微差异的ImageView上显示,则由于targetSize不一样,会在内存中被缓存两份,当然要解决这个问题也很简单,只要设置denyCacheImageMultipleSizesInMemory()即可避免这种情况,这样同一张图片在内存里就只会有一份缓存(之前的会被之后的替换掉)。
设置完denyCacheImageMultipleSizesInMemory()后又会出现一个新问题,虽然内存里同一张图片只有一份了,但这也意味着有轻微差异的ImageView加载的同一张图片在内存里没办法被复用了,每次都要去磁盘缓存里重新加载(磁盘缓存是只以url作为key的)。

那么如何做到让有轻微大小差异的ImageView加载同一张图片时既实现在内存缓存里进行复用又不会在内存缓存里保留两份缓存呢?

  1. 开启denyCacheImageMultipleSizesInMemory()避免同一张图片因为targetSize不同而存在多个内存缓存
  2. 将有轻微大小差异的ImageView加载图片时手动设置一样的targetSize,这样缓存的Key就一致了,就可以实现在内存里进行复用了,而指定一样的targetSize并不会有什么风险,因为上面说了,只有你指定的targetSize比图片实际大小小2倍以上,采样率才会生效,实际图片才会被压缩。

(2)详情页的内存分析优化

可以看看刚进入详情页后会有一个明显的波峰,通过点击Adnroid Profiler上的红色圆点来记录查看这段波峰里的内存分配。

首先详情页依然有大量的图片,所以对于图片的大小以及复用上的优化上面已经说了,这里就不重复说了。

问题1:在内存里发现两个极少概率出现的empty view,占用了接近2M的内存

优化:用ViewStub对empty view做了懒加载,对于这些没有马上用到的资源要做延迟加载,还有很多大概率不会出现的View更加要做懒加载。 -2M

问题2:发现详情页的轮播大图的Viewpager用的Adapter是FragmentPagerAdapter,导致了所有的page都会被保存,当图片页数多的时候,往后翻内存会不断上升。

优化:这种页数多的ViewPager使用FragmentStatePagerAdapter来替代,它只会保留前后pager,在页数多的时候可以 节省大量内存

问题3:对于一些实在大的图并且复用频率并不高的大图只采用文件缓存就行了,不做内存缓存。

问题4:我们项目在debug下会打印网络请求的reqeust和response,并且会用String.subString()对较长的response json进行截取

优化:本身subString()就比较耗内存,所以在response较大的时候就会申请大量的内存,好在这种情况只会在debug下发生,但是依然需要改进这种打印。

5.监控

内存的分析优化并不是一两个版本的事,而是一个必须每个版本持续进行的工作,这需要一套完善的线上用户内存使用情况监测系统来进行数据上传、数据分析、数据整理、数据对比,方便我们明确的了解每个版本线上App内存的具体情况。公司的一套性能监控平台,可以在这方面给我们App开发人员提供很直观的监控数据和版本迭代对比。

通过上面我们项目的内存分析,可以发现图片绝对是内存中的一块大头,所以对于图片的使用监控就显得尤为重要,我们自定义了一个简单的可以监控加载的图片是否过大的ImageView,可以在debug阶段发出警告,方便开发人员及早发现过大的图片。

当然要做的工作还有很多,比如当我们发现占用内存过高时,可以尝试来释放一些静态的缓存,一次来缓存内存的压力。

6.总结

这个版本利用了点时间对项目的内存占用做了以上分析以及优化,还需要做的还有很多,之后的版本会继续跟进,总得来说做内存分析和优化还是比较辛苦的,特别是各种内存快照的分析以及对代码问题的排查,当然时间有限,可能很多地方说的可能也有疏漏或者错误,纸上得来终觉浅,绝知此事要躬行,对于性能优化特别内存优化这一块,实践远比理论得到的要多。

目前项目里关于流畅度以及耗电量还没发现太大的问题,因为每个版本或多或少都会做一些优化,线上也有数据监测,之后还是想整理一下关于卡顿流程度的分析优化以及耗电量的分析优化实践。

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

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

相关文章

WinForm(十四)窗体滚动日志

在桌面程序里,一般日志记录到文件里就可以了,但有的时间,也需要在窗体上动态滚动显示,这时,就需要引入日志框架了。这里引入的依旧是NLog(在我的Mini API系统里,用的也是NLog)。首先…

xp计算机找不到音量调节,WinXP电脑没声音且小喇叭不见了如何解决?

有用户在使用电脑听音乐的时候,突然发现电脑没有声音了,本来以为只是被禁了音,想着调节音量即可解决问题。但是当他想要点开音量小喇叭的时候,发现桌面任务栏通知区域的小喇叭不见了,这该怎么办呢?下面小编…

2018-2019-1 20165211 实验四 外设驱动程序设计

2018-2019-1 20165211 实验四 外设驱动程序设计 任务一 1.实验要求 学习资源中全课中的“hqyj.嵌入式Linux应用程序开发标准教程.pdf”中的第十一章 提交康奈尔笔记的照片(可以多张) 2. 任务完成 任务二 1. 实验要求 在Ubuntu完成资源中全课中的“hqyj.嵌…

《ASP.NET Core 6框架揭秘》实例演示[31]:路由高阶用法

ASP.NET的路由是通过EndpointRoutingMiddleware和EndpointMiddleware这两个中间件协作完成的,它们在ASP.NET平台上具有举足轻重的地位,MVC和gRPC框架,Dapr的Actor和发布订阅编程模式都建立在路由系统之上。Minimal API更是将提升到了前所未有…

java中文乱码解决之道(五)—–java是如何编码解码的

编码&解码 1:I/O操作 2:内存 3:数据库 4:javaWeb 下面主要介绍前面两种场景,数据库部分只要设置正确编码格式就不会有什么问题,javaWeb场景过多需要了解URL、get、POST的编码,servlet的解码…

win10系统按esc会弹出计算机,win10系统版本2004控制面板多出ESC是什么原因?

如果我们的电脑在升级了win102004控制面板多出ESC什么情况方法一:“干净启动”,排除第三方软体的影响1.停止非核心的程序运作(包括第三方杀毒、优化软体)2.情况允许的话,卸载设备中的第三方杀毒、管家、优化软件3.同时按【4.点击【服务】>…

CentOS6/7 配置守护进程

CentOS6.xCentOS6中转用Upstrat代替以前的init.d/rcX.d的线性启动方式。一、相关命令通过initctl help可以查看相关命令[rootlocalhost ~]# initctl help Job commands:start Start job.stop Stop job.restart …

Java并发(二十一):线程池实现原理

一、总览 线程池类ThreadPoolExecutor的相关类需要先了解: (图片来自:https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8%A7%88) Executor:位于最顶层,只有一个 execute(Runnable runnable) 方法&a…

进程池

转自:https://www.cnblogs.com/kaituorensheng/p/4465768.html 在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用…

gulp版本号管理插件注意事项

2019独角兽企业重金招聘Python工程师标准>>> 打开node_modules\gulp-rev\index.js 第144行 manifest[originalFile] revisionedFile; 更新为: manifest[originalFile] originalFile ?v file.revHash; 打开node_modules\rev-path\index.js 第10行 return filena…

bigfile.to服务器位置,Cloudera Manager 迁移服务器

Cloudera Manager还是比较耗资源的,想把Cloudera Manager,移动到比较好的机器上。在这篇文章中,Cloudera Manager安装在bigserver1上面,bigserver1是奔腾双核的CPU。1,Cloudera Manager占资源比较多cloudera manager占…

公司新来了一位阿里P9,在全员大会上讲荤段子!还是上个世纪的老段子,太烂了!...

阿里P9在坊间的名声一向不好,这几年在业界出了不少令人无语的新闻,今天又来了一个:公司新来了一位阿里P9伪高管,全员大会上来先讲了一个荤段子,这个破段子还是上个世纪的,太烂了!关于这个段子&a…

【转】博客美化(1)基本后台设置与样式设置

阅读目录 1.博客园后台设置2.自定义样式的设置博客园美化相关文章目录:博客园博客美化相关文章目录 一直都拜膜那些博客园的皮肤设计高手,由于本人对前端研究甚少,所以js,css这种东西只能看得懂最基本的,会简单改改。然后一直对自…

Airdoc创始人:工智能可以在医疗领域多个环节发挥作用 但有局限性

7月1日,在由武汉国家生物产业基地建设管理办公室主办、火石创造承办、光谷健康智慧园协办的医疗大数据与医学人工智能高峰论坛上,Airdoc创始人兼董事长张大磊做了题为《AI在医疗领域中应用的问题与局限》的演讲。 Airdoc是医疗领域人工智能领军企业&…

我的世界服务器抽奖系统怎么弄,我的世界自动识别货币抽奖机如何制作

我的世界是一款很经典的沙盒类游戏,在游戏中红石和命令方块是这部作品的核心,可以制作很多装备和道具,下面给大家分享下我的世界自动识别货币抽奖机如何制作,希望对大家有所帮助。自动识别货币抽奖机制作方法废话不多说,(貌似一句…

Java并发编程中volatile实现过程详细解析

2019独角兽企业重金招聘Python工程师标准>>> 首先并发编程有三大特性: 可见性,有序性,原子性。volatile关键字实现了前面两个特性。那么它是如何实现这两个特性的呢? 首先是可见性。可见性主要是让缓存,直接…

《ASP.NET Core 6框架揭秘》实例演示[32]:错误页面的N种呈现方式

由于ASP.NET是一个同时处理多个请求的Web应用框架,所以在处理某个请求过程中出现异常并不会导致整个应用的中止。出于安全方面的考量,为了避免敏感信息外泄,客户端在默认情况下并不会得到详细的出错信息,这无疑会在开发过程中增加…

Golang并发模型:合理退出并发协程

goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。这篇文章介绍,如何合…

关于8位AD_DA转换芯片的采样率问题

关于使用Keil计算程序执行时间 打开Keil程序,进入“启动/停止调试”界面。在需要暂停的地方设置断点(在该句程序前双击)。在程序上方有一行工具栏:此工具栏分别代表复位、运行、停止、步进、步越、步出、运行到光标处等。 点击运…

CYQ.Data 数据框架 V4.0 开源版本发布(源码提供下载,秋色园V2.5版本标配框架)

说明的说明: 博客园团队两次移此文出首页,说 这篇文章不属于知识分享型文章,并且有广告嫌疑。 本文的确属于分享型文章,而且分享的知识点比其它文章都多很多,看看网友回复“谢谢分享”就知道是分享型文章了。 所谓广告…