【腾讯Bugly干货分享】Android内存优化总结&实践

本文来自于腾讯Bugly公众号(weixinBugly),未经做者赞成,请勿转载,原文地址:https://mp.weixin.qq.com/s/2MsEAR9pQfMr1Sfs7cPdWQphp

导语

智能手机发展到今天已经有十几个年头,手机的软硬件都已经发生了翻天覆地的变化,特别是Android阵营,从一开始的一两百M到今天动辄4G,6G内存。然而大部分的开发者观看下本身的异常上报系统,仍是会发现各类内存问题仍然层出不穷,各类OOM为crash率贡献很多。Android开发发展到今天也是已经比较成熟,各类新框架,新技术也是层出不穷,而内存优化一直都是Android开发过程一个不可避免的话题。 刚好最近作了内存优化相关的工做,这里也对Android内存优化相关的知识作下总结。html

在开始文章以前推荐下公司同事翻译整理版本《Android性能优化典范 - 第6季》,由于篇幅有限这里我对一些内容只作简单总结,同时若是有不正确内容也麻烦帮忙指正。java

本文将会对Android内存优化相关的知识进行总结以及最后案例分析(一二部分是理论知识总结,你也能够直接跳到第三部分看案例):linux

1、 Android内存分配回收机制
二 、Android常见内存问题和对应检测,解决方式。
3、 JOOX内存优化案例
四 、总结android

工欲善其事必先利其器,想要优化App的内存占用,那么仍是须要先了解Android系统的内存分配和回收机制。git

一 ,Android内存分配回收机制

参考Android 操做系统的内存回收机制[1],这里简单作下总结:程序员

从宏观角度上来看Android系统能够分为三个层次github

  1. Application Framework,
  2. Dalvik 虚拟机
  3. Linux内核。

这三个层次都有各自内存相关工做:面试

1. Application Framework

Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:算法

  • Empty process(空进程)
  • Background process(后台进程)
  • Service process(服务进程)
  • Visible process(可见进程)
  • Foreground process(前台进程)

系统须要进行内存回收时最早回收空进程,而后是后台进程,以此类推最后才会回收前台进程(通常状况下前台进程就是与用户交互的进程了,若是连前台进程都须要回收那么此时系统几乎不可用了)。

由此也衍生了不少进程保活的方法(提升优先级,互相唤醒,native保活等等),出现了国内各类全家桶,甚至各类杀不死的进程。

Android中由ActivityManagerService 集中管理全部进程的内存资源分配。

2. Linux内核

参考QCon大会上阿里巴巴的Android内存优化分享[2],这里最简单的理解就是ActivityManagerService会对全部进程进行评分(存放在变量adj中),而后再讲这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykiller, Oom_killer)。这里只是大概的流程,中间过程仍是很复杂的,有兴趣的同窗能够一块儿研究,代码在系统源码ActivityManagerService.java中。

3. Dalvik虚拟机

Android进程的内存管理分析[3],对Android中进程内存的管理作了分析。

Android中有Native Heap和Dalvik Heap。Android的Native Heap言理论上可分配的空间取决了硬件RAM,而对于每一个进程的Dalvik Heap都是有大小限制的,具体策略能够看看android dalvik heap 浅析[4]。

Android App为何会OOM呢?其实就是申请的内存超过了Dalvik Heap的最大值。这里也诞生了一些比较”黑科技”的内存优化方案,好比将耗内存的操做放到Native层,或者使用分进程的方式突破每一个进程的Dalvik Heap内存限制。

Android Dalvik Heap与原生Java同样,将堆的内存空间分为三个区域,Young Generation,Old Generation, Permanent Generation。

最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到必定程度,它会被移动到Old Generation,最后累积必定时间再移动到Permanent Generation区域。系统会根据内存中不一样的内存数据类型分别执行不一样的gc操做。

GC发生的时候,全部的线程都是会被暂停的。执行GC所占用的时间和它发生在哪个Generation也有关系,Young Generation中的每次GC操做时间是最短的,Old Generation其次,Permanent Generation最长。

GC时会致使线程暂停,致使卡顿,Google在新版本的Android中优化了这个问题, 在ART中对GC过程作了优化揭秘 ART 细节 —— Garbage collection[5],听说内存分配的效率提升了10倍,GC的效率提升了2-3倍(可见原来效率有多低),不过主要仍是优化中断和阻塞的时间,频繁的GC仍是会致使卡顿。

