Android性能优化概览

前言

最近自己对安卓有一些感慨,有时候也会质疑自己,在这里抒发一下,有兴趣的可以看看,赶时间的就直接看正文啦。

对目前大部分Android开发人员来说,当然这里说的是各种琳琅满目的小公司的大部分Android开发人员们,(一说我就想起上半年春招的时候,我居然才知道有个东西叫面经,所以目前还没能去大公司拧螺丝or造核弹暂且是拧螺丝吧,也只是听别人说说,但是至少我目前是怀揣着一颗造核弹的心),大多数时候我们的工作可能都是写写布局,有点经验的弄个酷炫的自定义view(然后满满成就感),再然后就是写写业务逻辑,根据逻辑写一写数据的展示等,可以说这两件事已经涵盖了%90 上述范围的Android开发人员的工作,再加上现在技术的发展,很多布局上的难题(各种滑动view的嵌套,悬浮布局,侧拉,导航,引导页)已经有了比较成熟的解决方案,这也是必然趋势,再比如业务上的处理,各种成熟的框架已经席卷并深入整个Android开发的每个项目中(Glide、retrofit、okhttp、RxJava等),基本只要你熟练,再对框架有一定理解(MVP、MVVM等),再对业务的分块分层明确,你甚至可以完全一个人完成一个app的开发,也就是说APP的开发难度相比之前已经大幅降低了,话说回来,那这些人剩下的%10的工作在干什么呢,这个,咳咳,因人而异吧,努力的人就学习学习新知识(dart、flutter),满足于现状的就在刷抖音微博了,每每看到后者,我自己都会有一种….(想了很久,不知道应该填什么词了)的感觉,但是我相信你看到我这篇文章的时候,你肯定是一个十分爱学习的人,不甘于平凡,有一腔热血随时挥洒,对技术的热爱,对完美的追求,对自己所向往事物的执着,但是偶尔你可能也会和我一样,看看周围的人和事,有一些自己的体会和感想,对前路的否定,对技术的不可控,对google开发人员目前在开发的技术仿佛永远也追赶不上,被google开发牵着在走,作为技术人员中平凡的一粒,技术日新月异的速度,我总有一种赛跑的感觉,但是当我们年轻的时候,能否成为领跑者,这不正是我们期待和值得付出的吗!

目录

1.性能优化的入手点
2.可以使用的工具
3.琐碎的优化点

正文

其实本来想说说为什么要做性能优化的,但是这不是废话吗,/捂脸,就跳过啦。

Android性能优化的入手点

其实说起性能优化,本身就是一个很宽泛的概念,大到架构的设计优化,小到一个bitmap的及时回收。所以为了知道性能优化的方方面面,我们有必要知道总共可以通过哪些方式来优化我们APP的性能。

1.布局优化

布局优化其实也有很多种优化,咱们一个个来看。
如果你对这个不了解的话,先放几个Android提供的原生标签感受一下,includemergeViewStub ,怎么样,都认识几个,include应该都是很熟悉的,所以第一个优化布局的方式就是使用include 标签来实现布局的复用,这样我们就不用去每次都在各个不同的Activity或者Fragment 里写重复的布局,但是后面两个是来干嘛的呢,举个栗子,比如我们平常可能会写一个标题布局,然后用include 标签将它包括在其它的布局中,这时候你的include标签包裹的内容假设如下

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="60dp">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="返回"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:layout_centerInParent="true"
        android:text="标题"/>
</RelativeLayout>

然后我们使用时

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_alignParentTop="true"
        android:layout_height="wrap_content">
        <include
            layout="@layout/layout_title"/>
    </RelativeLayout>

其实include实现的效果就是将包裹的布局给替换过来,如果我们将上述第一个布局中的内容替换到下面布局的话,是不是发现多了一个RelativeLayout 的层级,但是这个层级好像又没有办法去除,因为如果采用这种include的方式,必须用一个基础的布局来包裹他,否则在复杂的场景中,没法确定它的位置。这个时候,Merge就来了,怎么使用呢,很简单,我们只需要将title布局的根标签换为Merge 即可,这样最后加载的时候,merge这一层就相于会被忽略掉,也就是减少一层布局的层级,带来绘制上的优化。

