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

来源:http://www.zone-t.com 作者: 2018-09-15 08:08

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

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

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

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

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

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

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

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

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

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

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

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

  优化:应该在item被回收不可见时掉对图片的引用,这里注意RecyclerView与ListView的区别,如果是ListView,因为每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetchFromWindow的时候掉图片引用即可。

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

  ①对于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加载图片时手动设置一样的targetSize,这样缓存的Key就一致了,就可以实现在内存里进行复用了,而指定一样的targetSize并不会有什么风险,因为说了,只有你指定的targetSize比图片实际大小小2倍以上,采样率才会生效,实际图片才会被压缩。

  内存的分析优化并不是一两个版本的事,而是一个必须每个版本持续进行的工作,这需要一套完善的线上用户内存使用情况监测系统来进行数据上传、数据分析、数据整理、数据对比,方便我们明确的了解每个版本线上App内存的具体情况。

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

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