上面就是Android系统内存分配和回收相关知识,回过头来看,如今各类手机厂商鼓吹人工智能手机,号称18个月不卡顿,越用越快,其实很大一部分Android系统的内存优化有关,无非就是利用一些比较成熟的基于统计,机器学习的算法定时清理数据,清理内存,甚至提早加载数据到内存。

二 ,Android常见内存问题和对应检测,解决方式

1. 内存泄露

不止Android程序员,内存泄露应该是大部分程序员都遇到过的问题,能够说大部分的内存问题都是内存泄露致使的,Android里也有一些很常见的内存泄露问题[6],这里简单罗列下:

  • 单例(主要缘由仍是由于通常状况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,致使其没法释放)
  • 静态变量(一样也是由于生命周期比较长)
  • Handler内存泄露[7]
  • 匿名内部类(匿名内部类会引用外部类,致使没法释放,好比各类回调)
  • 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)

对Android内存泄露业界已经有不少优秀的组件其中LeakCanary最为知名(Square出品,Square可谓Android开源界中的业界良心,开源的项目包括okhttp, retrofit,otto, picasso, Android开发大神Jake Wharton就在Square),其原理是监控每一个activity,在activity ondestory后,在后台线程检测引用,而后过一段时间进行gc,gc后若是引用还在,那么dump出内存堆栈,并解析进行可视化显示。使用LeakCanary能够快速地检测出Android中的内存泄露。

正常状况下,解决大部份内存泄露问题后,App稳定性应该会有很大提高,可是有时候App自己就是有一些比较耗内存的功能,好比直播,视频播放,音乐播放,那么咱们还有什么能作的能够下降内存使用,减小OOM呢?

2. 图片分辨率相关

分辨率适配问题。不少状况下图片所占的内存在整个App内存占用中会占大部分。咱们知道能够经过将图片放到hdpi/xhdpi/xxhdpi等不一样文件夹进行适配,经过xml android:background设置背景图片,或者经过BitmapFactory.decodeResource()方法,图片实际上默认状况下是会进行缩放的。在Java层实际调用的函数都是或者经过BitmapFactory里的decodeResourceStream函数

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操做,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,而且Bitmap的大小将比原始的大,能够参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?。

关于Density、分辨率、-hdpi等res目录之间的关系:

举个例子,对于一张1280×720的图片,若是放在xhdpi,那么xhdpi的设备拿到的大小仍是1280×720而xxhpi的设备拿到的多是1920×1080,这两种状况在内存里的大小分别为:3.68M和8.29M,相差4.61M,在移动设备来讲这几M的差距仍是很大的。

尽管如今已经有比较先进的图片加载组件相似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,可是有时就是须要手动拿到一个bitmap或者drawable,特别是在一些可能会频繁调用的场景(好比ListView的getView),怎样尽量对bitmap进行复用呢?这里首先须要明确的是对一样的图片,要 尽量复用,咱们能够简单本身用WeakReference作一个bitmap缓存池,也能够用相似图片加载库写一个通用的bitmap缓存池,能够参考GlideBitmapPool[8]的实现。

咱们也来看看系统是怎么作的,对于相似在xml里面直接经过android:background或者android:src设置的背景图片,以ImageView为例,最终会调用Resource.java里的loadDrawable:

Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {

    // Next, check preloaded drawables. These may contain unresolved theme
    // attributes.
    final ConstantState cs;
    if (isColorDrawable) {
        cs = sPreloadedColorDrawables.get(key);
    } else {
        cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
    }

    Drawable dr;
    if (cs != null) {
        dr = cs.newDrawable(this);
    } else if (isColorDrawable) {
        dr = new ColorDrawable(value.data);
    } else {
        dr = loadDrawableForCookie(value, id, null);
    }

    ...

    return dr;
}

能够看到实际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不一样的drawable,若是图片时同样的,那么最终只会有一份bitmap(享元模式),存放于BitmapState中,获取drawable时,系统会从缓存中取出这个bitmap而后构造drawable。而经过BitmapFactory.decodeResource()则每次都会从新解码返回bitmap。因此其实咱们能够经过context.getResources().getDrawable再从drawable里获取bitmap,从而复用bitmap,然而这里也有一些坑,好比咱们获取到的这份bitmap,假如咱们执行了recycle之类的操做,可是假如在其余地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。

3. 图片压缩