接下来咱们再看ViewStub,相信看到Stub这个后缀应该都能猜到,这个是用来懒加载的,怎么使用呢?非常简单,就拿上面的布局来说,我们可以将主布局中的include 标签替换为ViewStub,如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ViewStub
        android:id="@+id/viewstub"
        android:layout="@layout/layout_title"   //注意这里有前缀android:
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />

</RelativeLayout>

这时我们再运行程序,发现标题栏并没有显示出来,然后我们在代码中再去加载它

    private ViewStub viewStub;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewStub=findViewById(R.id.viewstub);
        viewStub.inflate();
    }

再次运行,我们发现标题栏显示了,说明什么问题,说明我们初次运行程序,这个布局是不会被加载的,然后我们可以再需要的时候,去控制它来动态加载,这样的好处就是可以提升界面的初次加载速度。但是它也有一些局限性,比如包含的资源不能用merge 标签,同时这个加载过后就不能再次控制它隐藏了,也就是说这个inflate() 操作是一次性的,无法实现显示->隐藏->显示 的效果。

说完了这两个标签,咱们再说一些布局逻辑上的优化,这个就比较抽象了,首先我们对比一下常用布局的性能:
RelativeLayout默认是测量两次,这个可以在RelativeLayoutonMeasure 源码中看到,那为什么要测量两次呢?因为在RelativeLayout 中是没有一个明确的垂直排列和水平排列的概念的,它的子view必须要先经过水平方向的测量,然后再经过垂直方向上的测量,以此来达到“相对布局”的效果,但是LinearLayout 就不同了,因为你可以为它指定明确的方向,到底是水平还是垂直,所以在LinearLayout中,你可以看到它的onmeasure 方法中根据水平和垂直作了不同的测量处理,所以只会测量一次,但是有一个要特别注意的是,如果为LinearLayout 的子view设置了weight 属性时,那么对于这部分子view也还是要测量两次的,为什么呢?因为每个子view要根据weight 值来分配剩余空间时,所以第一遍测量需要计算出剩余空间,也就是测量没有设置weight 属性的子view,然后第二遍测量再根据weight 属性的值分配余下空间。

ok,我们掌握上面的知识后,然后可以比较直观的看到,LinearLayout 的性能是比RelativeLayout 要好的,但是设置weight 属性会导致测量两次,所以我们要尽量避免设置weight 属性,还有比较新的布局ConstraintLayout 它的onmeasure 也是对子view要测量两次,但是它的测量耗时要比RelativeLayout 要低,因为测量两次,所以比LinearLayout 要耗时,但是它的约束布局的方式能最大化程度的较少布局的层级,即便是对于一个复杂的布局,可能一次布局嵌套都不用,直接一个ConstraintLayout 做根视图就可以完成。

那说了这么多,我们在实际使用中如何去合理运用这些布局来写出性能最优的布局呢?我将如何选择布局来提升性能总结了两个原则:

1.最优选让布局层级最少的布局
2.在不改变布局层级的情况下,这三者中选择LinearLayout 最优

这里对第二点稍作解释,比如我有一个布局里只有一个控件,所以这个时候,不管是用RelativeLayout还是LinearLayout都可以达到效果,但是明显应该选择LinearLayout,因为二者布局层级一样!

布局这里其实还有很多零碎的细节可以优化,比如,避免不必要的background 属性设置,能用drawable或者shape等xml代码实现的最好就不要选择使用图片等等。

最后,咱们再来总结一下布局这块,可以优化的点

1、使用include 标签来实现布局复用,同时使用merge 标签减少include 使用时多出来的一层布局
2、使用ViewStub 标签来懒加载布局,提高界面初次加载的速度
3、合理的运用布局来最大化减少布局层级,在不改变布局层级的情况下,选择LinearLayout 最优
4、不要给控件设置不必要的background属性,合理使用图片资源,能用xml代码实现的就不要用图片

2.容器的选择

