Android性能优化

本章重点:
Android设备作为一种移动设备,不管是内存还是CPU的性能都受到了一定的限制,也意味着Android程序不可能无限制的使用内存和CPU资源,过多的使用内存容易导致OOM,过多的使用CPU资源容易导致手机变得卡顿甚至无响应(ANR)。这也对开发人员提出了更高的要求。
本章主要介绍一些有效的性能优化方法。主要包括布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bitmap优化、线程优化等;同时还介绍了ANR日志的分析方法。

  1. 布局优化的思想就是尽量减少布局文件的层级,这样绘制界面时工作量就少了,那么程序的性能自然就高了。
    i. 删除无用的控件和层级
    ii. 其次就是有选择的使用性能较低的ViewGroup,如果布局中既可以使用Linearlayout也可以使用RelativeLayout,那就是用LinearLayout,因为RelativeLayout功能比较复杂,它的布局过程需要花费更多的CPU时间。
    iii.有时候通过LinearLayou无法实现产品效果,需要通过嵌套来完成,这种情况还是推荐使用RelativeLayout,因为ViewGroup的嵌套相当于增加了布局的层级,同样降 低程序性能。
  1. 另一种手段是采用标签、标签和ViewStub。
    i. include标签 标签用于布局重用,可以将一个指定的布局文件加载到当前布局文件中。
    只支持android:layout开头的属性,当然android:id这个属性是个特例;如果指定了android:layout这种属性,那么要求android:layoutwidth和android:layout_height必须存在,否则android:layout属性无法生效。如果 指定了id属性,同时被包含的布局文件的根元素也指定了id属性,会以 指定的这个id属性为准。
    ii. merge标签
    标签一般和 标签一起使用从而减少布局的层级。如果当前布局是一个竖直方向的LinearLayout,这个时候被包含的布局文件也采用竖直的LinearLayout,那么显然被包含的布局文件中的这个LinearLayout是多余的,通过 标签就可以去掉多余的那一层LinearLayout。
    iii. ViewStub
    ViewStub意义在于按需加载所需的布局文件,因为实际开发中,有很多布局文件在正常情况下是不会现实的,比如网络异常的界面,这个时候就没必要在整个界面初始化的时候将其加载进来,在需要使用的时候再加载会更好。在需要加载ViewStub
    布局时:

