Android应用性能优化

前言

一个友好的 Android 应用应该具有运行稳定、操作流畅、省电、省流量、包体小等特点,但实际开发中,随着项目的不断迭代,应用逐渐变得卡顿、耗电、耗流量、包体过大,有时甚至出现严重的崩溃。本文将结合实例向大家展示怎么去识别、诊断、解决 Android 应用中常见的性能问题。

一、卡顿优化

大多数用户感知到的卡顿问题的主要根源是因为渲染性能。Android 系统每隔16ms发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

如果某个操作花费时间是24ms,系统在得到 VSYNC 信号的时候就无法进行正常渲染,这样就发生了丢帧现象。用户在32ms内看到的会是同一帧画面,从而感觉卡顿。

  • Android 系统为什么每隔16ms发出 VSYNC 信号触发对 UI 进行渲染呢?这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新,所以超过60fps是没有必要的,1000/60≈16ms。

可以借助开发者选项的的 GPU 渲染模式查看应用渲染效率开发者选项-GPU渲染模式-在屏幕上显示为条形图

  • 绿色线:代表16ms,保持应用流畅的关键就在于让垂直的柱状条尽可能地都保持在绿线下面,任何时候超过绿线,就有可能丢失一帧的内容;
  • 柱状条蓝色:表示测量/绘制时间,当柱状图很高的时候,有可能是因为一堆视图突然变得无效了,需要重新绘制;或者自定义视图的 onDraw() 过于复杂;
  • 柱状条红色:表示执行时间,这部分是 Android 进行2D渲染 Display List 的时间,为了绘制到屏幕上,Android 需要使用 OpenGL ES 的 API 接口来绘制 Display List。这些 API 将数据发送到 GPU,最终在屏幕上显示出来。当柱状图很高的时候,这些复杂的自定义 View 就是罪魁祸首;
  • 柱状条橙色:表示处理时间,CPU 告诉 GPU 渲染一帧是一个阻塞调用,CPU 会一直等待 GPU 发出接到命令的回复,如果柱状图很高,那就意味着给 GPU 太多的工作,太多的视图需要调用 OpenGL 命令去处理。

有很多原因可以导致丢帧,也许是因为 layout 太过复杂,无法在16ms内完成渲染,也有可能是因为 UI 上层叠太多的绘制单元,还有可能是因为动画执行的次数过多,这些都会导致 CPU 或者 GPU 负载过重。我们可以通过一些工具来定位问题,比如可以使用 Hierarchy Viewer 检测渲染效率,去除布局中不必要的嵌套;也可以打开开发者选项中的 Show GPU Overdraw 检测过度绘制,移除不必要的背景。

布局层次检测

可以使用 Android Device Monitor 自带的 Hierarchy Viewer 查看布局的层次结构。

  • 自 Android Studio 3.0 开始 Google 已经弃用 Android Device Monitor,在 Android Studio 中已经找不到 Android Device Monitor 的入口,但我们仍然可以使用其他方式来使用 Android Device Monitor。在终端运行 android-sdk/tools 目录下的 monitor 脚本,即可打开 Android Device Monitor。

为了实现以上布局结构,我们可以使用以下两种方法实现。

方法1代码结构:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_weight="1"
        android:layout_marginStart="10dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Title" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_description"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Description" />

            <TextView
                android:id="@+id/tv_source"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="10dp"
                android:text="Source"/>

        </LinearLayout>

    </LinearLayout>

    <Button
        android:id="@+id/bt_open"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Open" />

</LinearLayout>

 方法1 Hierarchy Viewer 视图:

 方法2代码结构:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/iv_icon"
        android:layout_marginStart="10dp"
        android:text="Title" />

    <TextView
        android:id="@+id/tv_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/tv_title"
        android:layout_below="@+id/tv_title"
        android:text="Description" />

    <TextView
        android:id="@+id/tv_source"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/tv_description"
        android:layout_toRightOf="@+id/tv_description"
        android:layout_marginStart="10dp"
        android:text="Source" />

    <Button
        android:id="@+id/bt_open"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="Open" />

</RelativeLayout>

方法2Hierarchy Viewer 视图:

方法1 Measure+Layout+Draw 时间为4.991ms,方法2 Measure+Layout+Draw 时间为4.992ms,但方法2 view 层级明显较浅,并且黄灯明显减少(两者都不存在红灯),在布局更复杂时,方法2渲染效率更高。