大家都知道在Java中,最常用的容器就是HashMap了,所以我们大部分开发者在平常的开发中,需要用到容器时,可能想都不会想,需要用到键值对形式的存储时,直接敲出HashMap,仿佛这是一个默认的标配一样,其实不然,细节决定成败,我们看看这样一些类,SparseArray , SparseLongArrayArrayMapArraySet,怎么样,都认识吗,首先简单介绍一下它们,SparseArrayArrayMap是安卓官方为安卓量身打造的存储容器,专门用于安卓这种数据量不大,但是操作频繁的场景,那这二者的区别又是什么呢,该怎么使用呢?

这二者的使用方法非常简单,可以说几乎与HashMap无异,但是SparseArray 可以做到避免装箱和拆箱,因为它的键只能为int,如果觉得int类型的范围不够,还有LongSparseArray,他和SparseArray 唯一的区别就是,它的键为 long 类型,同时,SparseArrayvalueObject 类型,然后针对value的不同, 还有SparseBooleanArray,表示value 值只能为boolean 类型,SparseIntArray,表示value 值只能为int 类型,SparseLongArray,表示value 值只能为long 类型,如下

SparseArray          <int, Object>
LongSparseArray      <long, Object>
SparseBooleanArray   <int, boolean>
SparseIntArray       <int, int>
SparseLongArray      <int, long>

HashMap 相比,HashMap 的键值对采用的都是泛型,所以就不可避免的需要自动拆箱和装箱,而装箱和拆箱是非常耗时的,但是因为设计原理的不同,SparseArray 查找是二分查找,效率自然没有HashMap高,但是在数据量很少的情况下,这个效率带来的损耗相对装箱拆箱带来的损耗是可以忽略不计的,所以在安卓这种少数据量的情况下,如果允许int类型作键,那么就毫不犹豫的选择SparseArray 吧.

这时候你可能会说,那要是键不能为int 类型呢,没关系,咱们还有ArrayMap ,ArrayMap也是安卓官方为安卓量身打造的容器,它和HashMap 一样,键值对是采用的泛型,但是相对HashMap 来说,在数据量很少的情况下,它在设计时采用的空间缓存机制使得内存利用率极高,同时它支持使用索引来迭代,而HashMap 只能使用迭代器来迭代,迭代效率要比HashMap 高很多。

但是上述说的两种情况都是在数据量很少的情况下SparseArrayArrayMap 所具有的优点,但是安卓中大部分场景都是数据量很少的情况,所以我们在符合要求的情况下要尽量根据具体情况选用这两个容器来提高性能,而不是笼统的一概选择HashMap

然后我们也总结一下容器选择这块

1.允许使用int 作为键时,选用SparseArray 可以避免装箱拆箱,性能最佳
2.不允许使用int 作为键时,选用ArrayMap 提高内存利用率,提高迭代效率
3.数据量很大时(大于1000),还是使用HashMap较优

有关SparseArrayArrayMap 的具体实现原理可以看我的这篇文章:
Android集合框架之SparseArray、ArrayMap超级详解

3.处理内存泄露

说到内存泄露,很难去以一个完整具体的方法去完全解决,只能是通过编码习惯和在常见的内存泄露的场景多加留心以避免,内存优化中,最老生常谈的一个问题就是内存泄露,我们首先来看看内存泄露。

什么叫内存泄露?当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中的现象,就叫做内存泄漏

其实内存泄露也是一个很大的概念,要举出具体的例子也有很多,但是如果不掌握分析内存泄露的方法,以及不知道为什么会内存泄露,就很难在实际项目中去避免它, 或者简单点,也就是需要掌握大部分场景中内存泄露的原理,下面举一个具体的实例来说明
我们在之前使用Handler 传递消息时,可能会这样写:

public class MainActivity extends AppCompatActivity {

    private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.d("Main",(int)msg.obj+"");
        }
    };

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

    public void send(View view){
        Message message=Message.obtain();
        message.obj=1;
        handler.sendMessage(message);
    }
}

ok,这样写在使用上没有任何问题,编译也能通过,也能运行,也能正确接收到消息得到结果。
但是细心一点的话,你会发现AndroidStudio非常智能友好的提示警告,具体警告信息为:handler应该是静态的,否则可能会发生内存泄露。什么意思呢?我们现在来分析一下,为什么会发生内存泄露,首先在Java中,非静态的内部类和非静态的匿名内部类都会隐式的持有一份对外部类的引用,而静态的内部类则不会包含对外部类的引用,这里我们声明的handler 对象其实就是一个匿名内部类声明的对象,所以handler 此时会持有外部类的引用