BitmapFactory 在解码图片时,能够带一个Options,有一些比较有用的功能,好比:

  • inTargetDensity 表示要被画出来时的目标像素密度

  • inSampleSize 这个值是一个int,当它小于1的时候,将会被当作1处理,若是大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、下降分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4

  • inJustDecodeBounds 字面意思就能够理解就是只解析图片的边界,有时若是只是为了获取图片的大小就能够用这个,而没必要直接加载整张图片。

  • inPreferredConfig 默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可使用RGB_565,一个像素只会占用2个byte,一下能够省下50%内存。

  • inPurgeableinInputShareable 这两个须要一块儿使用,BitmapFactory.java的源码里面有注释,大体意思是表示在系统内存不足时是否能够回收这个bitmap,有点相似软引用,可是实际在5.0之后这两个属性已经被忽略,由于系统认为回收后再解码实际会反而可能致使性能问题

  • inBitmap 官方推荐使用的参数,表示重复利用图片内存,减小内存分配,在4.4之前只有相同大小的图片内存区域能够复用,4.4之后只要原有的图片比将要解码的图片大既能够复用了。

4. 缓存池大小

如今不少图片加载组件都不只仅是使用软引用或者弱引用了,实际上相似Glide 默认使用的事LruCache,由于软引用 弱引用都比较难以控制,使用LruCache能够实现比较精细的控制,而默认缓存池设置太大了会致使浪费内存,设置小了又会致使图片常常被回收,因此须要根据每一个App的状况,以及设备的分辨率,内存计算出一个比较合理的初始值,能够参考Glide的作法。

5. 内存抖动

什么是内存抖动呢?Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会致使卡顿,严重时还会致使OOM。

一个很经典的案例是string拼接建立大量小的对象(好比在一些频繁调用的地方打字符串拼接的log的时候), 见Android优化之String篇[9]。

而内存抖动为何会引发OOM呢?

主要缘由仍是有由于大量小的对象频繁建立,致使内存碎片,从而当须要分配内存时,虽然整体上仍是有剩余内存可分配,而因为这些内存不连续,致使没法分配,系统直接就返回OOM了。

好比咱们坐地铁的时候,假设你没带公交卡去坐地铁,地铁的售票机就只支持5元,10元,而哪怕你这个时候身上有1万张1块的都没用(是否是以为很反人类..)。固然你能够去兑换5元,10元,而在Android系统里就没那么幸运了,系统会直接拒绝为你分配内存,并扔一个OOM给你(有人说Android系统并不会对Heap中空闲内存区域作碎片整理,待验证)。

其余

经常使用数据结构优化,ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在必定状况下取代HashMap而达到节省内存的目的,具体性能见HashMap,ArrayMap,SparseArray源码分析及性能对比[10],对于key为int的HashMap尽可能使用SparceArray替代,大概能够省30%的内存,而对于其余类型,ArrayMap对内存的节省实际并不明显,10%左右,可是数据量在1000以上时,查找速度可能会变慢。

枚举,Android平台上枚举是比较争议的,在较早的Android版本,使用枚举会致使包过大,在个例子里面,使用枚举甚至比直接使用int包的size大了10多倍 在stackoverflow上也有不少的讨论, 大体意思是随着虚拟机的优化,目前枚举变量在Android平台性能问题已经不大,而目前Android官方建议,使用枚举变量仍是须要谨慎,由于枚举变量可能比直接用int多使用2倍的内存。

ListView复用,这个你们都知道,getView里尽可能复用conertView,同时由于getView会频繁调用,要避免频繁地生成对象

谨慎使用多进程,如今不少App都不是单进程,为了保活,或者提升稳定性都会进行一些进程拆分,而实际上即便是空进程也会占用内存(1M左右),对于使用完的进程,服务都要及时进行回收。

尽可能使用系统资源,系统组件,图片甚至控件的id

减小view的层级,对于能够 延迟初始化的页面,使用viewstub

数据相关:序列化数据使用protobuf能够比xml省30%内存,慎用shareprefercnce,由于对于同一个sp,会将整个xml文件载入内存,有时候为了读一个配置,就会将几百k的数据读进内存,数据库字段尽可能精简,只读取所需字段。

