Android 性能优化那些事

       Android手机品牌型号特别多,造成了Android手机性能也是参差不齐,我们开发出来的app可能会运行在很多性能比较差的手机上,所以android开发对性能方面要求相对比较高,我们尽量保证我们的app在各种手机上都能流畅运行,才能使客户更愿意用我们的appapp的用户才会越来越多。

       Android应用的开发语言为Java,每个应用最大可使用的堆内存受到Android系统的限制;Android每一个应用的堆内存大小有限,通常的情况为16M-48M;通过ActivityManagergetMemoryClass()来查询可用堆内存限制;3.0(HoneyComb)以上的版本可以通过largeHeap=true”来申请更多的堆内存Nexus S(4.2.1):normal 192, largeHeap 512;如果试图申请的内存大于当前余下的堆内存就会引发OutOfMemoryError();应用程序由于各方面的限制,需要注意减少内存占用,避免出现内存泄漏。

    由于以上种种原因,android app开发做好性能优化至关重要。关于android性能优化方面的文章网上也是很多,建议大家多看看这方面的文章,google官方也已经推出了一系列的优化专题,以后我们可以一起学习下。在这里总结一些关于这方面的一些经验和感悟,分享出来我们一起探讨和交流下。

       Android的性能优化我们可以从UI、内存性能分析优化、API使用以及代码逻辑等,这几个大方面来分析和学习。

一、UI方面的一些性能优化

       1.首先看下造成UI卡段的原因主要有哪些:

       我们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;我们从应用开发的角度往回推理可以得出常见卡顿原因有下面这些:

    (1)人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

    (2)布局Layout过于复杂,无法在16ms内完成渲染;

    (3)同一时间动画执行的次数过多,导致CPUGPU负载过重;

    (4)View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPUGPU负载过重;

    (5)View频繁的触发measurelayout,导致measurelayout累计耗时过多及整个View频繁的重新渲染;

    (6)内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

    (7)冗余资源及逻辑等导致加载和执行缓慢;

    (8)默认情况下,在androidActivityui最长执行时间5秒没有响应系统就会报anr的错误,BroadcastReceiver的最长执行时间超过10秒也会报anr的错误。

    可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮OK的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现很多性能问题。

     2.UI卡顿分析以及一些解决方法:

     分析UI卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具。

    (1).使用Lint进行资源及冗余UI布局等优化

    冗余资源及逻辑等也可能会导致加载和执行缓慢,所以我们就来看看Lint这个工具是如何发现优化这些问题的(当然了,Lint实际的功能是非常强大的,我们开发中也是经常使用它来发现一些问题的,这里主要有点针对UI性能的说明了,其他的雷同)。

      Android Studio 中使用Lint最简单的办法就是将鼠标放在代码区点击右键->Analyze->Inspect Code>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:

    可以看见,Lint检测完后给了我们很多建议的,我们重点看一个关于UI性能的检测结果;上图中高亮的那一行明确说明了存在冗余的UI层级嵌套,所以我们是可以点击跳进去进行优化处理掉的。

    当然了,Lint还有很多功能,大家可以自行探索发挥,这里只是达到抛砖引玉的作用。

    (2).使用HierarchyViewer分析ui嵌套和耗时

     Hierarchy Viewer有两个用途,一个是用于分析当前页面视图层级,再者也能分析布局的时间统计(MeasrueLayoutDraw)所需要的具体时间,本篇主要是关注布局时间的统计功能。

        选中一个Window界面item,然后点击右上方Hierarchy window或者Pixel Perfect window(这里不介绍,主要用来检查像素属性的)即可操作。

     先看下Hierarchy window,如下:

       一个ActivityView树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入Viewid直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psdPhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前ViewActivity中的位置等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:

     类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。

      当然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate LayoutrequestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。

    可以发现,有了HierarchyViewer调试工具,我们的UI性能分析变得十分容易,这个工具也是我们开发中调试UI的利器,在平时写代码时会时常伴随我们左右。

    (3).使用GPU过度绘制分析UI性能

      我们对于UI性能的优化还可以通过开发者选项中的GPU过度绘制工具来进行分析。在设置->开发者选项->调试GPU过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析):


       可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下:

颜色

含义

无色

WebView等的渲染区域

蓝色

1x过度绘制

绿色

2x过度绘制

淡红色

3x过度绘制

红色