布局层次优化

  • 在 view 层级相同的情况下,尽量使用 LinerLayout 代替 RelativeLayout,因为 RelativeLayout 在测量的时候会测量二次(横、纵各一次),而 LinerLayout 只测量一次;
  • 如果布局比较复杂,单纯的使用 LinearLayout 会导致层级过深,使用RelativeLayout /ConstraintLayout可以有效的减少布局层级;
  • 使用 include 标签,重用通用布局。抽取通用的布局可以让布局的逻辑更清晰明了;
  • 使用 merge 标签,减少布局嵌套层次。通常配合 include 标签一起使用;
  • 使用 ViewStub 标签,提升渲染性能。ViewStub 标签实质上是一个宽高都为0的不可见 View,通过延迟加载布局的方式提升渲染性能;
  • 显示隐藏尽量用GONE代替INVISIBLE。因为GONE不绘制,INVISIBLE绘制后隐藏。

过度绘制检测

开发者选项-调试GPU过度绘制-显示过度绘制区域

原色:没有过度绘制
蓝色:1 次过度绘制
绿色:2 次过度绘制
粉色:3 次过度绘制
红色:4 次及以上过度绘制

我们仍以刚才的那个布局结构作为示例。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <LinearLayout
        android:id="@+id/ll_title_container"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_weight="1"
        android:layout_marginStart="10dp"
        android:background="@android:color/white">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Title" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_description"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Description" />

            <TextView
                android:id="@+id/tv_source"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="10dp"
                android:text="Source"/>

        </LinearLayout>

    </LinearLayout>

    <Button
        android:id="@+id/bt_open"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Open" />

</LinearLayout>

显示过度绘制区域如下图所示:

可以看到布局整体有1次过度绘制,Tile 等文本内容有2次过度绘制,OPEN 按钮有3次过度绘制。这个Activity要求背景色是白色,我们在根布局中去设置了背景色白色,这时id为 ll_title_container 的 LinearLayout 背景色就显得多余了,可以去掉。为什么布局整体有1次过度绘制呢?原来我们的Activity的布局最终会添加在 DecorView 中,DecorView 中的背景就没有必要了,可以在Activity 中调用 getWindow().setBackgroundDrawable(null) 去除默认背景色。

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().setBackgroundDrawable(null);
    }
}

经过上述处理后,再次查看过度绘制区域如下图所示:

过度绘制优化

  • 移除多余的背景。移除布局文件中非必须的背景,如果我们的布局设置了背景,同时 DecorView 会有一个默认背景,这时可以把 DecorView 的背景去掉;如果父控件有颜色,也是子控件需要的颜色,那么就不必在子控件加背景颜色;如果每个子控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要再父控件上加背景颜色;

自定义 View 优化

  • onDraw() 中不要做耗时的任务,也不做过多的循环操作,特别是嵌套循环。虽然每次循环耗时很小,但是大量的循环势必霸占CPU的时间片,从而造成View的绘制过程不流畅;
  • onDraw() 中不要创建新的局部对象。因为 onDraw() 一般都会频繁调用,就意味着会产生大量的临时对象,不仅占用过大内存,而且会导致系统更加频繁的GC,大大降低程序的执行速度和效率;
  • 绘制时使用 canvas.clipRect() 来帮助系统识别哪些是可见的区域,只有可见的区域才绘制。

 

二、内存优化

由于 Java 直接将内存交由JVM 管理并具有GC(Garbage Collection)机制,我们大多时候不需要关心内存的使用情况,但如果使用不当,过多的内存泄露最终会导致内存溢出,影响程序的正常使用。所以避免内存泄露就显得尤其重要。

常用的内存泄漏检测工具

LeakCanary:常用的内存泄漏检查第三方库,使用比较方便。

Memory Profiler:Android Studio 自3.0版本开始自带的内存使用可视化工具。

Memory Monitor :Android Studio 3.0版本之前自带的内存使用可视化工具,可以很好地监控系统或应用的内存使用情况。

Heap Viewer :Android Device Monitor 自带的内存检测工具,主要功能是查看不同数据类型在内存中的使用情况,可以看到当前进程中的 Heap Size 的情况,分别有哪些类型的数据,以及各种类型数据占比情况。通过分析这些数据来找到大的内存对象,再进一步分析这些大对象,进而通过优化减少内存开销,也可以通过数据的变化发现内存泄漏。Heap Viewer不只可以用来检测是否有内存泄漏,对于内存抖动,我们也可以用该工具检测,因为内存抖动的时候,会频繁发生GC,这个时候我们只需要开启 Heap Viewer,观察数据的变化,如果发生内存抖动,会观察到数据在短时间内频繁更新。

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

