Android 进阶第二篇——性能优化

Android 进阶第二篇——性能优化

一些Android书籍喜欢把性能优化放在最后的章节,简单提一提作为内容全面的点缀。在这里我将工具使用和性能优化的一些个人经验放在进阶系列博客的开始,因为我认为防病永远比治病有意义重要得多。我们不应该等到一个问题已经发生了,并且到了一定程度才想起来需要重构代码或者进行性能优化,通过早早的学习性能优化的思维和工具能避免很多问题,纠正一些不良的编码习惯,对Coder的编码能力提高具有很大的意义。

UI界面优化

UI界面就是和用户打交道的前台,UI界面的优化直接关乎用户体验,因此UI界面优化是重中之重。在这里,我个人将UI界面优化分两部分内容,一是启动速度优化,二是UI渲染优化。 
1.启动速度优化

启动速度实际上也是很关乎体验的,我个人在使用一些app的时候,点起来之后半天才进入正题往往让我非常反感,下次就不想使用了。当然很多时候这是人为造成的,为了投放广告,让广告显示一段时间,这种就不在讨论之内了。目前很多APP都有一个Splash界面,主要是一个过渡,在加载显示Splash界面的时候做一些资源加载,全部准备就绪后再进入主界面,这种方案实际上是比较好的,当然,前题是Splash界面不是为了投放广告而延时,如果只是为了展示广告,且时间明显超过2秒,我认为体验是非常差的。笔者主要从事系统应用开发,Splash界面的情况很少使用,就不讨论这种方案,主要探讨的是从点击图标到出现主界面这个过程的优化。

这里写图片描述
如上图,黄色框中为应用的主Activity显示到前台经历的生命周期回调,具体的启动情况将在之后的博客中详细分析Activity的启动流程源码。那么我们要做启动速度的优化就很简单了,只需要在这一系列的生命周期回调中移除所有不必要的耗时代码即可。要做到这一点或许并没有那么容易,有时候生命周期中会做很多初始化以及业务逻辑的处理,如果你的项目中,在生命周期中做了太多初始化和业务逻辑的处理,那往往说明这个项目的架构设计有问题,是时候重构代码了。事实上,我理解的生命周期,应该就是一个主线程任务队列,也就是说当我们需要更新一下界面元素的时候,我们就往任务队列扔一个更新任务,包括界面的首次初始化也可以理解为一个更新任务。再通俗的讲,生命周期回调以及主线程中,与更新界面元素无关的代码都应该移除。在这里应该尽可能的使用线程去做加载以及处理各种业务逻辑,包括数据的请求,当数据准备完毕之后再去给主线程消息队列扔一个更新UI的任务。这样的设计不仅可以提升启动速度,还能改善UI卡顿问题,从而彻底避免ANR的发生。

在前一篇博客已提到了关于Activity的冷启动测试命令,使用如下命令即可获取一些启动时间数据

adb shell am start -W -S <包名/完整类名>
  • ThisTime:通常和TotalTime时间相同
  • TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
  • WaitTime:通常比TotalTime大,包括系统影响的耗时

笔者也接手过超烂的代码,是比较老的代码,基本上所有的操作全是在主线程中完成的,包括常说的数据库操作、文件读写,使用体验可想而知。在这里我建议将所有的与界面元素更新无关的代码尽可能全部放入线程中操作,斩断所有的可能阻塞主线程的可能。为什么提出这种建议呢?笔者在系统应用开发过程中,偶尔会收到某些上报的ANR错误报告,其中会碰到很多理论上不可能,但实际上确实会发生的ANR情况,比如开发中在方法名前滥用synchronized关键字,异步线程中发生某种异常,导致主线程调用相应的synchronized方法时被阻塞,从而ANR,synchronized关键字应该被用来锁定关键部分,且笔者非常不建议偷懒直接在方法上加synchronized,应当主动创建不同的锁对象,正确合理使用synchronized关键字;还有的是调用通信相关的远程服务时,发生ANR,例如:TelephonyManager tm = (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);类似这种获取远程服务的代码通常会在主线程中,实际上获取远程服务在绝大多数时候都是可靠的快速的,但是在某种情况下可能会阻塞,例如重启手机后,立刻进入应用,这时系统底层的通信服务可能并未准备好,调用上述代码就发生阻塞,最后导致报ANR挂掉。还包括其他的某些远程服务,因此直接在主线程中调用,并不排除可能发生耗时或阻塞的情况。最简单可靠的手段就是将一切具有发生耗时可能的代码移除到异步线程,保证生命周期回调以及主线程代码简单单一且稳定。