handler 持有外部类引用说明什么呢,因为我们这里是在主线程使用的,主线程中每一个Message 对象都会由LooperMessageQueue取出,最后传递给handler,然后执行handleMessage 方法。好了,现在我们假设当前Activity需要被回收,但是handler 却还有message没有处理完(可能为延迟发送的message导致,或者message太多等等原因),所以这个handler 对象不会被回收,而handler 持有当前Activity 的引用,导致Activity 也没法被正常回收,此时,就发生Activity泄露啦!

ok,我们知道了泄露的原因,就应该学会根据原因找到解决办法,我们这里导致泄露的原因就是:hander 持有了外部类对象,也就是Activity 的引用,那么我们的解决思路就是只需要消除这个引用就行,但是这在Java规范中是不允许的,但是有一个解决方案是可以达到和消除他们之间的引用有异曲同工之妙的效果的,那就是让handler持有外部类的弱引用

java的引用一共分为四种,强引用、弱引用、软引用、虚引用,不懂的童鞋可以百度啦

handler持有的外部类引用为弱引用时,在上述情况中,当Activity再次回收时,因为handler 持有的是弱引用,所以此时并不会阻碍Activity被回收。
ok,我们现在找到了解决办法,来看看是如何用代码解决的吧

public class MainActivity extends AppCompatActivity {

    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler=new MyHandler(this);
    }

    public void send(View view){
        Message message=Message.obtain();
        message.obj=1;
        handler.sendMessage(message);
    }

    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        MyHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }
        @Override
        public void handleMessage(Message msg)
        {
            MainActivity activity = mActivity.get();
            if (activity != null) {
                Log.d("Main",(int)msg.obj+"");
            }
        }
    }
}

核心就在于静态内部类中将MainActivity的对象设置为了WeakReference,同时设置MyHandler 为静态内部类。

OK, 我们通过上面这个比较典型的例子应该能直观感受到内存泄露的查找以及解决的整个过程,当然在实际开发中,这种潜在的内存泄露就需要我们用一双慧眼去识别了,当然为了更好的处理这样一个复杂的问题,我们可以对一些常见的容易泄露的场景做一些总结,这样当我们遇到这些场景的时候,就要提醒自己格外小心了。目前我知道的主要如下:

  • 非静态内部类和非静态的匿名内部类会持有外部类的引用
  • 使用Handler(其实就是上面的非静态的匿名内部类持有外部类引用的情况)
  • bitmap及时回收
  • 自定义view获取自定义属性后,TypedArray 及时回收
  • 静态view(比如private static Button btn)会持有Activity的引用
  • 广播等注册之后没有取消注册,当然还有各种Manager,一般有register方法的,都会有unregister方法,所以要记得在合适的时机调用
  • WebView也容易泄露(这个接触的比较少,不过作为提醒,还是记录下来)

当然了,如果要完全罗列出来所有的情况是不可能的,而且也没有这个必要,我们只要学会在遇到内存泄露了怎么分析,怎么根据原因解决问题,那就基本ok了,最后再提醒一下,泄露中最容易发生的就是Activity和Bitmap的泄露。

4.缓存优化处理

缓存优化处理其实在Android中已经非常常见了,你可能不知道,但是你一定悄无声息的使用过,比如最简单的使用Glide 加载图片时,就使用到了缓存处理,但是这是别人已经封装好了,但是这毕竟是别人帮咱们做的,那要是下次遇到需要缓存其它对象的时候,你真的知道该怎么做吗

首先,我们都知道Android的内存是非常有限的,所以这就导致一个问题,没有足够的空间让我们缓存,缓存的时候必须要有取舍,所以这就涉及到一个策略问题,Android其实已经给我们提供了一个策略,这个策略其实就是计算机系统中的一个策略,叫最近最少使用算法,什么意思呢?就是当缓存容量达到阈值的时候,必须要去除掉已缓存的某一个对象,然后加入新的需要缓存的对象,而选取的原则就是该对象是在最近的某段时间内,是访问最少的,其实概念有点难懂,说白话一点就是要从缓存中删除的对象是我目前最不需要的对象,而判断标准就是我最后访问它的时间距离现在的时间最长。