4x(+)过度绘制


     由于过度绘制指在屏幕的一个像素上绘制多次(譬如一个设置了背景色的TextView就会被绘制两次,一次背景一次文本;这里需要强调的是Activity设置的Theme主题的背景不被算在过度绘制层级中),所以最理想的就是绘制一次,也就是蓝色(当然这在很多绚丽的界面是不现实的,所以大家有个度即可,我们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见还是比较宽松的规定),因此我们需要依据此颜色分布进行代码优化,譬如优化布局层级、减少没必要的背景、暂时不显示的View设置为GONE而不是INVISIBLE、自定义ViewonDraw方法设置canvas.clipRect()指定绘制区域或通过canvas.quickreject()减少绘制区域等。

     (4).使用Traceviewdmtracedump进行分析优化

     关于UI卡顿问题我们还可以通过运行Traceview工具进行分析,记录了应用程序中每个函数的执行时间;我们可以打开DDMS然后选择一个进程,接着点击上面的“Start Method Profiling”按钮(红色小点变为黑色即开始运行),然后操作我们的卡顿UI(小范围测试,所以操作最好不要超过5s),完事再点一下刚才按的那个按钮,稍等片刻即可出现下图,如下:

      花花绿绿的一幅图我们怎么分析呢?下面我们解释下如何通过该工具定位问题:

    整个界面包括上下两部分,上面是你测试的进程中每个线程运行的时间线,下面是每个方法(包含parentchild)执行的各个指标的值。通过上图的时间面板可以直观发现,整个trace时间段main线程做的事情特别多,其他的做的相对较少。当我们选择上面的一个线程后可以发现下面的性能面板很复杂,其实这才是TraceView的核心图表,它主要展示了线程中各个方法的调用信息(CPU使用时间、调用次数等),这些信息就是我们分析UI性能卡顿的核心关注点,所以我们先看几个重要的属性说明,如下:

属性名

含义

name

线程中调运的方法名;

Incl CPU Time

当前方法(包含内部调运的子方法)执行占用的CPU时间;

Excl CPU Time

当前方法(不包含内部调运的子方法)执行占用的CPU时间;

Incl Real Time

当前方法(包含内部调运的子方法)执行的真实时间,ms单位;

Excl Real Time

当前方法(不包含内部调运的子方法)执行的真实时间,ms单位;

Calls+Recur Calls/Total

当前方法被调运的次数及递归调运占总调运次数百分比;

CPU Time/Call

当前方法调运CPU时间与调运次数比,即当前方法平均执行CPU耗时时间;

Real Time/Call

当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注)


      有了对上面Traceview图表的一个认识之后我们就来看看具体导致UI性能后该如何切入分析,一般Traceview可以定位两类性能问题:

      方法调运一次需要耗费很长时间导致卡顿;

      方法调运一次耗时不长,但被频繁调运导致累计时长卡顿。

      譬如我们来举个实例,有时候我们写完App在使用时不觉得有啥大的影响,但是当我们启动完App后静止在那却十分费电或者导致设备发热,这种情况我们就可以打开Traceview然后按照Cpu Time/Call或者Real Time/Call进行降序排列,然后打开可疑的方法及其child进行分析查看,然后再回到代码定位检查逻辑优化即可;当然了,我们也可以通过该工具来trace我们自定义View的一些方法来权衡性能问题。

    可以看见,Traceview能够帮助我们分析程序性能,已经很方便了,然而Traceview家族还有一个更加直观强大的小工具,那就是可以通过dmtracedump生成方法调用图。具体做法如下:

dmtracedump -g result.png target.trace  //结果png文件  目标trace文件

通过这个生成的方法调运图我们可以更加直观的发现一些方法的调运异常现象。

    (5).使用traces.txt文件进行ANR分析优化

      ANRApplication Not Responding)是AndroidAMSWMS监测应用响应超时的表现;之所以把ANR单独作为UI性能卡顿的分析来说明是因为ANR是直接卡死UI不动且必须要解掉的Bug,我们必须尽量在开发时避免他的出现,当然了,万一出现了那就用下面介绍的方法来分析吧。

    我们应用开发中常见的ANR主要有如下几类:

    按键触摸事件派发超时ANR,一般阈值为5s(设置中开启ANR弹窗,默认有事件派发才会触发弹框ANR);

    广播阻塞ANR,一般阈值为10s(设置中开启ANR弹窗,默认不弹框,只有log提示);

    服务超时ANR,一般阈值为20s(设置中开启ANR弹窗,默认不弹框,只有log提示);

       ANR发生时除过logcat可以看见的log以外我们还可以在系统指定目录下找到traces文件或dropbox文件进行分析,发生ANR后我们可以通过如下命令得到ANR trace文件:

adb pull /data/anr/traces.txt ./

然后我们用txt编辑器打开可以发现如下结构分析:

//显示进程idANR发生时间点、ANR发生进程包名

----- pid 19073 at 2015-10-08 17:24:38 -----

Cmd line: com.example.yanbo.myapplication

//一些GCobject信息,通常可以忽略

......

//ANR方法堆栈打印信息!重点!

DALVIK THREADS (18):"main" prio=5 tid=1 Sleeping

  | group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000

  | sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8

  | state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100

  | stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB

  | held mutexes=

  at java.lang.Thread.sleep!(Native method)

  - sleeping on <0x0a2ae345> (a java.lang.Object)

  at java.lang.Thread.sleep(Thread.java:1031)

  - locked <0x0a2ae345> (a java.lang.Object)

//真正导致ANR的问题点,可以发现是onClick中有sleep导致。我们平时可以类比分析即可,这里不详细说明。

  at java.lang.Thread.sleep(Thread.java:985)

  at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)

  at android.view.View.performClick(View.java:4908)

  at android.view.View$PerformClick.run(View.java:20389)

  at android.os.Handler.handleCallback(Handler.java:815)

  at android.os.Handler.dispatchMessage(Handler.java:104)

  at android.os.Looper.loop(Looper.java:194)

  at android.app.ActivityThread.main(ActivityThread.java:5743)

  at java.lang.reflect.Method.invoke!(Native method)

  at java.lang.reflect.Method.invoke(Method.java:372)

  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)

  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)

......

//省略一些不常关注堆栈打印

至此常见的应用开发中ANR分析定位就可以解决了。

    3.UI性能分析解决总结

      进过以上分析,关于Android UI卡顿的性能分析还是有很多工具的,上面只是介绍了应用开发中我们经常使用的一些而已,还有一些其他的,譬如SystarceOprofile等工具不怎么常用,这里就不再详细介绍。

       通过上面UI性能的原理、原因、工具分析总结可以发现,我们在开发应用时一定要时刻重视UI性能问题,如若真的没留意出现了性能问题,不妨使用上面的一些案例方式进行分析。但是那终归是补救措施,在我们知道上面UI卡顿原理之后我们应该尽量从项目代码架构搭建及编写时就避免一些UI性能问题,具体项目中常见的注意事项如下:

    (1).布局优化;尽量使用includemergeViewStub标签;

     a.使用include标签将可复用的组件抽取出来(引用布局)
     b.使用merge标签减少布局的嵌套层次(merge相当于framelayout
       场景1:布局根结点是FrameLayout且不需要设置backgroundpadding等属性,可以用merge代替。
       场景2:某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时,顶结点会自动被忽略。
      c.使用ViewStub标签来加载一些不常用的布局
        作用:ViewStub标签同include标签一样可以用来引入一个外部布局,不同的是,ViewStub引入的布局默认不会扩张,既不会占用显示也不会占用位置,从而在解析Layout时节省cpu和内存

    (2)尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽量使用GONE替换INVISIBLE,使用weight后尽量将widthheigh设置为0dp减少运算,Item存在非常复杂的嵌套时考虑使用自定义Item View来取代,减少measurelayout次数等。

    列表及Adapter优化;尽量复用getView方法中的相关View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行UI元素刷新等。

       背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。

    自定义View等绘图与布局优化;尽量避免在drawmeasurelayout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少drawmeasurelayout等执行次数。

    尽量使用RelativeLayout,可以减少层级的嵌套。
    慎用LinearLayoutlayout_weight属性,可以使用RelativeLayoutcenterHorizontal=”true”toLefttoRight代替.

    避免复杂的View层级。布局越复杂就越臃肿,就越容易出现性能问题,寻找最节省资源的方式去展示嵌套的内容;

     尽量避免在视图层级的顶层使用相对布局 RelativeLayout 。相对布局 RelativeLayout 比较耗资源,因为一个相对布局 RelativeLayout 需要两次度量来确保自己处理了所有的布局关系,而且这个问题会伴随着视图层级中的相对布局 RelativeLayout 的增多,而变得更严重;

    布局层级一样的情况建议使用线性布局 LinearLayout 代替相对布局 RelativeLayout,因为线性布局 LinearLayout 性能要更高一些;确实需要对分支进行相对布局 RelativeLayout 的时候,可以考虑更优化的网格布局 GridLayout ,它已经预处理了分支视图的关系,可以避免两次度量的问题;

      相对复杂的布局建议采用相对布局 RelativeLayout ,相对布局 RelativeLayout 可以简单实现线性布局 LinearLayout 嵌套才能实现的布局;

     不要使用绝对布局 AbsoluteLayout 

     去掉多余的不可见背景。有多层背景颜色的布局,只留最上层的对用户可见的颜色即可,其他用户不可见的底层颜色可以去掉,减少无效的绘制操作;

    尽量避免使用 layoutweight 属性。使用包含 layoutweight 属性的线性布局 LinearLayout 每一个子组件都需要被测量两次,会消耗过多的系统资源。在使用 ListView 标签与 GridView 标签的时候,这个问题显的尤其重要,因为子组件会重复被创建。平分布局可以使用相对布局 RelativeLayout 里一个 0dp view 做分割线来搞定,如果不行,那就这样。

二、Memory内存性能分析优化

    说完了应用开发中的UI性能问题后我们就该来关注应用开发中的另一个重要、严重、非常重要的性能问题了,那就是内存性能优化分析。

      Android其实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的android设备硬件配置都可以了,所以内存不会再像以前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用还是越用系统越卡呢?这里面的原因有很多,不过相信有了这一章下面的内容分析,作为一个移动开发者的你就有能力打理好自己应用的那一亩三分地内存了,能做到这样最好了。

    1.Andriod内存管理原理

     1)系统内存管理

      Android系统内核是基于Linux,所以说Android的内存管理其实也是Linux的升级版而已。Linux在进程停止后就结束该进程,而Android把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台serviceThread等单独UI线程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。

    再直白点就是说Android系统级内存管理机制其实类似于Java的垃圾回收机制;在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型。

 

   2)应用级内存管理

      在说应用级别内存管理原理时我们先想一个问题,假设有一个内存为1GAndroid设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢?

    这肯定是不会的,Android的框架使得每个应用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。

    既然每个Android应用程序都执行在自己的虚拟机中,那了解Java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为64M,也可以为128M等;它的配置是在Android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。

    接着我们运行的App在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是在Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收一般都会有明显的log打印出回收类型,常见的如下:

