Android 内存优化总结

背景


Android手机不像pc等设备拥有大量的内存,因此内存成为Android中非常稀缺的资源。开发者在开发应用时,要特别关注应用的内存使用情况。内存吃紧会引发系统gc,频繁gc会造成卡顿,申请内存过多甚至超过最大限制时会oom,当一个使用内存过多的应用不在前台时,被系统强制回收的概率也会增加。

内存最大值


Android为每个应用创建单独的jvm进程,为了不让其中的一个应用消耗过多的内存资源,每个jvm进程所能申请的内存有最大限制。该值受机型分辨率,最大内存等因素影响,最终记录在/system/build.prop文件中,可以通过查看该文件了解jvm进程所能申请的最大内存。

cat /system/build.prop | grep dalvik
dalvik.vm.heapgrowthlimit=256m
dalvik.vm.heapsize=512m

heapgrowthlimit为普通应用所能申请的最大内存,也可以用ActivityManager.getLargeMemoryClass() 方法得到。
而当AndroidManifest.xml 的Application标签android:largeHeap=”true” 时,所能获取的最大内存为heapsize。

内存分配与GC


图片—内存大户


图片资源是可以说是内存中的大户,一个应用无论是图标,背景都和Drawable, Bitmap等相关,哪怕采用svg格式,最终也是通过绘成drawable来完成界面的渲染。那我们就有必要好好了解下图片加载到内存中到底有多大了。

内存中的图片有多大?

计算图片加载到内存中的大小,要看图片是以哪种格式加载进去的。如下表所示:

格式大小 (byte)
Alpha_81
RGB_5652
ARGB_4444(不推荐)2
ARGB_88884

Alpha_8 只有透明度
RGB_565 没有透明度
ARGB_8888 有透明度

现在我们有一张图片,它被加载到内存后是300 * 400的大小,格式为RGB_565,那它有多大呢?

 size = 300 * 400 * 2 / 1024 = 234.375(kb) 

那我们来计算下如果内存中加载了一张网络图片,它的大小为1920*1080, 格式为ARGB_8888,那它的大小有多大呢?

size = 1920 * 1080 * 4 / 1024 / 1024 = 7.91(mb)

理论上,1920 * 1080的手机,留出3个屏幕的图片内存作为图片缓存便够了,大概24M。

图片加载到内存中有多大?

下面我们来看另一个问题,一张300 * 400 的图片,放在xx-hdpi的目录下,它被加载到内存中有多大?
Android 手机为了适配不同分辨的手机,会根据手机的屏幕密度与资源目录对应的密度比作适当的缩放。比如,我们使用的手机是1920 * 1080, 尺寸为5.5’。
那手机的屏幕密度为:

dpi = √ (1920 ^ 2 + 1080 ^ 2) / 5.5 = 400

而资源目录对应的dpi如下表:

目录dpi
drawable-ldpi120
drawable-mdpi160
drawable-hdpi240
drawable-xhdpi320
drawable-xxhdpi480
drawable-xxxhdpi640

那么这张图加载到内存中的大小:

scaledWidth = int(300 * 400 / 480f + 0.5f) = 250
scaledHeight = int(400 * 400 / 480f + 0.5f) = 333

如果它是以ARGB_8888 的格式加载到内存中,则它所占用的在大小为:

scaledWidth * scaledHeight * 4 / 1024 = 250 * 333 * 4 / 1024 = 325.19(kb)

我们要清楚地知道资源文件放在不同的地方,加载内存中是多大。

编程猫App的优化


加入编程猫之后,我看到了App的内存占用情况,惊人的198M。造成的直接问题是有些页面卡顿情况非常严重。为什么会这么大呢?能造成这么大的内存使用量,肯定是哪里出了严重的问题。看来对它的优化势在必行!

寻找内存大对象

我们利用android studio对它多次gc之后,dump出内存文件。

dump

android studio dump 出的内存文件并非标准hprof文件, 我们需要用hprof-conv 转成标准hprof文件。

hprof-conv com.codemao.dan_2017.07.04_12.09.hprof com.codemao.dan_converted.hprof