最后提一点,尽可能避免Activity继承过于复杂的父类,性能和功能本身就是一对矛盾,有时候只能采用折中的办法调和矛盾。
某些框架使用起来确实非常简洁方便,但是需要继承一些BaseActivity,某些过于复杂的基类可能会导致Activity生命周期耗时。

2.渲染优化 
关于Android渲染的一些基本知识

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,也就是说每隔16ms就重新绘制一次Activity,那意味着应用必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60帧,每秒帧数的参数是由手机硬件所决定,现在大多数手机屏幕刷新率是60赫兹(国际单位制中频率的单位,表示每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60次=16.66ms)的时间去完成每帧的绘制逻辑操作,如果错过了,比如说我们花费34ms才完成计算,那么就会出现称之为丢帧的情况。

最理想情况 
这里写图片描述 
丢帧情况 

安卓系统尝试在屏幕上绘制新的一帧,如果这一帧还没准备好,画面就不会刷新。如果用户盯着同一张图看了32ms而不是16ms,用户会很容易察觉出卡顿感,哪怕仅仅出现一次掉帧,用户都会发现动画不是很顺畅,如果出现多次掉帧,用户就会开始抱怨卡顿,如果此时用户正在和系统进行交互操作,例如滑动列表或者输入数据,那么卡顿感就会更加明显,现在对绘制每帧花费的时间有了清晰了解,接下来看看是什么原因导致了卡顿,如何去解决些问题

Android把经过测量、布局、绘制后的surface缓存数据,通过SurfaceFlinger渲染到显示屏幕上,通过Android的刷新机制来刷新数据。应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕。在Android的每个View绘制中有三个核心步骤,即通过Measure和Layout来确定当前需要绘制的View所在的大小和位置,通过Draw方法绘制到surface。对于每一个ViewGroup绘制来说,系统会首先遍历它的每一个子View,即深度优先原则。因此随着布局深度的加深,遍历每一个子View的时间会呈现指数级上升。

从Android系统的显示原理中可以看出,UI卡顿的原因有两方面
  • 主线程忙碌。我们知道绘制工作是由主线程即UI线程来负责,主线程忙就会导致VSYNC信号到来时还没有准备好数据产生丢帧

  • 绘制任务过重,绘制一帧内容耗时过长

绘制耗时过长需从UI布局和绘制上进行优化,主线程忙碌则需要避免任何阻碍主线程的操作。关于主线程的的耗时操作我们前面已经讲了基本思路,尽可能将一切与UI更新无关的代码移到异步线程。这里稍微补充一个细节,在使用异步机制时,很多人会使用简单方便的AsyncTask机制,特别是AsyncTask.execute(Runnable)简化的静态方法使用。AsyncTask会给每一个进程开辟一个全局唯一的线程池和消息队列,直接new对象使用和调用execute静态方法都是串行的,也就是说它是排队的一个一个执行。在某个地方执行极为耗时的任务时,消息队列就是阻塞的状态,这时在另外一个地方启动一个任务并不会立刻执行,而是会排队,因此使用时需要小心,在项目里到处使用可能造成不可预知的BUG,建议在执行比较重要的任务时使用并行方式启动new AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR),或者使用其他的异步方案。

使用工具分析优化

启用严格模式(Strict mode enabled)

该工具在上一篇博客工具的使用中已经提过,在开发者选项中启动该模式,该模式可以用来初步检查主线程是否存在耗时问题,依照之前关于启动速度优化的建议,严格模式下不应出现闪烁屏幕现象 
这里写图片描述 

启用GPU呈现模式分析(Profile GPU Rendering)

它是一个图形监测工具,能实时反应当前绘制的耗时。在Android 4.1系统开始提供,同样在开发者选项中开启。 

其中横轴表示时间,纵轴表示每一帧的耗时(单位为ms),随着时间推移,从左到右的刷新呈现,如果高于标准耗时,表示当前这一帧丢失 
柱状图主要由4种颜色组成:红、黄、蓝、紫,这些线对应每一帧在不同阶段的实际耗时。

  • 蓝色:测量绘制的时间,可以理解为执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象。在蓝色的线很高时,有可能是因为需要重新绘制,或者自定义视图的onDraw函数处理事情太多。
  • 红色:是Android进行2D渲染Display List的时间。当红色的线非常高时,可能是由重新提交了视图而导致
  • 橙色:表示处理时间。如果柱状图很高,就意味着GPU太繁忙
  • 紫色:表示将资源转移到渲染线程的时间