主要功能:

  • 显示可用和已用内存,并且以时间为维度实时反应内存分配和回收情况;
  • 快速判断应用程序的运行缓慢是否由于过度的内存回收导致;
  • 快速判断应用程序崩溃是否由于内存不足导致。

常见内存泄漏情景及解决办法

  • 单例模式导致的内存泄漏
    详情:单例的生命周期和应用的生命周期一样长,创建单例时如果传入Activity 类型的 Context,使得持有对 Activity 的引用,Activity 关闭时无法及时回收内存,从而导致内存泄露。
    解决办法:创建单例时传入Application 类型的 Context。
public class Singleton {

    private static Singleton sSingleton = null;

    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

    public static Singleton getSingleton(Context context) {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton(context);
                }
            }
        }
        return sSingleton;
    }
}
  • 非静态内部类导致的内存泄漏
    详情:非静态内部类的静态对象一直持有着外部类的引用,导致外部类无法被回收。
    解决办法:将内部类设为静态内部类或独立出来。
public class MainActivity extends Activity {

    private static Inner sInner = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (sInner == null) {
            sInner = new Inner();
        }
    }

    class Inner {

    }
}
  • 无限循环的属性动画导致的内存泄漏
    详情:如果没有在 Activity.onDestroy() 中停止无限循环的属性动画,使得 View 持有了Activity。
    解决办法:在 Activity.onDestroy() 中调用 Animator.cancel() 停止无线循环动画。
  • 资源性对象未关闭导致的内存泄漏
    详情:未及时关闭资源导致内存泄漏,如File、Cursor、Stream、Bitmap等。
    解决办法:在 Activity.onDestroy() 中及时关闭。
  • 注册对象未注销导致的内存泄漏
    详情:事件注册后未注销,会导致观察者列表中维持着对象的引用,如动态注册的BraodcastReceiver。
    解决办法:在 Activity.onDestroy() 中及时注销。
  • Handler导致的内存泄漏
    详情:在 Activity 中声明一个内部类 Handler,当使用这个 Handler 发送一个延迟消息时,此消息执行前,Activity关闭会造成内存泄漏。
    解决办法:使用静态内部类+弱引用;当外部类结束生命周期时清空消息队列。
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // do something

            }
        }, TimeUnit.MINUTES.toMillis(1));
        finish();
    }

    private Handler handler = new Handler() {

        @Override
        public void handleMessage(@NonNull Message msg) {
            // do something
        }

    };
}
  • 多线程导致的内存泄漏
    详情:AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。
    解决办法:将 AsyncTask 和 Runnable 设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用。
  • WebView导致的内存泄漏。
    详情:布局文件中定义的 WebView 比较特殊,即使是调用了它的 destroy 方法,依然会导致内存泄漏。
    解决办法:使用时再创建 WebView 并添加到布局中;让 WebView 所在的 Activity 处于独立进程中,当这个 Activity 结束时杀死当前 WebView 所处的进程;Activity 的 onDestroy() 方法先移除并销毁 WebView 再执行super. onDestroy()。

    @Override
    public void onDestroy() {
        ViewParent viewParent = mWebView.getParent();
        if (viewParent != null) {
            ((ViewGroup) viewParent).removeView(mWebView);
            mWebView.stopLoading();
            mWebView.removeAllViews();
            mWebView.destroy();
        }
        super.onDestroy();
    }

扩大内存

除了尽量避免内存泄露,我们也可以扩大内存来减少内存溢出发生的几率。常见的扩大内存有以下方法:

  • 在清单文件中的 Application 下添加 android:largeHeap="true" 属性;
  • 同一个应用开启多个进程来扩大一个应用的总内存空间。

 

三、包体优化

包体优化的核心是减小安装包的大小,常见的方案如下:

  • 使用 ProGuard 代码混淆器混淆、压缩,可以在一定程度上减小安装包的大小;
  • 在不影响应用显示效果的情况下只保留 drawable-xxhdpi 格式的图片资源并尽量压缩图片;
  • 在不影响应用显示效果的情况下尽量使用系统自带的字体,某些字体文件过大,比如:思源黑体;
  • 删除应用中无用的依赖库及资源(使用 Lint 工具检测);
  • 使用 WebP 格式代替 PNG 格式图片(根据 Google 测试结果,WebP 的无损压缩比相同效果的 PNG 少了45%的文件大小,即使这些 PNG 图片使用PNG Crush和PNGOUT处理过,WebP还是可以减少28%的文件大小。);
  • 在使用了 SO 库的时候优先保留v7版本的 SO 库,删掉其他版本的 SO 库。截止目前,v7版本的 SO 库可以满足市场上绝大多数的手机要求;
  • 使用 7zip 极限压缩,参考微信的 AndResGuard
  • 使用插件化,动态加载一些功能模块。

 

