Android 性能优化最佳实践

本文由玉刚说写做平台提供写做赞助html

原做者:Mr.Sjava

版权声明:本文版权归微信公众号玉刚说全部,未经许可,不得以任何形式转载android

什么是性能git

这张图很好诠释了什么性能

快,稳,省,小,这四点很形象的表明了性能的四个方面,同时也让咱们知道咱们App如今是不是款性能良好的APP,若是有一项不达标,那么说明咱们的应用有待优化。程序员

不少时候咱们注重功能实现,保证能用,可是咱们会发现,这样的应用很难拿的出手,里面的槽点太多了,性能不好,可是又不知道从哪里下手进行优化,那么咱们就一步一步来,看看咱们到底应该怎么优化咱们的APP。github

1 、布局优化web

和UI相关的首先就是布局,特别是在开发一些复杂界面的时候,一般咱们都是采用布局嵌套的方法,每一个人的布局思路不太同样,写出的也不太同样,,因此就可能形成嵌套的层级过多。面试

官方 屏幕上的某个像素在同一帧的时间内被绘制了屡次。在多层次的UI结构里面,若是不可见的UI也在作绘制的操做,这就会致使某些像素区域被绘制了屡次。这就浪费大量的CPU以及GPU资源。算法

白话 显示一个布局就比如咱们盖一个房子,首先咱们要测量房子的大小,还要测量房间里面各个家具的大小,和位置,而后进行摆放同时也要对房子进行装修,若是咱们是一层,都在明面上,干起活来敞亮也轻松,但是有的人的房子,喜欢各类隔断,分红一个一个的大隔断间,每一个大隔断间里还有小隔断间,小隔断间里有小小隔断间,还有小小小隔断间。。。N层隔断间。数据库

看到这些头皮发麻吧,并且是一个大隔断间里面全部的小隔断,小小隔断等等都测量完摆放好,才能换另一个大隔断,天呢,太浪费时间了,不能都直接都放外面吗?也好摆放啊,这么搞我怎么摆,每一个隔断间都要装修一遍,太浪费时间了啊。

咱们的Android虚拟机也会这么抱怨,我们家原本就不富裕,什么都要省着用,你这么搞,确定运转有问题啊,那么多嵌套的小隔断间须要处理,都会占用cpu计算的时间和GPU渲染的时间。显示GPU过分绘制,分层以下如所示:

分层颜色.png
经过颜色咱们能够知道咱们应用是否有多余层次的绘制,若是一路飘红,那么咱们就要相应的处理了。

因此咱们有了第一个优化版本:

优化 1.0

  1. 若是父控件有颜色,也是本身须要的颜色,那么就没必要在子控件加背景颜色
  2. 若是每一个自控件的颜色不太同样,并且能够彻底覆盖父控件,那么就不须要再父控件上加背景颜色
  3. 尽可能减小没必要要的嵌套
  4. 能用LinearLayout和FrameLayout,就不要用RelativeLayout,由于RelativeLayout控件相对比较复杂,测绘也想要耗时。

作到了以上4点只能说恭喜你,入门级优化已经实现了。

针对嵌套布局,谷歌也是陆续出了一些新的方案。对就是includemergeViewStub三兄弟。

include能够提升布局的复用性,大大方便咱们的开发,有人说这个没有减小布局的嵌套吧,对,include确实没有,可是include和merge联手搭配,效果那是杠杠滴。

merge的布局取决于父控件是哪一个布局,使用merge至关于减小了自身的一层布局,直接采用父include的布局,固然直接在父布局里面使用意义不大,因此会和include配合使用,既增长了布局的复用性,用减小了一层布局嵌套。

ViewStub它能够按需加载,什么意思?用到他的时候喊他一下,再来加载,不须要的时候像空气同样,在一边静静的呆着,不吃你的米,也不花你家的钱。等须要的时候ViewStub中的布局才加载到内存,多节俭持家啊。对于一些进度条,提示信息等等八百年才用一次的功能,使用ViewStub是极其合适的。这就是不用不知道,一用戒不了。

咱们开始进化咱们的优化

优化 1.1

  1. 使用include和merge增长复用,减小层级
  2. ViewStub按需加载,更加轻便