dex优化,代码优化,谨慎使用外部库, 有人以为代码多少于内存没有关系,实际会有那么点关系,如今稍微大一点的项目动辄就是百万行代码以上,多dex也是常态,不只占用rom空间,实际上运行的时候须要加载dex也是会占用内存的(几M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库,此时能够考虑抽取必要部分,开启proguard优化代码,使用Facebook redex使用优化dex(好像有很多坑)。

三 案例

JOOX是IBG一个核心产品,2014年发布以来已经成为5个国家和地区排名第一的音乐App。东南亚是JOOX的主要发行地区,实际上这些地区仍是有不少的低端机型,对App的进行内存优化势在必行。

上面介绍了Android系统内存分配和回收机制,同时也列举了常见的内存问题,可是当咱们接到一个内存优化的任务时,咱们应该从何开始?下面是一次内存优化的分享。

1. 首先是解决大部份内存泄露。

无论目前App内存占用怎样,理论上不须要的东西最好回收,避免浪费用户内存,减小OOM。实际上自JOOX接入LeakCanary后,每一个版本都会作内存泄露检测,通过几个版本的迭代,JOOX已经修复了几十处内存泄露。

2. 经过MAT查看内存占用,优化占用内存较大的地方。

JOOX修复了一系列内存泄露后,内存占用仍是居高不下,只能经过MAT查看究竟是哪里占用了内存。关于MAT的使用,网上教程无数,简单推荐两篇MAT使用教程[11],MAT - Memory Analyzer Tool 使用进阶[12]。

点击Android Studio这里能够dump当前的内存快照,由于直接经过Android Sutdio dump出来的hprof文件与标准hprof文件有些差别,咱们须要手动进行转换,利用sdk目录/platform-tools/hprof-conv.exe能够直接进行转换,用法:hprof-conv 原文件.hprof 新文件.hprof。只须要输入原文件名还有目标文件名就能够进行转换,转换完就能够直接用MAT打开。

下面就是JOOX打开App,手动进行屡次gc的hprof文件。

这里咱们看的是Dominator Tree(即内存里占用内存最多的对象列表)。

  • Shallo Heap:对象自己占用内存的大小,不包含其引用的对象内存。

  • Retained Heap: Retained heap值的计算方式是将retained set中的全部对象大小叠加。或者说,因为X被释放,致使其它全部被释放对象(包括被递归释放的)所占的heap大小。

第一眼看去 竟然有3个8M的对象,加起来就是24M啊 这究竟是什么鬼?

咱们经过List objects->with incoming references查看(这里with incoming references表示查看谁引用了这个对象,with outgoing references表示这个对象引用了谁)

经过这个方式咱们看到这三张图分别是闪屏,App主背景,App抽屉背景。

这里其实有两个问题:

  • 这几张图原图实际都是1280x720,而在1080p手机上实测这几张图都缩放到了1920x1080

  • 闪屏页面,其实这张图在闪屏显示事后应该能够回收,可是由于历史缘由(和JOOX的退出机制有关),这张图被常驻在后台,致使无谓的内存占用。

优化方式:咱们经过将这三张图从xhdpi挪动到xxhdpi(固然这里须要看下图片显示效果有没很大的影响),以及在闪屏显示事后回收闪屏图片。
优化结果:

从原来的8.29x3=24.87M 到 3.68x2=7.36M 优化了17M(有没一种万马奔腾的感受。。可能有时费大力气优化不少代码也优化不了几百K,因此不少状况下内存优化时优化图片仍是比较立竿见影的)。

一样方式咱们发现对于一些默认图,实际要求的显示要求并不高(图片相对简单,同时大部分状况下图片加载会成功),好比下面这张banner的背景图:

优化前1.6M左右,优化后700K左右。

同时咱们也发现了默认图片一个其余问题,由于历史缘由,咱们使用的图片加载库,设置默认图片的接口是须要一个bitmap,致使咱们原来几乎每一个adapter都用BitmapFactory decode了一个bitmap,对同一张默认图片,不但没有复用,还保存了多份,不只会形成内存浪费,并且致使滑动偶尔会卡顿。这里咱们也对默认图片使用全局的bitmap缓存池,App全局只要使用同一张bitmap,都复用了同一份。

另外对于从MAT里看到的图片,有时候由于看不到在项目里面对应的ID,会比较难确认究竟是哪一张图,这里stackoverflow上有一种方法,直接用原始数据经过GIM还原这张图片。

这里其实也看到JOOX比较吃亏一个地方,JOOX很多地方都是使用比较复杂的图片,同时有些地方还须要模糊,动画这些都是比较耗内存的操做,Material Design出来后,不少App都遵循MD设计进行改版,一般默认背景,默认图片通常都是纯色,不只App看起来比较明亮轻快,实际上也省了不少的内存,对此,JOOX后面对低端机型作了对应的优化。

3. 咱们也对Bugly上的OOM进行了分析,发现其实有些OOM是能够避免的。

下面这个crash就是上面提到的在LsitView的adapter里不停建立bitmap,这个地方是咱们的首页banner位,理论上App一打开就会缓存这张默认背景图片了,而实际在使用过一段时间后,才由于为了解码这张背景图而OOM, 改成用全局缓存解决。

下面这个就是传说中的内存抖动

实际代码以下,由于打Log而进行了字符串拼接,一旦这个函数被比较频繁地调用,那么就颇有可能会发生内存抖动。这里咱们新版本已经改成使用stringbuilder进行优化。

还有一些比较奇怪的状况,这里是咱们扫描歌曲文件头的时候发生的,有些文件头竟然有几百M大,致使一次申请了过大的内存,直接OOM,这里暂时也没法修复,直接catch住out of memory error。

4. 同时咱们对一些逻辑代码进行调整,好比咱们的App主页的第三个tab(Live tab)进行了数据延迟加载,和定时回收。

这里由于这个页面除了有大图还有轮播banner,实际强引用的图片会有多张,若是这个时候切到其余页面进行听歌等行为,这个页面一直在后台缓存,实际是很浪费耗内存的,同时为优化体验,咱们又不能直接经过设置主页的viewpager的缓存页数,由于这样常常都会回收,致使影响体验,因此咱们在页面不可见后过一段时间,清理掉adapter数据(只是清空adapter里的数据,实际从网络加载回来的数据还在,这里只是为了去掉界面对图片的引用),当页面再次显示时再用已经加载的数据显示,即减小了不少状况下图片的引用,也不影响体验。

5. 最后咱们也遇到一个比较奇葩的问题,在咱们的Bugly上报上有这样一条上报

咱们在stackoverflow上看到了相关的讨论,大体意思是有些状况下好比息屏,或者一些省电模式下,频繁地调System.gc()可能会由于内核状态切换超时的异常。这个问题貌似没有比较好的解决方法,只能是优化内存,尽可能减小手动调用System.gc()

优化结果

咱们经过启动App后,切换到个人音乐界面,停留1分钟,屡次gc后,获取App内存占用

优化前:

优化后:

屡次试验结果都差很少,这里只截取了其中一次,有28M的优化效果。
固然不一样的场景内存占用不一样,同时上面试验结果是经过屡次手动触发gc稳定后的结果。对于使用其余第三方工具不手动gc的状况下,试验结果可能会差别比较大。

对于上面提到的JOOX里各类图片背景等问题,咱们作了动态的优化,对不一样的机型进行优化,对特别低端的机型设置为纯色背景等方式,最终优化效果以下:

平均内存下降41M。

本次总结主要仍是从图片方面下手,还有一点逻辑优化,已经基本达到优化目标。

四 总结

上面写了不少,咱们能够简单总结,目前Andorid内存优化仍是比较重要一个话题,咱们能够经过各类内存泄露检测组件,MAT查看内存占用,Memory Monitor跟踪整个App的内存变化状况, Heap Viewer查看当前内存快照, Allocation Tracker追踪内存对象的来源,以及利用崩溃上报平台从多个方面对App内存进行监控和优化。上面只是列举了一些常见的状况,固然每一个App功能,逻辑,架构也都不同,形成内存问题也是不尽相同,掌握好工具的使用,发现问题所在,才能对症下药。

参考连接

1.Android 操做系统的内存回收机制
https://www.ibm.com/developerworks/cn/opensource/os-cn-android-mmry-rcycl/

2.阿里巴巴的Android内存优化分享
http://www.infoq.com/cn/presentations/android-memory-optimization

3.Android进程的内存管理分析
http://blog.csdn.net/gemmem/article/details/8920039

4.android dalvik heap 浅析
http://blog.csdn.net/cqupt_chen/article/details/11068129

5.揭秘 ART 细节 —— Garbage collection
http://www.cnblogs.com/jinkeep/p/3818180.html

6.Android性能优化之常见的内存泄漏
http://blog.csdn.net/u010687392/article/details/49909477

7.Android App 内存泄露之Handler
http://blog.csdn.net/zhuanglonghai/article/details/38233069

8.GlideBitmapPool
https://github.com/amitshekhariitbhu/GlideBitmapPool

9.Android 性能优化之String篇
http://blog.csdn.net/vfush/article/details/53038437

10.HashMap,ArrayMap,SparseArray源码分析及性能对比
http://www.jianshu.com/p/7b9a1b386265

11.MAT使用教程
http://blog.csdn.net/itomge/article/details/48719527

12.MAT - Memory Analyzer Tool 使用进阶
http://www.lightskystreet.com/2015/09/01/mat_usage/


更多精彩内容欢迎关注腾讯 Bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索