Android性能优化总结(一)

Posted by newtonker blog on September 14, 2018

Android性能优化总结(一)

1 什么是性能

Android性能大致可以概括为快、稳、省、小四个方面。在开发过程中,我们可以从这四个方面分析,判断App还有优化的空间。接下来我们就从上述四个方面来分析,看下应该如何优化我们的App。

2 优化方向

2.1 布局和绘制

2.1.1 原理

布局和绘制的原理如下图所示:

其中CPU主要负责测量和布局,已经创建下一步栅格化所需要的DisplayList数据。在这个过程中主要会遇到的问题是布局嵌套的太深,以及重复计算的问题。分析方法是通过HierarchyViewer管理器来分析布局文件。

而GPU的主要工作是讲CPU传过来的DisplayList数据进行栅格化操作(即设置每个像素点需要绘制什么)。这个过程中主要遇到的问题是过度绘制的问题。分析方法是通过开启调试GPU过度绘制来分析重复绘制区域。

2.1.2 CPU布局嵌套太深

在布局阶段,CPU遇到的主要问题是布局嵌套太深,导致计算和测量消耗了大量的计算,进而影响了性能的问题。

解决方案:

  1. 布局解决复杂的布局尽可能采用一层布局来实现(如RelativeLayout或者ConstraintLayout);
  2. include+merge减少布局层级;
  3. ViewStub在View需要展示时再加载;

2.1.3 GPU过度绘制

Android的官方文档中介绍到:屏幕上的某个像素点,在同一帧的时间里被绘制了多次。在多层级的UI结构里,如果看不见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这样浪费了大量的CPU和GPU资源。

在待调试的手机中:开发者选项 -> 调试GPU过度绘制 -> 显示过度绘制区域。这样设置后便可查看GPU的过度绘制问题。如下图所示:蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

在自定义View中,如果已知一些场景下存在过度绘制的问题,可以采用clipRect方法限制绘制区域,进而避免过度绘制的问题;

总结起来解决方案是:

  1. 尽量只设置一层颜色值,避免在父类和子类中重复设置;
  2. 在可预见的场景下,自定义View的onDraw()中考虑使用canvas.clipRect()减少过度绘制

2.2 内存优化

2.2.1 合理选择容器

经验如下:

  1. 如在数据量小于1k的场景下采用ArrayMap来代替HashMap,进而减小内存的占用。
  2. 避免在循环中或onDraw方法方法中创建局部变量,避免内存抖动。

2.2.2 避免内存泄漏

内存泄漏一般指的是程序中那些不再使用的对象无法被GC回收。当存在内存泄漏时,内存的占用会越来越多,GC很容易被触发,GC会越来越频繁。GC触发时,所有的线程都是暂停状态,需要处理的对象越多耗时越长,这样便很容易造成卡顿。

出现内存的场景大致可以分为以下三大类:

  1. 单例/静态变量造成的内存泄漏;
  2. 匿名内部类/非静态内部类;
  3. 资源未关闭造成的内存泄漏;

单例/静态内部类造成的内存泄漏

当单例持有Activity的context时,很容易造成内存泄漏,因为当Activity要销毁时,单例仍然持有Activity的引用,所以无法GC回收,造成内存泄漏。推荐的做法是单例持有Application的Context。

public class SingleInstance {

    private Context mContext;
    private static volatile SingleInstance mInstance;

    private SingleInstance(Context context){
        // 注意:这里容易造成内存泄漏
        this.mContext = context;
        // 推荐的做法如下:
        // this.mContext = context.getApplicationContext();
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
        	  synchronized(SingleInstance.class) {
        	      if(mInstance == null) {
                    mInstance = new SingleInstance(context);
                }
            }
        }
        return sInstance;
    }
}

匿名内部类/非静态内部类

非静态内部类会持有其外部类的引用,当非静态内部类的生命周期比静态内部类长时,很容易造成内存泄漏。

public class TestActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		 //...
		 mHandler.sendEmptyMessageDelayed(0, 10_000);
    }

上面的例子中,Handler发出了一个10s延时的任务。这种延时时间过长的场景,很容易造成内部类的生命周期大于外部类,造成内存泄漏。推荐的一种做法是:

public class TestActivity extends Activity {
    private MyHandler myHandler = new MyHandler(TestActivity.this);

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            TestActivity activity = weakReference.get();
            if(null != activity) {
                // do something
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...
        myHandler.sendEmptyMessageDelayed(0, 10_000);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //最后清空消息
        myHandler.removeCallbacksAndMessages(null);
    }

资源未关闭造成的内存泄漏

  • 网络、文件流未关闭;
  • 注册了广播,却未注销;
  • Service启动后未关闭;
  • EventBus等观察者框架忘记解除注册;

此外,我们还可以借助工具来辅助我们查找内存泄漏:

  • leakcanary查找内存泄漏;
  • MemoryMonitor进行内存监控;
  • AndroidLint提高和改善代码质量;

2.4 启动速度优化

提升App的启动性能,可以提高用户体验。Google官方文档《Launch-Time Performance》对应用启动方式的概述如下:

  1. 冷启动:指的是应用程序从头开始:系统的进程没有,直到此开始,创建了应用程序的进程。 在应用程序自设备启动以来第一次启动或系统杀死应用程序等情况下会发生冷启动。 这种类型的启动在最小化启动时间方面是最大的挑战,因为系统和应用程序比其他启动状态具有更多的工作。

  2. 热启动:与冷启动相比,热启动应用程序要简单得多,开销更低。在热启动,系统会把你活动放到前台,如果所有应用程序的活动仍驻留在内存中,那么应用程序可以避免重复对象初始化,UI的布局和渲染。 热启动显示与冷启动场景相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。

  3. 温启动:用户退出您的应用,但随后重新启动。该过程可能已继续运行,但应用程序必须通过调用onCreate()从头开始重新创建活动。系统从内存中驱逐您的应用程序,然后用户重新启动它。进程和Activity需要重新启动,但任务可以从保存的实例状态包传递到onCreate()中。

这里是慢的定义:

  • 冷启动需要5秒或更长时间。
  • 温启动需要2秒或更长时间。
  • 热启动需要1.5秒或更长时间。

无论何种启动,我们的优化点都是:Application、Activity创建以及回调等过程。谷歌官方给的建议是:

  1. 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
  2. 避免在启动时做密集沉重的初始化(Heavy app initialization);
  3. 避免I/O操作、反序列化、网络操作、布局嵌套等。

2.5 包体优化

对于产品来讲,包体越小,则用户的转化率可能会越高。对于包体的优化,我们需要从代码和资源两个方面去优化。常用的做法有以下几种:

  1. 开启资源压缩,自动删除无用的资源;
  2. 能用代码实现的,尽量不要用切图,如果需要用切图的地方,在不影响视觉效果的前提下,尽量减少切图的大小;能使用矢量图的地方,考虑用矢量图实现;
  3. 插件化。非必须的功能模块可以放到服务端,按需下载。

gradle中删除无用资源的配置如下:

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }

2.7 其他优化

  • 电量优化;
  • 位图bitmap优化;
  • 响应速度优化;
  • 线程优化;
  • 网络优化;

这些将在以后的文章中做具体的讲解;

参考资料

  1. Android性能优化典范 - 第1季
  2. Android 性能优化最佳实践
  3. App Startup Time
  4. Android性能优化之电量篇