可能又有人说了:背景复用了,嵌套已经很精简了,再精简就实现了不了复杂视图了,但是仍是一路飘红,这个怎么办?面对这个问题谷歌给了咱们一个新的布局ConstraintLayout

ConstraintLayout能够有效地解决布局嵌套过多的问题。ConstraintLayout使用约束的方式来指定各个控件的位置和关系的,它有点相似于 RelativeLayout,但远比RelativeLayout要更强大(照抄隔壁IOS的约束布局)。因此简单布局简单处理,复杂布局ConstraintLayout很好使,提高性能从布局作起。

再次进化:

优化 1.2

  1. 复杂界面可选择ConstraintLayout,可有效减小层级

二、绘制优化

咱们把布局优化了,可是和布局息息相关的还有绘制,这是直接影响显示的两个根本因素。

其实布局优化了对于性能提高影响不算很大,可是是咱们最容易下手,最直接接触的优化,因此无论能提高多少,哪怕只有百分之一的提高,咱们也要作,由于影响性能的地方太多了,每一个部分都提高一点,咱们应用就能够提高不少了。

咱们平时感受的卡顿问题最主要的缘由之一是由于渲染性能,由于愈来愈复杂的界面交互,其中可能添加了动画,或者图片等等。咱们但愿创造出愈来愈炫的交互界面,同时也但愿他能够流畅显示,可是每每卡顿就发生在这里。

这个是Android的渲染机制形成的,Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,可是渲染未必成功,若是成功了那么表明一切顺利,可是失败了可能就要延误时间,或者直接跳过去,给人视觉上的表现,就是要么卡了一会,要么跳帧。

View的绘制频率保证60fps是最佳的,这就要求每帧绘制时间不超过16ms(16ms = 1000/60),虽然程序很难保证16ms这个时间,可是尽可能下降onDraw方法中的复杂度老是切实有效的。

这个正常状况下,每隔16ms draw()一下,很整齐,很流畅,很完美。

image.png
每每会发生以下图的状况,有个便秘的家伙霸占着,一帧画面拉的时间那么长,这一下可不就卡顿了嘛。把后面的时间给占用了,后面只能延后,或者直接略过了。

image.png
既然问题找到了,那么咱们确定要有相应的解决办法,根本作法是 减轻onDraw()的负担。

因此

第一点: onDraw方法中不要作耗时的任务,也不作过多的循环操做,特别是嵌套循环,虽然每次循环耗时很小,可是大量的循环势必霸占CPU的时间片,从而形成View的绘制过程不流畅。

第二点: 除了循环以外,onDraw()中不要建立新的局部对象,由于onDraw()方法通常都会频繁大量调用,就意味着会产生大量的零时对象,不进占用过的内存,并且会致使系统更加频繁的GC,大大下降程序的执行速度和效率。

其实这两点在android的UI线程中都适用。

升级进化:

优化2.0

  1. onDraw中不要建立新的局部对象
  2. onDraw方法中不要作耗时的任务

其实从渲染优化里咱们也牵扯出了另外一个优化,那就是内存优化。

三、内存优化

内存泄漏指的是那些程序再也不使用的对象没法被GC识别,这样就致使这个对象一直留在内存当中,占用了没来就很少的内存空间。

内存泄漏是一个缓慢积累的过程,一点一点的给你,温水煮青蛙通常,咱们每每很难直观的看到,只能最后内存不够用了,程序奔溃了,才知道里面有大量的泄漏,可是究竟是那些地方?估计是狼烟遍地,千疮百孔,都不知道如何下手。怎么办?最让人难受的是内存泄漏状况那么多,记不住,理解也不容易,关键是老会忘记。怎么办呢?老这么下去也不是事,总不能面试的时候突击,作项目的时候不知所措吧。因此必定要记住了解GC原理,这样才能够更准确的理解内存泄漏的场景和缘由。不懂GC原理的能够先看一下这个JVM初探:内存分配、GC原理与垃圾收集器