知道了最近最少使用算法,我们在缓存的时候就可以使用这个策略,那么如何实现呢?当然我们知道该策略的原理之后,完全可以自己手动实现一个,但是Android早已考虑到了这一点,提供了原生的类LruCache 供我们使用,我们只需要简单的put、get即可实现采取最近最少使用策略的一个缓存,基本用法和hashmap很像,LruCache的键值对采用泛型,构造方法中的参数代表缓存的最大数量

LruCache<String,Bitmap> lruCache=new LruCache<>(5);
lruCache.put("1",bitmap);
Bitmap bitmap=lruCache.get("1");

当然我们在使用的时候,一定要记得在合适的地方去释放缓存,这些小细节一定要注意,而且多线程的同步问题,也要记得去控制。

当然了,这里的LruCache 是用于内存的,那么用于本地缓存的有没有现成的可以使用的呢?当然有,就是DiskLruCache 这个是JakeWharton大神的一个开源库,比较久了,但是使用起来还是OK的,具体使用我就不赘述啦,放上地址:DiskLruCache

掌握了缓存的策略之后,我们还需要知道缓存应该缓存在哪个地方,这时候就冒出来了另一个东东——三级缓存

什么是三级缓存?很简单,取对象的时候,首先从内存中取,若内存中没有,再从本地取,若本地还是没有,才从网络上取。我们很容易理解,这样做的效果其实就相当于最大化程度实现对象的快速获取,因为这三者在执行时需要的时间是:从内存取 < 从本地文件取 < 请求网络取。对象的获取速度快了,那么界面展现的时间就很快,自然带给用户的体验就非常流畅。

三级缓存的常用代码如下(bitmap举例):

Bitmap bitmap = null;
//首先从内存取
bitmap = mLruCacheUtils.getBitmapFromMemory(url);
if(bitmap != null)
{
    imageview.setImageBitmap(bitmap);
    return;
}
//再从本地取
bitmap = mLocalCacheUtils.getBitmapFromLocal(url);
if(bitmap != null)
{
    imageview.setImageBitmap(bitmap);
    mMemoryCacheUtils.setBitmapToMemory(url, bitmap);   //记得将图片保存在内存
    return;
}
//最后才走网络
bitmap = mNetCacheUtils.getBitmapFromNet(url);
imageview.setImageBitmap(bitmap);
mMemoryCacheUtils.setBitmapToMemory(url, bitmap);//保存在内存
mLocalCacheUtils.setBitmapToLocal(url, bitmap);//保存在内存

基本在缓存这里,采用三级缓存+Lru,是我目前知道比较常用的方式,当然为了达到理想的性能,仅仅使用缓存是不够的,还是会存在诸多不足,所以实际使用既要处理好缓存,同时还要处理好其他的细节,比如合理使用三方库,及时释放,压缩处理,延迟删除,占位设置(针对图片),异步加载,懒加载等等,妈耶,一口气说了这么多,让我喝口水先。

5.启动页优化

首先我们扩展一下视野,启动这里有一些专有名词挺有意思的,它们是,冷启动、热启动。
什么是冷启动?当应用启动时,后台没有该应用的进程。
什么是热启动?当应用启动时,后台有该应用的进程。

我们关心和主要优化的也就是冷启动,那在冷启动的时候,安卓系统发生了什么呢?
首先从Zygote进程中fork出一个新的进程给应用,然后创建初始化Application,然后创建初始化MainActivity,然后是熟悉的oncreate -> onStart -> onresume 然后再measure -> layout -> draw 到这里为止,我们就看到了我们的画面。

那再回到启动页优化,最容易碰到的问题就是经典的白屏黑屏问题,具体的原因主要就是我们在applicaton 创建和初始化中做了过多的耗时操作,一般都是各种三方框架的初始化,所以我们在这里可以优化一下用户的体验,具体的解决方案其实准确来说是不算优化的,只能说是一个视觉上的优化,将这个白屏的界面设置为透明或者我们自己想要展现给用户的图片等等,达到“欺骗”用户视觉的目的。