((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
//或者
View importPanel = ((ViewStub)findViewById(R.id.stub_import)).inflate();

当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,ViewStub也就不再是整个布局结构的一部分了。

1.2 绘制优化
View的onDraw方法要避免执行大量的操作;

  1. onDraw中不要创建大量的局部对象,因为onDraw方法会被频繁调用,这样就会在一瞬间产生大量的临时对象,不仅会占用过多内存还会导致系统频繁GC,降低程序执行效率。
  2. onDraw也不要做耗时的任务,也不能执行成千上万的循环操作,尽管每次循环都很轻量级,但大量循环依然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。根据Google官方给出的标准,View绘制保持在60fps是最佳的,这也就要求每帧的绘制时间不超过16ms(1000/60);所以要尽量降低onDraw方法的复杂度。

1.3 内存泄露优化
内存泄露是最容易犯的错误之一,内存泄露优化主要分两个方面;一方面是开发过程中避免写出有内存泄露的代码,另一方面是通过一些分析工具如LeakCanary或MAT来找出潜在的内存泄露继而解决。

  1. 静态变量导致的内存泄露 比如Activity内,一静态Conext引用了当前Activity,所以当前Activity无法释放。或者一静态变量,内部持有了当前Activity,Activity在需要释放的时候依然无法释放。
  2. 单例模式导致的内存泄露 比如单例模式持有了Activity,而且也没用解注册的操作。因为单例模式的生命周期和Application保存一致,生命周期比Activity要长,这样一来就导致Activity对象无法及时被释放。
  3. 属性动画导致的内存泄露 属性动画中有一类无限循环的动画,如果在Activity播放了此类动画并且没有在onDestroy中去停止动画,那么动画会一直播放下去,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终导致Activity无法释放。解决办法是在Activity的onDrstroy中调用animator.cancel()来停止动画。

1.4 响应速度优化和ANR日志分析
响应速度优化的核心思想就是避免在主线程中去做耗时操作,将耗时操作放在其他线程当中去执行。Activity如果5秒无法响应屏幕触摸事件或者键盘输入事件就会触发ANR,而BroadcastReceiver如果10秒还未执行完操作也会出现ANR。当一个进程发生ANR以后系统会在/data/anr的目录下创建一个文件traces.txt,通过分析该文件就能定位出ANR的原因。

1.5 ListView优化和Bitmap优化
ListView/GridView优化:

  1. 采用ViewHolder避免在getView中执行耗时操作
  2. 其次通过列表的滑动状态来控制任务的执行频率,比如快速滑动时不是和开启大量异步任务
  3. 最后可以尝试开启硬件加速使得ListView的滑动更加流畅。
    Bitmap优化:主要是想是根据需要对图片进行采样显示,详细请上一章。

1.6 线程优化
线程优化的思想是采用线程池,避免程序存在大量的Thread

1.6 一些性能优化的小建议

  1. 避免创建过多的对象,尤其在循环、onDraw这类方法中,谨慎创建对象;
  2. 不要过多的使用枚举,枚举占用的内存空间比整形大。
  3. 常量使用static final来修饰;
  4. 使用一些Android特有的数据结构,比如 SparseArray 和 Pair 等,他们都具有更好的性能;
  5. 适当的使用软引用和弱引用;
  6. 采用内存缓存和磁盘缓存;
  7. 尽量采用静态内部类,这样可以避免非静态内部类隐式持有外部类所导致的内存泄露问题。

Android设备作为一种移动设备,不管是内存还是CPU的性能都受到了一定的限制,无法做到像PC设备那样具有超大的内存和高性能的CPU。鉴于这一点,这也意味着Android程序不可能无限制地使用内存和CPU资源,过多地使用内存会导致程序内存溢出,即OOM。而过多地使用CPU资源,一般是指做大量的耗时任务,会导致手机变得卡顿甚至出现程序无法响应的情况,即ANR。由此来看,Android程序的性能问题就变得异常突出了,这对开发人员也提出了更高的要求。为了提高应用程序的性能,本章第一节介绍了一些有效的性能优化方法,主要内容包括布局优化、绘制优化、内存泄露优化、响应速度优化、ListView优化、Bitmap优化、线程优化以及一些性能优化建议,同时在介绍响应速度优化的同时还介绍了ANR日志的分析方法。

性能优化中一个很重要的问题就是内存泄露,内存泄露并不会导致程序功能异常,但是它会导致Android程序的内存占用过大,这将提高内存溢出的发生几率。如何避免写出内存泄露的代码,这和开发人员的水平和意识有很大关系,甚至很多情况下内存泄露的原因是很难直接发现的,这个时候就需要借助一些内存泄露分析工具,在本章的第二节将介绍内存泄露分析工具MAT的使用,通过MAT就可以发现一些开发过程中难以发现的内存泄露问题。

1.Android的性能优化方法

本节介绍了一些有效的性能优化方法,主要内容包括布局优化、绘制优化、内存泄露优化、响应速度优化、ListView优化、Bitmap优化、线程优化以及一些性能优化建议,在介绍响应速度优化的同时还介绍了ANR日志的分析方法。

1.1 布局优化

布局优化的思想很简单,就是尽量减少布局文件的层级,这个道理是很浅显的,布局中的层级少了,这就意味着Android绘制时的工作量少了,那么程序的性能自然就高了。

如何进行布局优化呢?首先删除布局中无用的控件和层级,其次有选择地使用性能较低的ViewGroup,比如RelativeLayout。如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间。FrameLayout和LinearLayout一样都是一种简单高效的ViewGroup,因此可以考虑使用它们,但是很多时候单纯通过一个LinearLayout或者FrameLayout无法实现产品效果,需要通过嵌套的方式来完成。这种情况下还是建议采用RelativeLayout,因为ViewGroup的嵌套就相当于增加了布局的层级,同样会降低程序的性
能。

布局优化的另外一种手段是采用标签、标签和ViewStub。标签主要用于布局重用,标签一般和配合使用,它可以降低减少布局的层级,而ViewStub则提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,这提高了程序的初始化效率,下面分别介绍它们的使用方法。

标签

标签可以将一个指定的布局文件加载到当前的布局文件中,如下所示。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/app_bg"
        android:gravity="center_horizontal">
        <include layout="@layout/titlebar"/>
        <TextView android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/text"
            android:padding="5dp" />
        ...
    </LinearLayout>

上面的代码中,@layout/titlebar指定了另外一个布局文件,通过这种方式就不用把titlebar这个布局文件的内容再重复写一遍了,这就是的好处。标签只支持以android:layout_开头的属性,比如android:layout_width、android:layout_height,其他属性是不支持的,比如android:background。当然,android:id这个属性是个特例,如果
指定了这个id属性,同时被包含的布局文件的根元素也指定了id属性,那么以指定的id属性为准。需要注意的是,如果标签指定了android:layout_*这种属性,那么要求android:layout_width和android:layout_height必须存在,否则其他android:layout_*形式的属性无法生效,下面是一个指定了android:layout_*属性的示例。

<include android:id="@+id/new_title"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        layout="@layout/title"/>

标签

标签一般和标签一起使用从而减少布局的层级。在上面的示例中,由于当前布局是一个竖直方向的LinearLayout,这个时候如果被包含的布局文件中也采用了竖直方向的LinearLayout,那么显然被包含的布局文件中的LinearLayout是多余的,通过标签就可以去掉多余的那一层LinearLayout,如下所示。

<merge xmlns:android="http://schemas.android.com/apk/res/android">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/one"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/two"/>
    </merge>

ViewStub

ViewStub继承了View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。ViewStub的意义在于按需加载所需的布局文件,在实际开发中,有很多布局文件在正常情况下不会显示,比如网络异常时的界面,这个时候就没有必要在整个界面初始化的时候将其加载进来,通过ViewStub就可以做到在使用的时候再加载,提高了程序初始化时的性能。下面是一个ViewStub的示例:

<ViewStub
        android:id="@+id/stub_import"
        android:inflatedId="@+id/panel_import"
        android:layout="@layout/layout_network_error"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom" />

其中stub_import是ViewStub的id,而panel_import是layout/layout_network_error这个布局的根元素的id。如何做到按需加载呢?在需要加载ViewStub中的布局时,可以按照如下两种方式进行:

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

或者

View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构中的一部分了。另外,目前ViewStub还不支持标签。

1.2 绘制优化

绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面。

首先,onDraw中不要创建新的局部对象,这是因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。

另外一方面,onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。按照Google官方给出的性能优化典范中的标准,View的绘制帧率保证60fps是最佳的,这就要求每帧的绘制时间不超过16ms(16ms = 1000 / 60),虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法的复杂度总是切实有效的。

1.3 内存泄露优化

内存泄露在开发过程中是一个需要重视的问题,但是由于内存泄露问题对开发人员的经验和开发意识有较高的要求,因此这也是开发人员最容易犯的错误之一。内存泄露的优化分为两个方面,一方面是在开发过程中避免写出有内存泄露的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄露继而解决。本节主要介绍一些常见的内存泄露的例子,通过这些例子读者可以很好地理解内存泄露的发生场景并积累规避内存泄露的经验。关于如何通过工具分析内存泄露将在15.2节中专门介绍。

场景1:静态变量导致的内存泄露

面这种情形是一种最简单的内存泄露,相信读者都不会这么干,下面的代码将导致Activity无法正常销毁,因此静态变量sContext引用了它。

public class MainActivity extends Activity {
            private static final String TAG = "MainActivity";
            private static Context sContext;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                sContext = this;
            }
        }

上面的代码也可以改造一下,如下所示。sView是一个静态变量,它内部持有了当前Activity,所以Activity仍然无法释放,估计读者也都明白。

public class MainActivity extends Activity {
            private static final String TAG = "MainActivity";
            private static View sView;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                sView = new View(this);
            }
        }