原本GC的诞生是为了让java程序员更加轻松(这一点隔壁C++痛苦的一匹),java虚拟机会自动帮助咱们回收那些再也不须要的内存空间。经过引用计数法,可达性分析法等等方法,确认该对象是否没有引用,是否能够被回收。

有人会说真么强悍的功能看起来无懈可击啊,对,理论上能够达到消除内存泄漏,可是不少人不按常理出牌啊,每每不少时候,有的对象还保持着引用,但逻辑上已经不会再用到。就是这一类对象,游走于GC法律的边缘,我没用了,可是你又不知道我没用了,就是这么赖着不走,空耗内存。

由于有内存泄漏,因此内存被占用愈来愈多,那么GC会更容易被触发,GC会愈来愈频发,可是当GC的时候全部的线程都是暂停状态的,须要处理的对象数量越多耗时越长,因此这也会形成卡顿。

image.png

那么什么状况下会出现这样的对象呢? 基本能够分为如下四大类: 一、集合类泄漏 二、单例/静态变量形成的内存泄漏 三、匿名内部类/非静态内部类 四、资源未关闭形成的内存泄漏

一、集合类泄漏

集合类添加元素后,仍引用着集合元素对象,致使该集合中的元素对象没法被回收,从而致使内存泄露。

举个栗子:

static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
       obj = null;
    }
复制代码

当mList没用的时候,咱们若是不作处理的话,这就是典型的占着茅坑不拉屎,mList内部持有者众多集合元素的对象,不泄露天理难容啊。解决这个问题也超级简单。把mList清理掉,而后把它的引用也给释放掉。

mList.clear();
  mList = null;
复制代码

二、单例/静态变量形成的内存泄漏

单例模式具备其 静态特性,它的生命周期 等于应用程序的生命周期,正是由于这一点,每每很容易形成内存泄漏。 先来一个小栗子:

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

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

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

复制代码

当咱们在Activity里面使用这个的时候,把咱们Acitivty的context传进去,那么,这个单例就持有这个Activity的引用,当这个Activity没有用了,须要销毁的时候,由于这个单例还持有Activity的引用,因此没法GC回收,因此就出现了内存泄漏,也就是生命周期长的持有了生命周期短的引用,形成了内存泄漏。

因此咱们要作的就是生命周期长的和生命周期长的玩,短的和短的玩。就比如你去商场,原本就是传个话的,话说完就要走了,忽然保安过来非要拉着你的手,说要和你天长地久。只要商场在一天,他就要陪你一天。天呢?太可怕了。叔叔咱们不约,我有个人小伙伴,我还要上学呢,你赶忙找你的保洁阿姨去吧。你在商场的生命周期原本可能就是1分钟,而保安的生命周期那是要和商场开关门一致的,因此不一样生命周期的最好别一块儿玩的好。

解决方案也很简单:

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

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

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

复制代码

还有一个经常使用的地方就是Toast。你应该知道和谁玩了吧。

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

这里有一张宝图:

image.png
非静态内部类他会持有他外部类的引用,从图咱们能够看到非静态内部类的生命周期可能比外部类更长,这就是二楼的状况一致了,若是非静态内部类的周明周期长于外部类,在加上自动持有外部类的强引用,个人乖乖,想不泄漏都难啊。

咱们再来举个栗子:

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

复制代码

咱们常常会用这个方法去异步加载,而后更新数据。貌似很日常,咱们开始学这个的时候就是这么写的,没发现有问题啊,可是你这么想想,MyAscnyTask是一个非静态内部类,若是他处理数据的时间很长,极端点咱们用sleep 100秒,在这期间Activity可能早就关闭了,原本Activity的内存应该被回收的,可是咱们知道非静态内部类会持有外部类的引用,因此Activity也须要陪着非静态内部类MyAscnyTask一块儿天荒地老。好了,内存泄漏就造成了。

怎么办呢?

既然MyAscnyTask的生命周期可能比较长,那就把它变成静态,和Application玩去吧,这样MyAscnyTask就不会再持有外部类的引用了。二者也相互独立了。

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }
//改了这里 注意一下 static
   static  class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
复制代码