四、网络优化

常见的网络优化方案如下:

  • 加入网络数据的缓存,避免频繁请求网络;
  • 尽量减少网络请求,能够合并的尽量合并。因为 HTTP 底层也是 TCP 连接,对于每个 HTTP 请求,发出请求的时候都会创建 TCP 连接,请求结束后会断开 TCP 连接,那么当 HTTP 请求次数很多的时候就会频繁的创建和断开 TCP 连接,如果把当中一些请求进行合理的合并,那么就会减少 HTTP 请求次数;
  • 避免 DNS 解析,根据域名查询 IP 可能会耗费上百毫秒的时间。可以根据业务需求采用动态更新 IP 的方式,或者在IP方式访问失败时切换到域名访问方式;
  • 大量数据的加载采用分页的方式;
  • 网络数据传输采用 GZIP 压缩;
  • 使用 WebP 格式图片代替 PNG、JPEG 格式图片;
  • 根据网络状态返回不同分辨率的图片;
  • 使用 HTTP/2,压缩请求头部;
  • 使用断点续传,提升文件上传/下载速度。

 

五、耗电优化

在移动设备中,电池的重要性不言而喻。对于操作系统和设备开发商来说,追求更长的待机时间,耗电优化一致没有停止,而对于一款应用来说,也不能忽略电量使用问题。Android系统上应用的电量消耗主要由 CPU、GPS、WakeLock、数据传输、传感器的运行等组成,而耗电异常也是由于这几个模块的使用不当。针对与此,说一下几种优化方案,具体如下:

  • 合理的使用 wake_lock。wake_lock 主要是相对系统的休眠而言,程序给 CPU 加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题,所以微信里面是有大量使用到了 wake_lock 锁。合理的使用wake_lock应该在使用完成后及时的release;
  • 在不需要使用唤醒功能的情况下,尽量取消 AlarmManger,否则会一直处于耗电状态;
  • 使用 JobScheduler 集中处理一些任务。谷歌推荐使用JobScheduler,来调整任务优先级等策略来达到降低损耗的目的。JobScheduler可以避免频繁的唤醒硬件模块,造成不必要的电量消耗。避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量;
  • 低电量时降低更新频率或停止更新。通过系统广播,获取充电状态和电池电量的变化来调整数据更新等操作,如在充电时,更新数据及应用,在低电量时,减少更新频率或停止更新;
  • 减少无用的 GPS 请求和及时关闭 GPS 搜索。

 

六、其他优化

除了以上一些优化,日常开发中还有以下这些方面可以优化:

  • 避免在应用启动时做密集沉重的初始化;
  • 采用线程池,避免在程序中存在大量的线程。线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致阻塞现象发生;
  • 合理使用浮点类型。在 Android 设备中,浮点型大概比整型数据处理速度慢两倍,所以如果整型可以解决的问题就不要用浮点型;
  • 避免创建不必要的对象。每创建一个对象,系统就会在堆内存申请一块空间,大量申请内存会造成OOM。如果对于一个操作可以用一个对象完成尽量不要申请多余的对象;
  • 合理使用位运算替换乘除法。数据在计算机中都是2进制存储,这样2的整数倍的乘除法操作可以通过位运算移位进行处理,这种方式是速度最快的,效率最高;
  • 尽量使用局部变量。局部变量在应用上速度会比全局变量要快;
  • HashMap 和 ArrayMap,优先使用 ArrayMap;
  • 优先使用基本类型,而非包装类;
  • 减少占用内存较大的枚举的使用;
  • 图片压缩:加载Bitmap时,如果Bitmap过大,合理使用inSampleSize(采样率)、RGB_565替换RGB_8888;
  • Bitmap使用完以后,调用 bitmap.recycle() 来释放内存。

 

结语

Android 应用性能优化包含很多方面,每个方面都包含很多知识,本文只是浅尝辄止。性能优化需要走的路还很远,希望能和大家一同前行,一起进步,写出更加友好的应用。

 

参考文档

https://developer.android.google.cn/topic/performance

https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE

http://hukai.me/android-performance-patterns/

https://blog.csdn.net/lmj623565791/article/details/45556391

https://www.jianshu.com/p/43c9d827dc2a

https://www.jianshu.com/p/d71b51a0e29f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值