查看hprof文件我们采用mat工具,dominator_tree列表,列出占用内存最大的对象。并使用list object -> with imcoming references 查看它被谁引用了

dominator_tree

image_buffer

最后发现是SimpleDraweeView使用的图片资源,可是为什么这么大呢?找到它对应的原始图片文件,发现这张图的尺寸大小达到了2480*3507! 加载到内存中的大小达到30m之多,而且像这样的图片还有3张。造成相应的页面因为频繁gc,非常卡顿,而那个图片在屏幕的大小只有 100dp * 142dp。

项目中采用了fresco图片加载框架,但是没有做缓存策略,而服务器上的图片资源本来不是用来适配移动设备的,所以就造成图片资源非常巨大。

好的,知道原因了, 我们为fresco加上最大30m的缓存区,开启自动裁剪尺寸设置,然后,加载到内存的图片大小与SimpleDraweeView的大小一致。这样图片问题基本解决了。

无用的xml控件与资源

图片问题解决之后,内存问题己经好了很多,下降到了56M左右了。再次查看下还有其他的大内存对象吗?
xml_problem

发现一个drawable对象达到了2m,查看它的引用链,发现是一张从drawable-nodpi目录中加载出来的图片,尺寸达到1020 * 520, 而屏幕实际使用大小18dp * 18dp。而且更关键的是,图片是在xml中作为背景图使用的,而在代码中,又把整个控件都setVisibility(GONE) 完全不显示!

  1. 对于这种无用的布局,我们应该尽量删去它,一方面可以减少绘制,另一方面也可以减少不必要的资源加载。
  2. 资源文件的大小应该尽量与控件大小相适配,对于这点恐怕我们平时在开发的时候也会很容易忽视。
mDrawingCache

发现一个很奇怪的问题,为什么这个recyclerView 会有mDrawingCache对象,占用内存还达到4M,而其他的recyclerView却没有呢?

mDrawingCache是View里用于缓存视图的bitmap, 可调用buildDrawingCache() 生成,通过getDrawingCache方法获取视图内容。

查看代码也没有主动对这个recyclerView调用buildDrawingCache(),那它是怎么生成的呢?为了弄清这个问题,自定义了recyclerView,重写了buildDrawingCache(),打印了方法的调用栈信息。

@Override
public void buildDrawingCache(boolean auto) {
    try {
        throw new Exception("buildDrawingCache");
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    super.buildDrawingCache(auto);
}

顺着打印的信息,找到View.java如下代码:

if (layerType == LAYER_TYPE_SOFTWARE) {
    buildDrawingCache(true);
    Bitmap cache = getDrawingCache(true);
    if (cache != null) {
       canvas.drawBitmap(cache, 0, 0, mLayerPaint);
    }
}

发现如果View开启了离屏缓存,便会在绘制的时候build出自己的bitmap,之后如果没有内容变更会直接使用缓存的bitmap来进行绘制,从而提高绘制速度。

我们找到recyclerView对应的xml,看到确实打开了缓存

<android.support.v7.widget.RecyclerView
    android:id="@+id/rv_Novel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layerType="software"
    android:layout_marginLeft="18dp">

想必是因为之前的开发为了解决那个页面的卡顿问题而开启了缓存,可是并不知道卡顿的原因是因为内存吃紧下的频繁GC,而开启了缓存之后反而加剧了内存问题。将android:layerType="software"去除就好了。

一般经常变动的View不适合开启缓存,复杂而不易变动的View适合开启。

总结
  1. 图片资源要注意使用缓存防止oom,加载到内存的图片大小应尽量与显示的大小一致,减少内存开销。这方面,glide能自动加载imageView大小的图片,fresco需要手动设置。
  2. xml里无用的布局尽量删除,避免不必要的绘制,与引用资源文件消耗内存。

最后的战果,在经过上面的优化之后,内存使用稳定在46m左右,页面流畅不卡, 成就感满满。

参考文献:

Android屏幕适配(不同屏幕分辨率和尺寸)

Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

Android 内存优化总结&实践

MAT使用教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值