说完非静态内部类,我再来看看匿名内部类,这个问题很常见,匿名内部类和非静态内部类有一个共同的地方,就是会只有外部类的强引用,因此这哥俩本质是同样的。可是处理方法有些不同。可是思路绝对同样。换汤不换药。

举个灰常熟悉的栗子:

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
//do something
mText.setText(" do someThing");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
        //  匿名线程持有 Activity 的引用,进行耗时操做
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }

复制代码

想必这两个方法是咱们常常用的吧,很熟悉,也是这么学的,没感受不对啊,老师就是这么教的,经过咱们上面的分析,还这么想吗?关键是 耗时时间过长,形成内部类的生命周期大于外部类,对弈非静态内部类,咱们能够静态化,至于匿名内部类怎么办呢?同样把它变成静态内部类,也就是说尽可能不要用匿名内部类。完事了吗?不少人不注意这么一件事,若是咱们在handleMessage方法里进行UI的更新,这个Handler静态化了和Activity没啥关系了,可是好比这个mText,怎么说?全写是activity.mText,看到了吧,持有了Activity的引用,也就是说Handler费劲心思变成静态类,自认为不持有Activity的引用了,准确的说是不自动持有Activity的引用了,可是咱们要作UI更新的时候势必会持有Activity的引用,静态类持有非静态类的引用,咱们发现怎么又开始内存泄漏了呢?到处是坑啊,怎么办呢?咱们这里就要引出弱引用的概念了。

引用分为强引用,软引用,弱引用,虚引用,强度一次递减。

强引用 咱们平时不作特殊处理的通常都是强引用,若是一个对象具备强引用,GC宁肯OOM也毫不会回收它。看出多强硬了吧。

软引用(SoftReference) 若是内存空间足够,GC就不会回收它,若是内存空间不足了,就会回收这些对象的内存。

弱引用(WeakReference) 弱引用要比软引用,更弱一个级别,内存不够要回收他,GC的时候无论内存够不够也要回收他,简直是弱的一匹。不过GC是一个优先级很低的线程,也不是太频繁进行,因此弱引用的生活还过得去,没那么提心吊胆。

虚引用 用的甚少,我没有用过,若是想了解的朋友,能够自行谷歌百度。