任何时候超过绿横线(警戒线,时长16ms),就有可能丢失一帧的内容,为保持UI流畅,应尽可能让这些垂直的柱状条保持在绿横线之下。

除了在手机上直观的分析,还可以使用命令导出具体数据分析
adb shell dumpsys gfxinfo (包名) > gfx.txt
adb shell dumpsys gfxinfo (包名)framestats > gfx.txt  #添加framestats参数获取更详细信息

TraceView

该工具可以分析应用具体每一个方法的执行时间。我们可以用它来分析出现卡顿时在方法的调用上有没有很耗时的操作。总的来说,使用它能找到频繁被调用的方法,也能找到执行非常耗时的方法,前者可能会造成CPU频繁调用,手机发烫的问题,后者则可能造成卡顿问题。 

使用TraceVeiw分析问题之前需要得到一个.trace的文件,我们可以使用如上图方式抓取一个,在第二步开启后操作应用,之后再次点击第二步中的按钮停止抓取,这时会自动打开刚刚抓取的.trace进行分析 

在时间面板中,X轴表示时间消耗,单位为毫秒(ms),Y轴表示各个线程,每个线程中的不同方法使用了不同的颜色来表示,颜色占用面积越宽,表示该方法占用CPU时间越长

时间面板是可以放大查看的 

当需要查看该方法详细信息时,可双击该立柱,分析面板就会自动跳转到该方法

接下来看一下分析面板

解释
Name 所有的调用项
InclCpu Time CPU执行该方法该方法及其子方法所花费的时间
InclCpu Time % CPU执行该方法该方法及其子方法所花费占CPU总执行时间的百分比
ExclCpu Time CPU执行该方法所花费的时间
ExclCpu Time % CPU执行该方法所花费的时间占CPU总时间的百分比
Incl Real Time 该方法及其子方法执行所花费的实际时间,从执行该方法到结束一共花了多少时间
Incl Real Time % 上述时间占总的运行时间的百分比
Excl Real Time 该方法自身的实际允许时间
Excl Real Time % 上述时间占总的允许时间的百分比
Calls+Recur/Total 该方法调用次数加递归次数
Cpu Time/Call CPU执行时间和调用次数的百分比,代表该函数消耗CPU的平均时间
Real Time/Call 实际时间于调用次数的百分比,该表该函数平均执行时间

在分析时,可以主要关注Calls+Recur Calls/Total和Cpu Time/Call这两个值,即调用次数多和耗时久的方法,优化这些方法的逻辑和调用次数,减少耗时

Hierarchy Viewer

仍在使用eclipse的人建议淘汰掉,在Android Stuidio中打开Android Device Monitor 

该工具主要用来查看层级和耗时,通过树状图可以很清晰的分析布局的元素

调试GPU过度绘制(Show GPU Overdraw)

过度绘制指屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层重叠的UI结构中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。该功能也在开发者选项中开启 
 
可以看到,在该项中,微信优化还行,我们看到如下图示 
这里写图片描述 
如图,蓝色、4种颜色代表不同程度的Overdraw情况。对于过度绘制优化的目标,就是尽量减少红色,更多呈现蓝色区域

  • 无色:没有过度绘制
  • 蓝色:多绘制了1次。蓝色是可以接受的
  • 绿色:多绘制了2次。
  • 淡红:多绘制了3次。该区域不超过屏幕的1/4是可接受的
  • 深红:多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域

在做过度绘制分析时,还可以使用前面提到的Hierarchy Viewer工具,详细检查每个View的背景使用情况

过度绘制的主要优化办法就是去除View中不需要的背景,举个简单例子,假如布局中的父控件是灰色背景,子控件也设置了一个灰色背景,则子控件的背景是不必要的,会造成过度绘制,应当去除,同时在设计界面时,也可以考虑尽可能共用背景色。除了我们自己布局中设置的背景,在Activity中往往会被设置一个默认的背景,这个背景由DecorView持有,通常我们自己的应用都会设置一个背景,而这个默认背景就是多此一举,从而造成了过度绘制,应当手动去除

