因为项目里以前线上版本出现过必定比例的OOM,虽然比例并不大,可是仍是暴露了必定的问题,因此打算对咱们App分为几个步骤进行内存分析和优化,固然内存的优化是个长期的过程,不是一两个版本的事,每一个版本都须要收集线上内存数据进行监控以及分析。前端
版本迭代过程当中,内存增加过快,不只会致使必定几率的OOM,运行时若出现内存抖动,致使频繁GC,则会对App的流畅度以及用户体验形成很大影响。java
本文主要会根据实际项目中优化步骤分为如下几部分:android
这部分主要先介绍一些进行内存分析的基础方法以及工具,对这部分比较熟悉的同窗能够先跳过哈。算法
每一个App进程能够分配到的最大内存是有限的,固然不一样手机每一个App进程能够分配到的最大内存有可能不同,能够经过如下命令进行查看:shell
//dvm最大可用内存:
adb shell getprop | grep dalvik.vm.heapsize
复制代码
//单个程序限制最大可用内存:
adb shell getprop|grep heapgrowthlimit
复制代码
超过单个程序限制最大内存则OOM,若是设置了开启largeHeap,则可提升到dvm最大内存才OOM。数据库
咱们能够输出咱们App的内存使用状况概览:json
adb shell dumpsys meminfo 包名
复制代码
咱们就能够看到:缓存
Pss
: 该进程独占的内存+与其余进程共享的内存(按比例分配,好比与其余3个进程共享9K内存,则这部分为3K)性能优化
Privete Dirty
:该进程独享内存网络
Heap Size
:分配的内存
Heap Alloc
:已使用的内存
Heap Free
:空闲内存
AndroidStduio3.0后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是作比较细致的内存分析的利器了,功能十分强大,其中的:
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}
复制代码
根据以往经验,其实作内存优化最早要搞定的应该是内存中的大头,这类大头对内存的占用很大,也是内存问题的主要祸首,相对来讲比较容易定位问题,且优化后效果也很是明显,性价比很是高。
事实上不少优化都是这样,好比减包大小的优化,也是要先分析出主要大头祸首,好比可能你的包里包含了一张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,接下来开始作具体的内存分析与稍微细致一点的内存优化。
这边说的静态内存指的是在伴随着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
接下来作一下每一个页面的运行时内存分析优化,这一部分就是随着App运行过程增加以及回收的内存,这部分工做十分繁琐,须要耐得住寂寞啊。
分析和优化运行时内存主要是经过如下两个核心方式:
首先介绍一下咱们App中咱们产线的主要核心页面流程:搜索页-->列表页-->详情页-->信息页-->支付,这里重点对列表页和详情页作运行时内存分析优化。
下面是列表页的内存快照与搜索页的对比:
能够看到,绝大部分的内存增长仍是图片,固然还有一些静态缓存:
问题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加载同一张图片时既实如今内存缓存里进行复用又不会在内存缓存里保留两份缓存呢?
denyCacheImageMultipleSizesInMemory()
避免同一张图片由于targetSize不一样而存在多个内存缓存能够看看刚进入详情页后会有一个明显的波峰,经过点击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下发生,可是依然须要改进这种打印。
内存的分析优化并非一两个版本的事,而是一个必须每一个版本持续进行的工做,这须要一套完善的线上用户内存使用状况监测系统来进行数据上传、数据分析、数据整理、数据对比,方便咱们明确的了解每一个版本线上App内存的具体状况。公司的一套性能监控平台,能够在这方面给咱们App开发人员提供很直观的监控数据和版本迭代对比。
经过上面咱们项目的内存分析,能够发现图片绝对是内存中的一块大头,因此对于图片的使用监控就显得尤其重要,咱们自定义了一个简单的能够监控加载的图片是否过大的ImageView,能够在debug阶段发出警告,方便开发人员及早发现过大的图片。
固然要作的工做还有不少,好比当咱们发现占用内存太高时,能够尝试来释放一些静态的缓存,一次来缓存内存的压力。
这个版本利用了点时间对项目的内存占用作了以上分析以及优化,还须要作的还有不少,以后的版本会继续跟进,总得来讲作内存分析和优化仍是比较辛苦的,特别是各类内存快照的分析以及对代码问题的排查,固然时间有限,可能不少地方说的可能也有疏漏或者错误,纸上得来终觉浅,绝知此事要躬行,对于性能优化特别内存优化这一块,实践远比理论获得的要多。
目前项目里关于流畅度以及耗电量还没发现太大的问题,由于每一个版本或多或少都会作一些优化,线上也有数据监测,以后仍是想整理一下关于卡顿流程度的分析优化
以及耗电量的分析优化
实践。