因此咱们用弱引用来修饰Activity,这样GC的时候,该回收的也就回收了,不会再有内存泄漏了。很完美。

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

    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);
            weakReference.get().mText.setText("do someThing");

        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            super.run();

            try {
                sleep(100000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mText = findViewById(R.id.mText);
        myHandler.sendEmptyMessageDelayed(0, 100000);
        myThread.start();
    }
//最后清空这些回调 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }
复制代码

四、资源未关闭形成的内存泄漏

  • 网络、文件等流忘记关闭
  • 手动注册广播时,退出时忘记 unregisterReceiver()
  • Service 执行完后忘记 stopSelf()
  • EventBus 等观察者模式的框架忘记手动解除注册

这些须要记住又开就有关,具体作法也很简单就不一一赘述了。给你们介绍几个很好用的工具: 一、leakcanary傻瓜式操做,哪里有泄漏自动给你显示出来,很直接很暴力。 二、咱们平时也要多使用Memory Monitor进行内存监控,这个分析就有些难度了,能够上网搜一下具体怎么使用。 三、Android Lint 它能够帮助咱们发现代码机构 / 质量问题,同时提供一些解决方案,内存泄露的会飘黄,用起来很方便,具体使用方法上网学习,这里很少作说明了。

so

优化3.0

  1. 解决各个状况下的内存泄漏,注意平时代码的规范。

四、启动速度优化

不知道你们有没有细心发现,咱们的应用启动要比别的大厂的要慢,要花费更多的时间,明明他们的包体更大,app更复杂,怎么启动时间反而比咱们的短呢?

可是这块的优化关注的人不多,由于App经常伴有闪屏页,因此这个问题看起来就不是问题了,可是一款好的应用是绝对不容许这样的,我加闪屏页是个人事,启动速度慢绝对不能够。

app启动分为冷启动(Cold start)、热启动(Hot start)和温启动(Warm start)三种。

冷启动(Cold start)

冷启动是指应用程序从头开始:系统的进程在此开始以前没有建立应用程序。冷启动发生在诸如自设备启动以来首次启动应用程序或自系统终止应用程序以来。

在冷启动开始时,系统有三个任务。这些任务是: 一、加载并启动应用程序 二、启动后当即显示应用程序的空白启动窗口 三、建立应用程序进程

当系统为咱们建立了应用进程以后,开始建立应用程序对象。

一、启动主线程 二、建立主Activity 三、加载布局 四、屏幕布局 五、执行初始绘制

应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户能够开始使用该应用程序。至此启动完成。

image.png

Application建立

当Application启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。此时,系统进程会交换应用程序的启动窗口,容许用户开始与应用程序进行交互。这就是为何咱们的程序启动时会先出现一段时间的黑屏(白屏)。

若是咱们有本身的Application,系统会onCreate()在咱们的Application对象上调用该方法。以后,应用程序会生成主线程(也称为UI线程),并经过建立主要活动来执行任务。

从这一点开始,App就按照他的 应用程序生命周期阶段进行

Activity建立

应用程序进程建立活动后,活动将执行如下操做:

  1. 初始化值。
  2. 调用构造函数。
  3. 调用回调方法,例如 Activity.onCreate(),对应Activity的当前生命周期状态。

一般,该 onCreate()方法对加载时间的影响最大,由于它以最高的开销执行工做:加载和膨胀视图,以及初始化活动运行所需的对象。

热启动(Hot start)

应用程序的热启动比冷启动要简单得多,开销也更低。在一个热启动中,系统都会把你的Activity带到前台。若是应用程序的Activity仍然驻留在内存中,那么应用程序能够避免重复对象初始化、布局加载和渲染。

热启动显示与冷启动方案相同的屏幕行为:系统进程显示空白屏幕,直到应用程序完成呈现活动。

温启动(Warm start)

温启动包含了冷启动时发生的一些操做,与此同时,它表示的开销比热启动少,有许多潜在的状态能够被认为是温暖的开始。

场景:

  • 用户退出您的应用,但随后从新启动它。该过程可能已继续运行,但应用程序必须经过调用从头开始从新建立Activity 的onCreate()
  • 系统将您的应用程序从内存中逐出,而后用户从新启动它。须要从新启动进程和活动,可是在调用onCreate()的时候能够从Bundle(savedInstanceState)获取数据。

了解完启动过程,咱们就知道哪里会影响咱们启动的速度了。在建立应用程序和建立Activity期间均可能会出现性能问题。

这里是慢的定义:

  • 启动须要5秒或更长时间。
  • 启动须要2秒或更长时间。
  • 启动须要1.5秒或更长时间。

不管何种启动,咱们的优化点都是: Application、Activity建立以及回调等过程

谷歌官方给的建议是: 一、利用提早展现出来的Window,快速展现出来一个界面,给用户快速反馈的体验; 二、避免在启动时作密集沉重的初始化(Heavy app initialization); 三、避免I/O操做、反序列化、网络操做、布局嵌套等。

具体作法:

针对1:利用提早展现出来的Window,快速展现出来一个界面

使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable。

Layout XML file:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>
复制代码

Manifest file:

<activity ...
android:theme="@style/AppTheme.Launcher" />
复制代码

这样在启动的时候,会先展现一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,咱们将主题从新设置为正常的主题,从而产生一种快的感受。其实就是个障眼法而已,提早让你看到了假的界面。也算是一种不错的方法,可是治标不治本。

针对2:避免在启动时作密集沉重的初始化

咱们审视一下咱们的MyApplication里面的操做。初始化操做有友盟,百度,bugly,数据库,IM,神策,图片加载库,网络请求库,广告sdk,地图,推送,等等,这么多须要初始化,Application的任务过重了,启动不慢才怪呢。

怎么办呢?这些还都是必要的,不能不去初始化啊,那就只能异步加载了。可是并非全部的均可以进行异步处理。这里分状况给出一些建议: 一、好比像友盟,bugly这样的业务非必要的能够的异步加载。 二、好比地图,推送等,非第一时间须要的能够在主线程作延时启动。当程序已经启动起来以后,在进行初始化。 三、对于图片,网络请求框架必须在主线程里初始化了。

同时由于咱们通常会有闪屏页面,也能够把延时启动的地图,推进的启动在这个时间段里,这样合理安排时间片的使用。极大的提升了启动速度。

针对3:避免I/O操做、反序列化、网络操做、布局嵌套等。

这个不用多说了,你们应该知道如何去作了,有些上文也有说明。

so

优化4.0

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

五、包体优化

我作过两年的海外应用产品,深知包体大小对于产品新增的影响,包体小百分之五,可能新增就增长百分之五。若是产品基数很大,这个提高就更可怕了。无论怎么说,咱们要减肥,要六块腹肌,不要九九归一的大肚子。

既然要瘦身,那么咱们必须知道APK的文件构成,解压apk:

image.png

assets文件夹 存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是经过 AssetManager 类的接口获取。

res目录 res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。

META-INF 保存应用的签名信息,签名信息能够验证 APK 文件的完整性。

AndroidManifest.xml 这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可以使用权限等。

classes.dex Dalvik 字节码程序,让 Dalvik 虚拟机可执行,通常状况下,Android 应用在打包时经过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。

resources.arsc 记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。

咱们须要从代码和资源两个方面去减小响应的大小。

一、首先咱们可使用lint工具,若是有没有使用过的资源就会打印以下的信息(不会使用的朋友能够上网看一下)

res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
    to be unused [UnusedResources]
复制代码

同时咱们能够开启资源压缩,自动删除无用的资源

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

无用的资源已经被删除了,接下来哪里能够在瘦身呢?

二、咱们可使用可绘制对象,某些图像不须要静态图像资源; 框架能够在运行时动态绘制图像。Drawable对象(<shape>以XML格式)能够占用APK中的少许空间。此外,XML Drawable对象产生符合材料设计准则的单色图像。

上面的话官方,简单说来就是,能本身用XML写Drawable,就本身写,能不用公司的UI切图,就别和他们说话,我们本身造,作本身的UI,美滋滋。并且这种图片占用空间会很小。

三、重用资源,好比一个三角按钮,点击前三角朝上表明收起的意思,点击后三角朝下,表明展开,通常状况下,咱们会用两张图来切换,咱们彻底能够用旋转的形式去改变

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_thumb_up"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fromDegrees="180" />
复制代码

好比同一图像的着色不一样,咱们能够用android:tint和tintMode属性,低版本(5.0如下)可使用ColorFilter

四、压缩PNG和JPEG文件 您能够减小PNG文件的大小,而不会丢失使用工具如图像质量 pngcrushpngquant,或zopflipng。全部这些工具均可以减小PNG文件的大小,同时保持感知的图像质量。

五、使用WebP文件格式 可使用图像的WebP文件格式,而不是使用PNG或JPEG文件。WebP格式提供有损压缩(如JPEG)以及透明度(如PNG),但能够提供比JPEG或PNG更好的压缩。

可使用Android Studio将现有的BMP,JPG,PNG或静态GIF图像转换为WebP格式。

六、使用矢量图形 可使用矢量图形来建立与分辨率无关的图标和其余可伸缩Image。使用这些图形能够大大减小APK大小。一个100字节的文件能够生成与屏幕大小相关的清晰图像。

可是,系统渲染每一个VectorDrawable对象须要花费大量时间 ,而较大的图像须要更长的时间才能显示在屏幕上。所以,请考虑仅在显示小图像时使用这些矢量图形。

不要把AnimationDrawable用于建立逐帧动画,由于这样作须要为动画的每一个帧包含一个单独的位图文件,这会大大增长APK的大小。

七、代码混淆 使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。这个你们太熟悉了。很少说了。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile(‘proguard-android.txt'), 'proguard-rules.pro' } } 复制代码

八、插件化。 好比功能模块放在服务器上,按需下载,能够减小安装包大小。

so

优化5.0

  1. 代码混淆
  2. 插件化
  3. 资源优化

六、耗电优化

咱们可能对耗电优化不怎么感冒,没事,谷歌这方面作得也不咋地,5.0以后才有像样的方案,讲实话这个优化的优先级没有前面几个那么高,可是咱们也要了解一些避免耗电的坑,至于更细的耗电分析可使用这个Battery Historian

Battery Historian 是由Google提供的Android系统电量分析工具,从手机中导出bugreport文件上传至页面,在网页中生成详细的图表数据来展现手机上各模块电量消耗过程,最后经过App数据的分析制定出相关的电量优化的方法。

咱们来谈一下怎么规避电老虎吧。

谷歌推荐使用JobScheduler,来调整任务优先级等策略来达到下降损耗的目的。JobScheduler能够避免频繁的唤醒硬件模块,形成没必要要的电量消耗。避免在不合适的时间(例如低电量状况下、弱网络或者移动网络状况下的)执行过多的任务消耗电量。

具体功能: 一、能够推迟的非面向用户的任务(如按期数据库数据更新); 二、当充电时才但愿执行的工做(如备份数据); 三、须要访问网络或 Wi-Fi 链接的任务(如向服务器拉取配置数据); 四、零散任务合并到一个批次去按期运行; 五、当设备空闲时启动某些任务; 六、只有当条件获得知足, 系统才会启动计划中的任务(充电、WIFI...);

同时谷歌针对耗电优化也提出了一个懒惰第一的法则:

减小 你的应用程序能够删除冗余操做吗?例如,它是否能够缓存下载的数据而不是重复唤醒无线电以从新下载数据?

推迟 应用是否须要当即执行操做?例如,它能够等到设备充电才能将数据备份到云端吗?

合并 能够批处理工做,而不是屡次将设备置于活动状态吗?例如,几十个应用程序是否真的有必要在不一样时间打开收音机发送邮件?在一次唤醒收音机期间,是否能够传输消息?

谷歌在耗电优化这方面确实显得有些无力,但愿之后能够退出更好的工具和解决方案,否则这方面的优化优先级仍是很低。付出和回报所差太大。

so

优化6.0

  1. 使用JobScheduler调度任务
  2. 使用懒惰法则

六、ListView和 Bitmap优化

针对ListView优化,主要是合理使用ViewHolder。建立一个内部类ViewHolder,里面的成员变量和view中所包含的组件个数、类型相同,在convertview为null的时候,把findviewbyId找到的控件赋给ViewHolder中对应的变量,就至关于先把它们装进一个容器,下次要用的时候,直接从容器中获取。

如今咱们如今通常使用RecyclerView,自带这个优化,不过仍是要理解一下原理的好。 而后能够对接受来的数据进行分段或者分页加载,也能够优化性能。

对于Bitmap,这个咱们使用的就比较多了,很容易出现OOM的问题,图片内存的问题能够看一下我以前写的这篇文章一张图片占用多少内存

Bitmap的优化套路很简单,粗暴,就是让压缩。 三种压缩方式: 1.对图片质量进行压缩 2.对图片尺寸进行压缩 3.使用libjpeg.so库进行压缩

对图片质量进行压缩

public static Bitmap compressImage(Bitmap bitmap){  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中  
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
            int options = 100;  
            //循环判断若是压缩后图片是否大于50kb,大于继续压缩  
            while ( baos.toByteArray().length / 1024>50) {  
                //清空baos  
                baos.reset();  
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);  
                options -= 10;//每次都减小10  
            }  
            //把压缩后的数据baos存放到ByteArrayInputStream中  
            ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
            //把ByteArrayInputStream数据生成图片  
            Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);  
            return newBitmap;  
        }  