具体的操作方法为在styles文件中重新声明一个继承系统主题的style,然后我们在这里设置透明或者我们要展示给用户的图片,然后将启动页Activitytheme 属性设置为我们声明的style, 代码如下
如果想设置为透明的话,代码如下

    //启动页的theme
    <style name="WelcomeTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowNoTitle">true</item>
    </style>

如果想指定具体的图片的话,就这样设置

    //启动页的theme
    <style name="WelcomeTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@mipmap/welcome_bg</item>
        <item name="android:windowNoTitle">true</item>
    </style>

然后在AndroidManifest文件中给相应的Activity 设置上去

<activity android:name=".WelcomeActivity"
    android:theme="@style/WelcomeTheme">
......

ok,除了这个之外,我们要想真正解决启动慢的问题,还是要靠我们优化代码逻辑,比如我们在Applciation 中要尽量少的进行耗时操作,越“精简”越好,但是我们可以WelcomeActivity 中来做一些数据的初始化等耗时操作,因为我们的WelcomeActivity 不管什么情况下都是要经过三秒或者两秒才进入到主界面,所以把这个时间利用起来岂不是美滋滋。

然后还有另外一种场景需要我们注意,假设用户不小心误点了我们的APP。正常情况下,用户可能并没有耐心等到三秒之后进入主界面,而是果断按back键直接在WelcomeActivity 退出,如果这里没有做处理的话,用户过了几秒钟会在浏览其它应用的时候,突然跳转到我们的应用,显然这是极不友好的,那么为什么会有这样的现象呢,

我这里就放一下我当时的代码

public class WelcomeActivity extends AppCompatActivity{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(WelcomeActivity.this,MainActivity.class));
            }
        },3000);
    }
}

咋一看,确实没啥问题,功能也都实现了,现在我们结合问题的现象来分析,用户点击back键退出Activity 的时候,我们此时的handler 还没有完成它的使命,因为消息延迟3秒才处理,所以三秒后,即便按back键退出了WelcomeActivity ,此时并没有销毁activity,只是处于onstop 状态,所以仍然需要执行意图跳转到主界面的activity,那我们知道原因了,如何解决呢

如下,我们只需要在onstop方法中移除handler的runnable回调即可

    @Override
    protected void onStop() {
        super.onStop();
        handler.removeCallbacks(runnable);
    }

或者换一种简单暴力的思维,直接禁止返回按键,重写onBackPressed 方法也可以解决。

可以使用的工具

这里主要讲一下我所知道的可以通过哪些方式或者工具来检测问题。

首先我们来了解一下有一个东东叫严苛模式,我们可以通过开启这个严苛模式来检查问题,它主要用来检测线程策略和VM策略,线程策略检测的内容包括网络请求、磁盘读写、以及其它在线程中执行的逻辑耗时操作,VM策略检测的内容包括Activity泄露、资源未关闭、各种对象泄露等等。

严苛模式在Android中对应的类就是StrickMode ,好了,我们现在以一个简单的例子来看一下它的使用

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()//检测Activity泄露
                .penaltyLog()//在Logcat中打印相关日志
                .build());
        MyThread thread=new MyThread();
        thread.start();

    }
    class MyThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

很明显这里存在一个MainActivity泄露,愿意就是非静态内部类MyThread定义的对象thread 持有MainActivity 的引用,导致MainActivity 无法释放。当然解决办法就是将MyThread 改为静态的即可。
现在我们运行上述代码,然后不断的旋转手机来频繁地创建和销毁Activity,然后,来看看StrictMode 到底会打印什么,我们通过日志观察,发现打印如下

08-05 22:32:28.928 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=2; limit=1  //instance表示MainActivity的实例个数,现在是2个,表示已经泄露了
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=2; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:30.540 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=3; limit=1//3个
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=3; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:32.093 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=4; limit=1
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=4; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:33.745 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=5; limit=1
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=5; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

我们可以很清晰地看到,随着我们不断的旋转屏幕以创建销毁MainActivity ,发现实例个数在一直增加,这也正好验证了我们上述的预测。