在Activity的onCreate方法中调用

getWindow().setBackgroundDrawable(null);

关于布局优化的建议

布局优化最主要考虑的目标是减少布局的层级,当无法减少时,则可以考虑延迟加载。
  • 使用Merge标签 
    Merge标签可以有效优化某些情况下的多余层级。在自定义View中,如果父元素是FrameLayout或者LinearLayout,则子元素中可以使用;在Activity整体布局中,根元素需要是FrameLayout时,推荐使用。如上图中,系统给Activity默认的content也是FrameLayout,当我们自己应用的根布局也是FrameLayout时,就应该使用Merge标签,那么就会将其中的子元素添加到Merge标签的父布局中。

  • 合理的使用RelativeLayout和LinearLayout 
    实际开发中并不能说这两种布局谁的性能更好,要在特定条件下做最合适的选择。总的来说,RelativeLayout在测量时会进行多次测量才能确定子View大小,因此RelativeLayout不应嵌套使用,否则将会明显增加耗时,而LinearLayout在使用权重属性时,也会发生两次测量,增加耗时。根据这些特点,使用RelativeLayout时应避免嵌套,其本意也是为了让布局更扁平,而使用LinearLayout时,尽量避免使用权重以及嵌套太深。

  • 祭出终极大招,使用ConstraintLayout布局 
    ConstraintLayout是一个Support库,因此使用时需要添加依赖,该布局可以用来彻底解决层级嵌套过深的问题,可视化使用也极其简单,建议可以首先学习郭大神的博客,想要更深入了解,推荐另一篇博客

  • 使用ViewStub标签延迟加载 
    只要不是界面上最首要的元素,实际上都可以延迟加载。我们可以为ViewStub标签指定一个布局,当整体布局加载时,只会对ViewStub初始化,只有当ViewStub被设置为可见时或是调用了ViewStub.inflate()时,ViewStub所代表的布局才会被加载并实例化,而ViewStub的布局属性都会传给它指向的布局,这样就达到了将一些非首要的元素延迟加载到内存的目的,实际上进入应用主界面,很多其他的二级界面用户很可能并不会打开使用,对于这种非首要的界面元素,并不需要在一起动的时候去加载。

  • 其他的一些优化 
    避免使用wrap_content写法,当有明确的数值时,不要偷懒使用wrap_content,避免不必要的测量耗时,养成高性能编码意识和习惯。 
    还可以使用include标签复用布局。

处理内存泄露

  • 内存泄露

    指已经不需要再使用的内存对象,但垃圾回收时不能及时将它们回收,仍然保留在内存中,占用一定的空间

  • 内存回收

    垃圾回收器在GC时会将处于不可达阶段的对象进行内存回收。不可达阶段则是指该对象不再被任何强引用持有

  • 为什么要处理内存泄露 
    1.减少UI卡顿。 
    发生内存泄露时,系统在堆上为应用分配的内存空间可能会不断变小,从而导致频繁触发GC,只要GC消耗的时间超过了16ms的阈值,就会有丢帧的情况出现,导致卡顿。这是因为Dalvik虚拟机在GC时,会暂停应用的线程,这在ART虚拟机上有所改进,ART虚拟使用并发的GC,但仍然存在挂起线的操作。参见ART运行时垃圾收集(GC)过程分析 
    2.减少OOM的发生,确保APP稳定 
    当请求的内存超出系统为应用分配的堆内存空间时,发生内存溢出,导致应用Crash

在分析内存泄露问题时,其实有诸多工具可用,但是分析的过程仍然十分麻烦,因此我个人认为,在处理内存泄露时,应当首先从代码入手,排除所有常见的内存泄露情景,在主观上对内存泄露做出一个推断,然后再使用工具去验证分析,从而解决问题。让我们先看结论

  • 常见内存泄露情景的总结

1.资源对象未关闭 
主要指Cursor、File文件等,它们往往都用了一些缓冲,不使用时应及时关闭,以便缓存数据能够及时回收,对于这种情况,可以多留意log信息,往往会在log信息中报警告

2.注册了对象,在不用时需要注销 
Android中典型的有广播、Provider观察器等,其次是带有add前缀的注册监听方法,如addOnWindowFocusChangeListener等,这些大多都是按照发布订阅模式(观察者模式)设计的,内部维护一个注册对象集合,会一直持有该对象引用,导致无法垃圾回收