复制代码

对图片尺寸进行压缩

/**
     * 按图片尺寸压缩 参数是bitmap
     * @param bitmap
     * @param pixelW
     * @param pixelH
     * @return
     */
    public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
        if( os.toByteArray().length / 1024>512) {//判断若是图片大于0.5M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
            os.reset();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中
        }
        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        BitmapFactory.decodeStream(is, null, options);
        options.inJustDecodeBounds = false;
        options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH );
        is = new ByteArrayInputStream(os.toByteArray());
        Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options);
        return newBitmap;
    }


    /**
     * 动态计算出图片的inSampleSize
     * @param options
     * @param minSideLength
     * @param maxNumOfPixels
     * @return
     */
    public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
        int roundedSize;
        if (initialSize <= 8) {
            roundedSize = 1;
            while (roundedSize < initialSize) {
                roundedSize <<= 1;
            }
        } else {
            roundedSize = (initialSize + 7) / 8 * 8;
        }
        return roundedSize;
    }

    private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        double w = options.outWidth;
        double h = options.outHeight;
        int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
        int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
        if (upperBound < lowerBound) {
            return lowerBound;
        }
        if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
            return 1;
        } else if (minSideLength == -1) {
            return lowerBound;
        } else {
            return upperBound;
        }
    }
复制代码