通过开启严苛模式,你还可以做很多其他的事,这里我就不一一举例了,只要使用得当,还是一个非常好用的辅助工具。

接下来我们再看看Android官方给我们设计的检测内存的亲儿子Android Profiler,我们可以在AndroidStudio的地步工具栏中找到它,我们现在再次运行上述程序代码,同样的旋转屏幕的操作,然后打开Android Profiler工具栏,我们依然可以在这里直观形象的看到内存情况,在右侧,我们可以看到MainActivity的实例个数,很明显泄露了
这里写图片描述
不得不说这个工具真的非常强大,而且图形界面非常的友好,想检测什么只需要在对应的操作栏点击对应的条目即可。具体的使用我就不细说啦,留给大家去慢慢发掘吧!

其实AndroidStudio还有另外一个好东西,那就是代码检测,lint工具,具体的位置如下图
这里写图片描述
点击之后会弹出提示让你选择检测的代码范围,可以是整个项目,也可以是一个Module,也可以是一个具体的Activity,功能也是非常的强大,具体检测的结果会分的非常详细,一目了然,点击就可以查看具体的不规范信息,如下
这里写图片描述
这个虽然不是专门很对内存的,但是它针对的是代码优化也可以在无形之中帮助我们改善代码的结构,去除一些隐患,间接的提高我们的程序性能!

介绍完了官方的一些工具,再介绍一个开源的神器—-LeakCanary,之所以说它是神器,其实一点也不为过,毕竟人家的强大功能在那里,问题的层级分析给你分析的明明白白,先放上官方地址LeakCanary,具体的配置非常简单,如下

public class MyApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {//1
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
    }
}

然后我们测试用例同样使用上面的例子,不断的旋转屏幕以销毁和创建MainActivity ,最终会在发生泄露的时候弹出一个提示,然后你会发现同时你的手机状态栏多了一些推送,点击进去,如下,可以查看到具体的泄露信息

通过这些信息,我们再来定位我们的错误,再找解决的办法。

其它琐碎的优化点

这个主要就是列举一下我所知道的其它各种优化小细节,当做参考

  • 1.对于service,如果业务逻辑允许,并且占用内存特别大的话,可以为service 新开一个进程,因为内存的分配是以进程为单位的,而一个app就是一个进程,一个进程内存有限,所以新开进程不仅让service的内存充足,也可以为app主进程省下内存空间。
  • 2.在自定义view中或者其它manager 类的单例实现时,不要笼统的使用Activitycontext对象,所以尽量使用Applcaitioncontext,为什么呢,因为一旦这个context超过了它本身的生命周期,就会导致泄露,典型的就是Activity泄露
  • 3.自定义view时,不要在onMeasureonLayoutonDraw里创建对象,尤其是onDraw,否则会发生内存抖动
  • 4.有意识的避免装箱和拆箱
  • 5.如果需要开启多个线程,使用线程池来完成
  • 6.正则表达式很耗性能
  • 7.浮点运算比整数运算慢,能用 int 解决的就不要再麻烦float
  • 8.图片的webp格式是一个你需要知道的格式
  • 9.如果可以,尽量将网络请求合并,减伤网络请求的次数
  • 10.同步时,减少锁个数、减小锁范围
  • 未完待续

小结

我们现在再来总结一下正文中的内容,首先是关于性能优化的入手点,如下

  • 布局优化
  • 容器的选择
  • 处理内存泄露
  • 缓存优化处理
  • 启动页优化

其实还有很多其他的方面,但是限于本人水平,就没有给大家深入讲解,然后是几个辅助工具的使用

  • StrickMode模式的开启和使用
  • Android Profiler的使用
  • Android Lint工具的使用
  • LeakCanary开源库的使用

如果真正深入分析的话,上述每一个小点都可以单独拆分出来写一篇文章,具体深入的话,是可以有很多东西的,本身性能优化就是一个庞大而杂的工作,需要耐心和技术,只有不断的打磨和沉淀才可以锻造出一个性能优越的App,好啦,就到这啦,要是有什么疑问的话,欢迎留言交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章