3.非静态内部类的滥用(特别是匿名内部类) 
根据Java的设计,非静态内部类会隐式持有外部类的引用,如果非静态内部类创建了一个静态实例或者做了耗时处理,会一直持有外部类,导致无法被垃圾回收

public class MyActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        int a;

        //1、创建非静态内部类的静态实例,将始终持有对MyActivity的强引用,导致MyActivity结束时无法被回收
        static Inner in = new Inner();

        //2、匿名内部类
        new Thread(){
            @Override
            public void run() {
                a = 1;
                Thread.sleep(5000);//线程释放前,导致MyActivity结束时一直不能被回收
            }
        }; 
    }

    class Inner {
        //非静态内部类
    }
}

4.AlertDialog内存泄露 
Android 5.0 以下版本中,在AlertDialog中保存了Activity的引用,则Activity退出前应当dismiss 掉对话框,最好在Activity失去焦点的时候关闭它

5.静态变量的滥用 
静态变量的生命周期可以等同于整个进程,如果在静态变量中储存了一些数据,在进程退出前,这些数据是无法被回收的。比较常见的一种情况是单例的使用没有处理好,比如在单例中保存了非全局的Context引用,导致被引用的对象无法被回收

6.Handler阻塞导致对象无法及时回收 
通常会在主线程创建Handler用于消息通信,当Handler被阻塞时,发出的Message就会一直储存在MessageQueue中,导致Handler无法被及时回收,从而导致Activity或Service不能及时回收,,因此在组件退出时应当清空任务队列

7.使用MVP架构模式不当。将UI上的逻辑接口进行抽象化封装,使得Presenter一直持有UI的实例,导致UI组件实例不能被垃圾回收,因此良好的MVP设计,应当在组件退出时解绑,并使用弱引用保存对组件的引用。

使用工具分析内存泄露

  • Memory Monitors (Android Studio中的工具)

  • Heap Viewer

  • Allocation Tracker

  • MAT (Memory Analyzer Tool,Eclipse中的插件,可下载独立版)

  • LeakCanary

来看一种比较隐蔽但开发中比较常用的情景

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("com.study","MainActivity onCreate ...");
        mHandler.postDelayed(new Runnable() {
            @Override public void run() {
                //do something
            }
        }, Integer.MAX_VALUE);
    }

    Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

以上代码模拟了一种情形,即创建一个匿名内部类Runnable并放到Handler任务队列中,假设Runnable需要执行一个耗时的操作,那么它将阻塞当前的任务队列,我们这里使用postDelayed来模拟耗时,使任务一直存放在队列而不被消费掉,这样就会使得Runnable的外部类一直被持有而无法被回收,正如我们前面说的,匿名内部类会隐式的持有外部类的引用,那么Activity就会内存泄露。

Memory Monitors

Memory Monitors是Android Studio中的工具,首先使用该工具来看一下如何分析内存泄露,并验证我们上面的结论是否成立。

打开Android Studio运行上面的示例代码,并看到Memory Monitors界面 

如上图,该工具可以生动的展现应用的内存使用情况,特别是内存抖动,首先我们点击小货车按钮手动GC几次,使内存趋于平缓稳定,然后看到黄色框中的内容,表示当前应用占用的堆内存大小,这个时候点击GC,占用内存大小不会有太大幅度变化了 
接下来我们我们旋转模拟器的屏幕,我们知道当配置发生改变时,Activity会重新创建,这里切换系统语言也可以达到相同效果,为了方便操作,直接旋转屏幕,可以看到日志中会不断有Log.d("com.study","MainActivity onCreate ...")相关输出,说明不断有新的Activity被创建,查看Monitors中的情况 

我们再次不断的手动GC,发现占用的内存仍然没有大幅减少,开始时的数据是2.98M,现在始终停留在4.25M,可以看到我们只是旋转了屏幕,但是内存开销明显增长了,且GC无法回收,Memory Monitors很直观的就反映了内存泄露情况。

  • 查看内存快照 
    这里写图片描述 
    点击小货车后面的按钮dump内存快照,之后看到如下界面 

    从这个内存快照中可以很清晰的看出来,MainActivity被它的匿名内部类引用,而它的匿名内部类又被Message引用,而Message则被MessageQueue引用