使用libjpeg.so库进行压缩 能够参考这篇Android性能优化系列之Bitmap图片优化: https://blog.csdn.net/u012124438/article/details/66087785)

优化7.0

  1. ListView使用ViewHolder,分段,分页加载
  2. 压缩Bitmap

八、响应速度优化

影响响应速度的主要因素是主线程有耗时操做,影响了响应速度。因此响应速度优化的核心思想是避免在主线程中作耗时操做,把耗时操做异步处理。

九、线程优化

线程优化的思想是采用线程池,避免在程序中存在大量的Thread。线程池能够重用内部的线程,从而避免了现场的建立和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而致使阻塞现象发生。

《Android开发艺术探索》对线程池的讲解很详细,不熟悉线程池的能够去了解一下。

  • 优势: 一、减小在建立和销毁线程上所花的时间以及系统资源的开销。 二、如不使用线程池,有可能形成系统建立大量线程而致使消耗完系统内存以及”过分切换”。

  • 须要注意的是: 一、若是线程池中的数量为达到核心线程的数量,则直接会启动一个核心线程来执行任务。      二、若是线程池中的数量已经达到或超过核心线程的数量,则任务会被插入到任务队列中标等待执行。      三、若是(2)中的任务没法插入到任务队列中,因为任务队列已满,这时候若是线程数量未达到线程池规定最大值,则会启动一个非核心线程来执行任务。     四、若是(3)中线程数量已经达到线程池最大值,则会拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法通知调用者。