场景2:单例模式导致的内存泄露

静态变量导致的内存泄露都太过于明显,相信读者都不会犯这种错误,而单例模式所带来的内存泄露是我们容易忽视的,如下所示。首先提供一个单例模式的TestManager,TestManager可以接收外部的注册并将外部的监听器存储起来。

public class TestManager {
        private List<OnDataArrivedListener> mOnDataArrivedListeners = new
                ArrayList<OnDataArrivedListener>();
        private static class SingletonHolder {
            public static final TestManager INSTANCE = new TestManager();
        }
        private TestManager() {
        }
        public static TestManager getInstance() {
            return SingletonHolder.INSTANCE;
        }
        public synchronized void registerListener(OnDataArrivedListener
                                                          listener) {
            if (!mOnDataArrivedListeners.contains(listener)) {
                mOnDataArrivedListeners.add(listener);
            }
        }
        public synchronized void unregisterListener(OnDataArrivedListener
                                                            listener) {
            mOnDataArrivedListeners.remove(listener);
        }
        public interface OnDataArrivedListener {
            public void onDataArrived(Object data);
        }
    }

接着再让Activity实现OnDataArrivedListener接口并向TestManager注册监听,如下所示。下面的代码由于缺少解注册的操作所以会引起内存泄露,泄露的原因是Activity的对象被单例模式的TestManager所持有,而单例模式的特点是其生命周期和Application保持一致,因此Activity对象无法被及时释放。

 protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            TestManager.getInstance().registerListener(this);
        }

场景3:属性动画导致的内存泄露

从Android 3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。下面的动画是无限动画,会泄露当前Activity,解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.button1);
        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton,"rotation",
                0,360).setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
//animator.cancel();
    }

1.4 响应速度优化和ANR日志分析

响应速度优化的核心思想是避免在主线程中做耗时操作,但是有时候的确有很多耗时操作,怎么办呢?可以将这些耗时操作放在线程中去执行,即采用异步的方式执行耗时操作。响应速度过慢更多地体现在Activity的启动速度上面,如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至出现ANR。Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。在实际开发中,ANR是很难从代码上发现的,如果在开发过程中遇到了ANR,那么怎么定位问题呢?其实当一个进程发生ANR了以后,
系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值