解释
Total Count 内存中该类的对象个数
Heap Count 堆内存中该类的对象个数
Sizeof 物理大小
Shallow size 该对象本身占有内存大小
Retained Size 释放该对象后,节省的内存大小

对象实例区

名称 意义
depth 深度
Shallow Size 对象本身内存大小
Dominating Size 管辖的内存大小
  • 查看总体内存使用情况分析内存泄露 
    除了dump内存快照分析,还有一种更为简单有效的办法初步分析是否存在内存泄露,其操作也在Memory Monitors中 
    首先操作应用旋转屏幕,然后手动GC,最后按返回键退出应用,当我们退出应用时,理论上Activity应该是结束掉了,并且会被回收,那么我们点击如下按钮,查看内存的使用情况 
    这里写图片描述 
    点击之后,会生成一个文本文件,我们查看该文件 
    这里写图片描述
    看到对象一栏,在内存中仍然存在45个View,5个Activity,基本上能清晰的反映内存泄露的情况,只是该操作有较大限制,基本上只能用来检查View和Activity的内存泄露情况。

Heap Viewer

Heap Viewer是SDK自带的工具,主要用于实时查看App分配的内存大小和空闲内存大小,功能跟Memory Monitors是一致的,但是没有Memory Monitors直观,在Eclipse中比较常用,在Android Studio中也可以使用,但不是很推荐,因为会发生adb端口抢占问题,经常导致AS的工具掉线后无法恢复,无法调试无法使用LogCat等,最后可能要重启模拟器,如果是真机有时候还要重启电脑,否则无法识别adb端口,这是AS无法与SDK中的工具兼容,很是蛋疼。不过如果你使用Android Studio作为主要IDE,完全没有使用Heap Viewer的理由,它唯一的一点优势是可以直观显示堆内存中存储的数据的具体类型。

简单说一下使用,选择Tools->Android->Android Device Monitor 

看到右侧的中间部分,显示出了内存中数据的具体类型

Type 解释
free 空闲的对象
data object 数据对象,类类型对象,最主要的观察对象
class object 类类型的引用对象
1-byte array(byte[],boolean[]) 一个字节的数组对象
2-byte array(short[],char[]) 两个字节的数组对象
4-byte array(long[],double[]) 4个字节的数组对象
non-Java object 非Java对象
解释
Count 数量
Total Size 占用的总内存大小
Smallest 占用内存最小的对象的大小
Largest 占用内存最大的对象的大小
Median 拍在中间的对象占用的内存大小
Average 平均值

Allocation Tracker

内存分配跟踪器,该工具也是SDK中带有的,它可以跟踪记录应用程序的内存分配,并列出了它们的调用堆栈,可以查看所有对象内存分配的周期。它主要用于分析较短一段时间内的内存使用情况,在使用Allocation Tracker前,应当先用Memory Monitor或者Heap Viewer找到内存异常情况,然后使用Allocation Tracker分析具体的使用情况。

该工具可以在Android Device Monitor中打开,也可以直接在Android Studio中使用,建议直接使用Android Studio内部的Allocation Tracker,因为更清晰更方便

看到下图,Allocation Tracker和Memory Monitors是在一起的 
这里写图片描述 
步骤: 
1.单击上图追踪按钮 
2.在疑似内存泄漏的地方反复操作应用复现 
3.再次点击追踪按钮结束跟踪,随后自动生成一个alloc结尾的文件

自动进入如下界面 

上图中的count列表示分配次数,Total Size表示总共分配的大小,上图可以看到我们的MainActivity分配了13次

MAT

它是一个快速、功能丰富的Java Heap分析工具,通过分析Java进程的内存快照HPROF文件,从众多的对象中分析,快速计算出在内存中对象的占用大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象

我们选择之前在Memory Monitors处dump的内存快照文件 

打开我们下载的独立版MAT工具,选择File,并open file我们之前导出的内存快照文件 

  • OverView视图 
    是一个总体概览,显示总体的内存消耗情况。Biggest Objects by Retained Size 
    则会列举出Retained Size值最大的几个值,你可以将鼠标放到饼图中的扇叶上,可以在右侧看出详细信息

  • Histogram视图 
    点击工具栏的快捷按钮或OverView视图下方的Actions区域打开。列出内存中的所有实例类型对象、对象的个数以及大小,并支持正则表达式查找 
    这里写图片描述

  • Dominator Tree视图 
    列出最大的对象及其依赖存活的Object。分析流程和Histogram类似,但Dominator Tree能更方便地看出引用关系。这个视图主要就是用来发现大内存对象的,这些对象都可以展开查看更详细的信息,可以看到该对象内部引用的对象 
    这里写图片描述

  • Leak Suspects 
    自动分析泄漏的原因,实际上并不是很准确,该工具只是列出了怀疑的内存泄漏点,以及泄漏的内存大小,在后面有问题列表和所有对象,单击对应的查看详情。该信息可作为一种参考