十、微优化

这些微优化能够在组合时提升总体应用程序性能,但这些更改不太可能致使显着的性能影响。选择正确的算法和数据结构应始终是咱们的首要任务,以提升代码效率。

  • 编写高效代码有两个基本规则: 一、不要作你不须要作的工做 二、若是能够避免,请不要分配内存

一、避免建立没必要要的对象 对象建立永远不是免费的,虽然每个的代价不是很大,可是总归是代价的不是吗?能不建立何须要浪费资源呢?

二、首选静态(这里说的是特定情景) 若是您不须要访问对象的字段,请使您的方法保持静态。调用速度将提升约15%-20%。这也是很好的作法,由于你能够从方法签名中看出,调用方法不能改变对象的状态

三、对常量使用static final 此优化仅适用于基本类型和String常量,而不适用于 任意引用类型。尽管如此,static final尽量声明常量是一种好习惯。

四、使用加强的for循环语法 加强for循环(for-each)可用于实现Iterable接口和数组的集合。对于集合,分配一个迭代器来对hasNext()和进行接口调用next()。使用一个 ArrayList,手写计数循环快约3倍,但对于其余集合,加强的for循环语法将彻底等效于显式迭代器用法。

五、避免使用浮点数 根据经验,浮点数比Android设备上的整数慢约2倍

结尾

本文篇幅有限,性能优化的方面不少,每一项深刻下去,不写个几十万字是结束不了,因此不少都是浅尝辄止,但愿能够抛砖引玉,用个人拙劣的文章,给你们一些帮助。性能优化须要走的路还很远,但愿能和各位同窗一同前行,一块儿进步。

参考: Android APP 性能优化的一些思考 https://www.cnblogs.com/cr330326/p/8011523.html

谷歌官方 http://developer.android.com/topic/performance/

Android性能优化系列之Bitmap图片优化 https://blog.csdn.net/u012124438/article/details/66087785

Android 内存泄漏总结 https://yq.aliyun.com/articles/3009

欢迎关注个人微信公众号,接收第一手技术干货
相关文章
相关标签/搜索