GC_MALLOC——内存分配失败时触发;

GC_CONCUR RENT——当分配的对象大小超过一个限定值(不同系统)时触发;

GC_EXPLICIT——对垃圾收集的显式调用(System.gc())

GC_EXTERNAL_ALLOC——外部内存分配失败时触发;

      通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。

    2.Android内存泄露性能分析

      1)什么是Android应用内存泄露

      Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。如下就是一个最常见简单的泄露例子:

public final class MainActivity extends Activity {

    private DbManager mDbManager;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        //DbManager是一个单例模式类,这样就持有了MainActivity引用,导致泄露

        mDbManager = DbManager.getInstance(this);

    }

}

    可以看见,上面例子中我们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就无法得到垃圾回收,也就造成了内存泄露。

      内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:

    应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC;

       应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);

    应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值OOM);

      造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。

    (2)查找Android应用是否有内存泄露的方式

      知道了内存泄露的概念之后接下来就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?提供了几种常用的方式:

    1.ASMemory窗口如下,详细的说明这里就不解释了,很简单很直观(使用频率高):

    2.DDMS-Heap内存监测工具窗口如下,详细的说明这里就不解释了,很简单(使用频率不高):

    3.dumpsys meminfo命令如下(使用频率非常高,非常高效,平时一般关注几个重要的Object个数即可判断一般的泄露;当然了,adb shell dumpsys meminfo不跟参数直接展示系统所有内存状态):

    4.leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。

    关于leakcanary工具的配置使用方式这里不再详细介绍,

In your build.gradle:

 dependencies {

   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'

   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

 }

In your Application class:

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...

  }

}

具体看官方文档:

https://github.com/square/leakcanary

    5.MAT工具定位分析

     Eclipse Memory Analysis Tools点我下载)是一个专门分析Java堆数据内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置即可。

     这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然Android Studio已经实现了部分功能,比较简单,遇到问题目前还是使用Eclipse Memory Analysis Tools吧。

     Eclipse可以集成mat插件使用,下载地址:http://www.eclipse.org/mat/downloads.php

     MAT也是一个单独的可以运行的工具;

     我们需要用MAT打开内存分析的文件, 上文给大家介绍了使用Android Studio生成了 hprof文件, 这个文件在呢, Android Studio中的Captrues这个目录中,可以找到

      hprof目录]

    注意,这个文件不能直接交给MAT, MAT是不识别的, 我们需要右键点击这个文件,转换成MAT识别的。

    导出标准的hprof

    然后用MAT打开导出的hprof(File->Open heap dump) MAT会帮我们分析内存泄露的原因

    打开标准的hprof

    总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。

    3.Android应用开发规避内存泄露建议

    (1)创建对象从来都不应该是一件随意的事情,因为创建一个对象就意味着垃圾回收器需要回收一个对象,而这两步操作都是需要消耗时间的。虽说创建一个对象的代价确实非常小,并且Android 2.3版本当中又增加了并发垃圾回收器机制,这让GC操作时的停顿时间也变得难以察觉,但是这些理由都不足以让我们可以肆意地创建对象,需要创建的对象我们自然要创建,但是不必要的对象我们就应该尽量避免创建。

    (2)Context使用不当造成内存泄露;不要对一个Activity Context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用ApplicationContext代替Context的地方进行替换。

    (3)非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

    (4)警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过ActivityThread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThreadrun方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。

    (5)对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。

    (6)创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

    (7)不要在执行频率很高的方法或者循环中创建对象,可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。

    (8)如果你并不需要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,这会让调用的速度提升15%-20%,同时也不用为了调用这个方法而去专门创建对象了,这样还满足了上面的一条原则。另外这也是一种好的编程习惯,因为我们可以放心地调用静态方法,而不用担心调用这个方法后是否会改变对象的状态(静态方法内无法访问非静态字段)。

    (9)对常量使用static final修饰符

    我们先来看一下在一个类的最顶部定义如下代码:

[java] view plain copy 

static int intVal = 42;  

static String strVal = "Hello, world!";  

    编译器会为上述代码生成一个初始化方法,称为<clinit>方法,该方法会在定义类第一次被使用的时候调用。然后这个方法会将42的值赋值到intVal当中,并从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式来去访问具体的值了。

    但是我们还可以通过final关键字来对上述代码进行优化:

[java] view plain copy 

static final int intVal = 42;  

static final String strVal = "Hello, world!";  

    经过这样修改之后,定义类就不再需要一个<clinit>方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal时会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。

    另外需要大家注意的是,这种优化方式只对基本数据类型以及String类型的常量有效,对于其它数据类型的常量是无效的。不过,对于任何常量都是用static final的关键字来进行声明仍然是一种非常好的习惯。

    (10)使用增强型for循环语法

    增强型for循环(也被称为for-each循环)可以用于去遍历实现Iterable接口的集合以及数组,这是jdk 1.5中新增的一种循环模式。当然除了这种新增的循环模式之外,我们仍然还可以使用原有的普通循环模式,只不过它们之间是有效率区别的,我们来看下面一段代码:

[java] view plain copy 

static class Counter {  

    int mCount;  

}  

Counter[] mArray = ...  

public void zero() {  

    int sum = 0;  

    for (int i = 0; i < mArray.length; ++i) {  

        sum += mArray[i].mCount;  

    }  

}  

public void one() {  

    int sum = 0;  

    Counter[] localArray = mArray;  

    int len = localArray.length;  

    for (int i = 0; i < len; ++i) {  

        sum += localArray[i].mCount;  

    }  

}  

public void two() {  

    int sum = 0;  

    for (Counter a : mArray) {  

        sum += a.mCount;  

    }  

}  

    可以看到,上述代码当中我们使用了三种不同的循环方式来对mArray中的所有元素进行求和。其中zero()方法是最慢的一种,因为它是把mArray.length写在循环当中的,也就是说每循环一次都需要重新计算一次mArray的长度。而one()方法则相对快得多,因为它使用了一个局部变量len来记录数组的长度,这样就省去了每次循环时字段搜寻的时间。two()方法在没有JITJust In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,唯一需要注意的是这种写法需要JDK 1.5之后才支持。

    但是这里要跟大家提一个特殊情况,对于ArrayList这种集合,自己手写的循环要比增强型for循环更快,而其他的集合就没有这种情况。因此,对于我们来说,默认情况下可以都使用增强型for循环,而遍历ArrayList时就还是使用传统的循环方式吧。

    (11)多使用系统封装好的API

      Java语言当中其实给我们提供了非常丰富的API接口,我们在编写程序时如果可以使用系统提供的API就应该尽量使用,系统提供的API完成不了我们需要的功能时才应该自己去写,因为使用系统的API在很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。

    比如说String类当中提供的好多API都是拥有极高的效率的,像indexOf()方法和一些其它相关的API,虽说我们通过自己编写算法也能够完成同样的功能,但是效率方面会和这些方法差的比较远。这里举个例子,如果我们要实现一个数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然是可行的,但是如果我们直接使用系统中提供的System.arraycopy()方法将会让执行效率快9倍以上。

    (12)Handler发送消息时尽量使用obtain去获取已经存在的Message对象进行复用,而不是新new Message对象,这样可以减轻内存压力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值