接下来看一下列信息

列名 解释
Objects 实例对象个数
Shallow Heap 对象自身占用的内存大小,不包括它引用的对象
Retained Heap 当前对象大小与当前对象可直接或间接引用到的对象的大小总和

大致了解了一些MAT工具,接下来就要使用工具定位到具体内存泄露的地方,看到Histogram视图 
这里写图片描述
我们选中MainActivity其中的一个匿名内部类,这里要注意,选择incoming references查看被引用情况 
这里写图片描述
如图,我们再次右键选择Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc refereneces,排除一些软引用、弱引用等干扰 
这里写图片描述

最后我们可以很精准的查看到具体的引用情况,这里是被Message的队列引用,完全符合我们之前的推断。

关于MAT工具,最后补充说明一点,我们除了可以分析一份内存快照,还可以将未发生内存泄露前的快照和发生内存泄露之后的快照进行对比分析

对比步骤也很简单,使用MAT将两份内存快照打开,然后按如下操作 
这里写图片描述 
分别将两份快照都添加到对比中,然后点击感叹号按钮就可以开始对比了 
这里写图片描述

LeakCanary

LeakCanary是一个检测内存泄漏的开源类库,使用可以说是最简单的了,可以直接点击进入GitHub查看

  • 添加依赖
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
  • 初始化 
    重写application
public class ExampleApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
        // Normal app init code...
    }
}

使用自定义的application

<application
        android:name=".ExampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
  • 部署应用 
    在真机或模拟器部署应用之后,会自动安装一个检测的apk 
    这里写图片描述 
    操作本应用,产生内存泄露后,等待通知栏的通知 
    这里写图片描述 
    点击进入查看详细报告 
    这里写图片描述

应用的稳定性优化

应用的稳定性实际上比前面的性能体验方面的优化更加重要,可以想象,一个经常发生Crash或者ANR的应用,是否还有用户会愿意使用?

使用静态代码检查工具

使用静态代码检查工具的好处是,将潜在的隐患暴露在编译期间,从而提升代码的质量,尽可能的排除不稳定的因素。

Android Lint

Android Lint是SDK中提供的比较常用的检测工具,但是很多时候只是将它作为对xml资源的一个检测工具简单使用,实际上Android Lint是功能非常强大的工具,灵活熟练的掌握它,能明显的提高工程中的代码质量

在AndroidStudio中如下使用 
这里写图片描述 
之后可以选择整个工程或者某个模块,完成之后可以看到分析结果 

如图,内存泄露也检测出来了

在使用Lint工具时,如果全部项都检查,可能会花费较多时间,这里也可以指定关注的内容进行检查

这里写图片描述 
在搜索框输入检查项 
这里写图片描述

在IDE里面的使用比较简单,下面着重说明一下命令行如何使用。为什么要强调在命令行使用Lint呢?在大型的项目中,为了确保app的稳定,在Android Jenkins自动编译打包服务器上,可以对待编译打包的工程进行Lint检测,对于不符合Lint检测报告的任务,不允许其打包发布。在多人团体开发中,每个人通常只负责某个部分,为确保APP的稳定性避免人为因素造成的疏漏,这是非常有必要的措施。

Lint命令行的使用 
这里写图片描述 
Lint的检查结果分为6大类 
1·Correctness (正确性) 
2·Security (安全性) 
3·Performance (性能) 
4·Usability (可用性) 
5·Accessibility (可达性) 
6·I18n (国际化)

问题的严重级别(severity)从高到低依次是: 
1·Fatal 
2·Error 
3·Warning 
4·Information 
5·Ignore

在Android Gradle工程的根目录下通过命令运行Lint检测 
Windows 上:

gradlew lint

Linux 或 Mac 上:

./gradlew lint

通过以上命令启动检测使用的是默认的配置项,我们可以手动编写配置文件,实际上配置文件是一个XML文件,下面是一个官方示例

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- Disable the given check in this project -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- Ignore the ObsoleteLayoutParam issue in the specified files -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- Ignore the UselessLeaf issue in the specified file -->
    <issue id="UselessLeaf">
        <ignore path="res/layout/main.xml" />
    </issue>

    <!-- Change the severity of hardcoded strings to "error" -->
    <issue id="HardcodedText" severity="error" />
</lint>

接下来在gradle脚本中指定我们编写的配置文件

android {
  ...
  lintOptions {
    // 重置 lint 配置
    lintConfig file("my-lint.xml")

    // 指定生成报告的路径,它是可选的(默认为构建目录下的 lint-results.html )
    htmlOutput file("lint-report.html")
  }
}

再次通过命令运行,则会使用我们制定的配置项进行检查,下面列出gradle中lintOptions的所有可用项

lintOptions {
        // 设置为 true时lint将不报告分析的进度
        quiet true
        // 如果为 true,则当lint发现错误时停止 gradle构建
        abortOnError false
        // 如果为 true,则只报告错误
        ignoreWarnings true
        // 如果为 true,则当有错误时会显示文件的全路径或绝对路径 (默认情况下为true)
        //absolutePaths true
        // 如果为 true,则检查所有的问题,包括默认不检查问题
        checkAllWarnings true
        // 如果为 true,则将所有警告视为错误
        warningsAsErrors true
        // 不检查给定的问题id
        disable 'TypographyFractions','TypographyQuotes'
        // 检查给定的问题 id
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // * 仅 * 检查给定的问题 id
        check 'NewApi', 'InlinedApi'
        // 如果为true,则在错误报告的输出中不包括源代码行
        noLines true
        // 如果为 true,则对一个错误的问题显示它所在的所有地方,而不会截短列表,等等。
        showAll true
        // 重置 lint 配置(使用默认的严重性等设置)。
        lintConfig file("default-lint.xml")
        // 如果为 true,生成一个问题的纯文本报告(默认为false)
        textReport true
        // 配置写入输出结果的位置;它可以是一个文件或 “stdout”(标准输出)
        textOutput 'stdout'
        // 如果为真,会生成一个XML报告,以给Jenkins之类的使用
        xmlReport false
        // 用于写入报告的文件(如果不指定,默认为lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 如果为真,会生成一个HTML报告(包括问题的解释,存在此问题的源码,等等)
        htmlReport true
        // 写入报告的路径,它是可选的(默认为构建目录下的 lint-results.html )
        htmlOutput file("lint-report.html")

       // 设置为 true, 将使所有release 构建都以issus的严重性级别为fatal(severity=false)的设置来运行lint
       // 并且,如果发现了致命(fatal)的问题,将会中止构建(由上面提到的 abortOnError 控制)
        checkReleaseBuilds true
        // 设置给定问题的严重级别(severity)为fatal (这意味着他们将会
        // 在release构建的期间检查 (即使 lint 要检查的问题没有包含在代码中)
        fatal 'NewApi', 'InlineApi'
        // 设置给定问题的严重级别为error
        error 'Wakelock', 'TextViewEdits'
        // 设置给定问题的严重级别为warning
        warning 'ResourceAsColor'
        // 设置给定问题的严重级别(severity)为ignore (和不检查这个问题一样)
        ignore 'TypographyQuotes'
    }

获取检查列表

上面的配置文件中,只是使用了很少的检查项,实际上可配置的检查项非常多,我们可以通过命令行获取所有的检查项

首先将Lint命令配置到全局的环境变量中,也可以直接进入sdk根目录下的/tools/bin下面执行如下命令

lint --show   #可获得详细列表

lint --list   #仅可获得Issue的id和summary的简表

通过学习使用Lint命令,我们可以编写shell脚本来自动化定制化的实现Lint检查,并将报告生成到指定的位置便于观看,更多关于Lint工具的使用方式,请进入Lint官方的中文文档学习

FindBugs

FindBugs也是本人使用过的一个Java代码静态分析工具,还不错,推荐使用。在AndroidStudio中的安装与使用也非常简单 
这里写图片描述
如图,进入插件商店搜索并安装,重启后使用 
这里写图片描述
成功安装后就会出现如上的菜单,检测分析整个工程会非常耗时,可以按需使用

猜你喜欢

转载自blog.csdn.net/zhangbijun1230/article/details/83788088
今日推荐