Android性能优化

Android的性能优化,主要是从以下几个方面进行优化的: 稳定(内存溢出、崩溃) 流畅(卡顿) 耗损(耗电、流量) 安装包(APK瘦身) 影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用。所以做好Crash全局监控,处理闪退同时把崩溃信息、异常信息收集记录起来,以便后续分析;合理使用主线程处理业务,不要在主线程中做耗时操作,防止ANR程序无响应发生。

内存是Android运行性能至关重要的一项指标,每个进程能使用的内存是有限的。不合理的使用内存会导致频繁的GC、甚至发生OOM,过多GC会导致App卡顿,而内存泄漏或者内存抖动都可以导致OOM,这是无法接受的。

image.png
总结一下几点

......

  1. 如果父控件有颜色,也是自己需要的颜色,那么就不必在子控件加背景颜色
  2. 如果每个自控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要再父控件上加背景颜色
  3. 尽量减少不必要的嵌套
  4. 能用LinearLayout和FrameLayout,就不要用RelativeLayout,因为RelativeLayout控件相对比较复杂,测绘也想要耗时。
  5. 使用include和merge增加复用,减少层级
  6. ViewStub按需加载,更加轻便
  7. 复杂界面可选择ConstraintLayout,可有效减少层级
  8. onDraw中不要创建新的局部对象
  9. onDraw方法中不要做耗时的任务
  10. 解决各个情况下的内存泄漏,注意平时代码的规范。
  11. 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
  12. 避免在启动时做密集沉重的初始化(Heavy app initialization);
  13. 避免I/O操作、反序列化、网络操作、布局嵌套等。
  14. 代码混淆
  15. 插件化
  16. 资源优化
  17. 使用JobScheduler调度任务
  18. 使用懒惰法则
  19. ListView使用ViewHolder,分段,分页加载
  20. 压缩Bitmap
  21. 使用线程池优化线程
  22. 避免在主线程中做耗时操作

稳定(内存溢出,崩溃)

内存泄露

通俗来讲,内存泄露不仅仅会造成应用内存占用过大,还会导致应用卡顿,造成不好的用户体验。

image.png

这就是Android开发童鞋需要了解的Generational Heap Memory模型,这里我们只关心当对象在Young Generation中存活了一段时间之后,如果没被干掉,那么会被移动到Old Generation中,同理,最后会移动到Permanent Generation中。那么用脚想一想就知道,如果内存泄露了,那么,抱歉,你那块内存随时间推移自然而然将进入Permanent Generation中,然鹅,内存不是白菜,想要多少就有多少,这里,因为沙盒机制的原因,分配给你应用的内存当然是有那么一个极限值的,你不能逾越(有人笑了,不是有large heap么,当然我也笑了,我并没有看到这货被宗师android玩家青睐过),好了,你那块造成泄露内存的对象占着茅坑不拉屎,剩下来可以供其他对象发挥的内存空间就少了;打个比方,舞台小了,演员要登台表演,没有多余空间,他就只能等待其他演员下来他才能表演啊,这等待的时间,是没法连续表演的,所以就卡了嘛。

频繁GC

什么时候会导致频繁GC
  • 内存抖动
    短时间内创建了大量对象同时又被快速释放。比如在一个大循环里去不断创建对象,会导致频繁gc;

  • 内存泄漏
    内存泄漏会导致可用内存逐渐变少,而且内存碎片加多,这也会增多gc次数,甚至可能发生OOM

  • 一次申请太大内存空间
    由于内存碎片的存在,就算内存本身足够,但由于碎片导致无法找到一块大空间,这也会触发gc;

OOM问题

  • 内存泄漏了

  • 大量不可见的对象占据内存,这个其实,很常见,只是大家可能一直不太关心罢了,比如,请求接口返回了列表有100项数据,每项数据比如有100个字段,其中你用户展示数据的只有10几个而已,但是,你解析的时候,剩下的99个不知不觉吃了你的内存,当,有个胖子要内存时,呵呵,嗝屁了

  • 还有一种很常见的场景是一个页面多图的场景,明明每个图只需要加载一个100100的,你却使用原始尺寸(10801980)or更大,而且你一下子还加载个几十张,扛得住么?所以了解一下inSampleSize,或者,如果图片归你们上传管理,你可以借助万象优图,他为你做了剪切好不同尺寸的图片,这样省得你在客户端做图片缩放了

内存泄露

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

那么什么情况下会出现这样的对象呢? 基本可以分为以下四大类:
1、集合类泄漏
2、单例/静态变量造成的内存泄漏
3、匿名内部类/非静态内部类
4、资源未关闭造成的内存泄漏

集合类泄漏

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

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回收,所以就出现了内存泄漏,也就是生命周期长的持有了生命周期短的引用,造成了内存泄漏。

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;
    }
}

匿名内部类/非静态内部类
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 等观察者模式的框架忘记手动解除注册

工具

1、leakcanary傻瓜式操作,哪里有泄漏自动给你显示出来,很直接很暴力。

2、我们平时也要多使用Memory Monitor进行内存监控,这个分析就有些难度了,可以上网搜一下具体怎么使用。

3、Android Lint 它可以帮助我们发现代码机构 / 质量问题,同时提供一些解决方案,内存泄露的会飘黄,用起来很方便,具体使用方法上网学习,这里不多做说明了。

LeakCanary

LeakCanary提供了一种很方便的方式,让我们在开发阶段测试内存泄露,我们不需要自己根据内存块来分析内存泄露的原因,我们只需要在项目中集成他,然后他就会帮我们检测内存泄露,并给出内存泄露的引用链

通过监听Activity的onDestory,手动调用GC,然后通过ReferenceQueue+WeakReference,来判断Activity对象是否被回收,然后结合dump Heap的hpof文件,通过Haha开源库分析泄露的位置

LeakCanary主要的知识点

注册Activity的生命周期的监听器

通过Application.registerActivityLifecycleCallbacks()方法注册Activity的生命周期的监听器,每一个Activity的生命周期都会回调到这个ActivityLifecycleCallbacks上,如果一个Activity走到了onDestory,那么就意味着他就不再存在,然后检测这个Activity是否是真的被销毁

通过ReferenceQueue+WeakReference,来判断对象是否被回收

WeakReference创建时,可以传入一个ReferenceQueue对象,假如WeakReference中引用对象被回收,那么就会把WeakReference对象添加到ReferenceQueue中,可以通过ReferenceQueue中是否为空来判断,被引用对象是否被回收

快(流畅、卡顿)

image.png
布局优化

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

  • 如果父控件有颜色,也是自己需要的颜色,那么就不必在子控件加背景颜色

  • 如果每个自控件的颜色不太一样,而且可以完全覆盖父控件,那么就不需要再父控件上加背景颜色

  • 尽量减少不必要的嵌套

  • 能用LinearLayout和FrameLayout,就不要用RelativeLayout,因为RelativeLayout控件相对比较复杂,测绘也想要耗时。

  • 使用include和merge增加复用,减少层级

  • ViewStub按需加载,更加轻便

  • 复杂界面可选择ConstraintLayout,可有效减少层级

绘制优化

我们平时感觉的卡顿问题最主要的原因之一是因为渲染性能,因为越来越复杂的界面交互,其中可能添加了动画,或者图片等等。我们希望创造出越来越炫的交互界面,同时也希望他可以流畅显示,但是往往卡顿就发生在这里。

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

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

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

image.png

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

image.png
  • onDraw中不要创建新的局部对象

  • onDraw方法中不要做耗时的任务

启动速度优化

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

冷启动

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

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

当系统为我们创建了应用进程之后,开始创建应用程序对象。

1、启动主线程
2、创建主Activity
3、加载布局
4、屏幕布局
5、执行初始绘制

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

image.png

Application创建

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

如果我们有自己的Application,系统会onCreate()在我们的Application对象上调用该方法。之后,应用程序会生成主线程(也称为UI线程),并通过创建主要活动来执行任务。

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

Activity创建

应用程序进程创建活动后,活动将执行以下操作:

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

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

热启动

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

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

温启动

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

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

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

这里是慢的定义:

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

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

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

具体做法:

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

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

<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>

<activity ...
android:theme="@style/AppTheme.Launcher" />

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

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

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

怎么办呢?这些还都是必要的,不能不去初始化啊,那就只能异步加载了。但是并不是所有的都可以进行异步处理。这里分情况给出一些建议:

1、比如像友盟,bugly这样的业务非必要的可以的异步加载。

2、比如地图,推送等,非第一时间需要的可以在主线程做延时启动。当程序已经启动起来之后,在进行初始化。

3、对于图片,网络请求框架必须在主线程里初始化了。

同时因为我们一般会有闪屏页面,也可以把延时启动的地图,推动的启动在这个时间段里,这样合理安排时间片的使用。极大的提高了启动速度。

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

1. GPU过度绘制,定位过度绘制区域

这里直接在开发者选项,打开Show GPU Overdraw,就可以看到效果,轻松发现哪块需要优化。

  • 减少布局层级
    使用ConstraintLayout替换传统的布局方式。

  • 检查是否有多余的背景色设置

我们通常会犯一些低级错误--对被覆盖的父view设置背景,多数情况下这些背景是没有必要的。

2. 主线程耗时操作排查

  • 开启strictmode,这样一来,主线程的耗时操作都将以告警的形式呈现到logcat当中

StrictMode,严苛模式,是Android提供的一种运行时检测机制,用于检测代码运行时的一些不规范的操作,最常见的场景是用于发现主线程的IO操作和网络读写等耗时的操作。

3. 对于measure,layout耗时过多的问题

一般这类问题是由于布局过于复杂的原因导致,现在因为有ConstraintLayout,所以,强烈建议使用ConstraintLayout减少布局层级,问题一般得以解决,如果发现还存在性能问题,可以使用traceView观察方法耗时,来定位下具体原因。

TraceView 是 Android SDK 内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。

4.

5. onDraw里面写代码需要注意

onDraw由于大概每16ms都会被执行一次,因此本身就相当于一个forloop,如果你在里面new对象的话,不知不觉中就满足了短时间内大量对象创建并释放,于是频繁GC就发生了,嗯,内存抖动,于是,卡了。因此,正确的做法是将对象放在外面new出来。

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

第二点: 除了循环之外,onDraw()中不要创建新的局部对象,因为onDraw()方法一般都会频繁大量调用,就意味着会产生大量的零时对象,不进占用过的内存,而且会导致系统更加频繁的GC,大大降低程序的执行速度和效率。

6. json反序列化问题

json反序列化是指将json字符串转变为对象,这里如果数据量比较多,特别是有相当多的string的时候,解析起来不仅耗时,而且还很吃内存。解决的方式是:

  • 精简字段,与后台协商,相关接口剔除不必要的字段。保证最小可用原则。

  • 使用流解析,之前我考虑过json解析优化,在Stack Overflow上搜索到这个。于是了解到Gson.fromJson是可以这样玩的,可以提升25%的解析效率。

image.png

7.viewStub&merge&ViewStub的使用

这里merge和viewStub想必是大家非常了解的两个布局组件了,对于只有在某些条件下才展示出来的组件,建议使用viewStub包裹起来,同样的道理,include 某布局如果其根布局和引入他的父布局一致,建议使用merge包裹起来,如果你担心preview效果问题,这里完全没有必要,因为你可以tools:showIn=""属性,这样就可以正常展示preview了。

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

8.加载优化

这里并没有过多的技术点在里面,无非就是将耗时的操作封装到异步中去了,但是,有一点不得不提的是,要注意多进程的问题,如果你的应用是多进程,你应该认识到你的application的oncreate方法会被执行多次,你一定不希望资源加载多次吧,于是你只在主进程加载,如是有些坑就出现了,有可能其他进程需要那某份资源,然后他这个进程缺没有加载相应的资源,然后就嗝屁了。

9.刷新优化。

  • item 刷新
    对于列表的中的item的操作,比如对item点赞,此时不应该让整个列表刷新,而是应该只刷新这个item.

  • 复杂Activity刷新
    对于较为复杂的页面,个人建议不要写在一个activity中,建议使用几个fragment进行组装,这样一来,module的变更可以只刷新某一个具体的fragment,而不用整个页面都走刷新逻辑。但是问题来了,fragment之间如何共享数据呢?好,看我怎么操作。

image.png

Activity将数据这部分抽象成一个LiveData,交个LiveDataManger数据进行管理,然后各个Fragment通过Activity的这个context从LiveDataManger中拿到LiveData,进行操作,通知activity数据变更等等。哈哈,你没有看错,这个确实和Google的那个LiveData有点像,当然,如果你想使用Google的那个,也自然没问题,只不过,这个是简化版的。项目的引入
'com.tencent.tip:simple_live_data:1.0.1-SNAPSHOT'

10. 动画优化

耗损(耗电、流量)

耗电优化

  • 在定位精度要求不高的情况下,使用wifi或移动网络进行定位,没有必要开启GPS定位。

  • 先验证网络的可用性,在发送网络请求,比如,当用户处于2G状态下,而此时的操作是查看一张大图,下载下来可能都200多K甚至更大,我们没必要去发送这个请求,让用户一直等待那个菊花吧。

  • 适当的做本地缓存,避免频繁请求网络数据,这里,说起来容易,做起来并非三刀两斧就能搞定,要配合良好的缓存策略,区分哪些是一段时间不会变更的,哪些是绝对不能缓存的很重要。

安装包(Apk瘦身)

既然要瘦身,那么我们必须知道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 寻找资源。

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

1、首先我们可以使用lint工具,如果有没有使用过的资源就会打印如下的信息(不会使用的朋友可以上网看一下)

同时我们可以开启资源压缩,自动删除无用的资源

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

2、能自己用XML写Drawable,就自己写,能不用公司的UI切图,就别和他们说话,咱们自己造,做自己的UI,美滋滋。而且这种图片占用空间会很小。

3、重用资源,比如一个三角按钮,点击前三角朝上代表收起的意思,点击后三角朝下,代表展开,一般情况下,我们会用两张图来切换,我们完全可以用旋转的形式去改变

<?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" />

4、压缩PNG和JPEG文件 您可以减少PNG文件的大小,而不会丢失使用工具如图像质量 pngcrushpngquant,或zopflipng。所有这些工具都可以减少PNG文件的大小,同时保持感知的图像质量。

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

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

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

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

不要把AnimationDrawable用于创建逐帧动画,因为这样做需要为动画的每个帧包含一个单独的位图文件,这会大大增加APK的大小。

7、代码混淆 使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。这个大家太熟悉了。不多说了。

8、插件化。 比如功能模块放在服务器上,按需下载,可以减少安装包大小。

减少安装包大小

代码混淆。使用IDE 自带的 proGuard 代码混淆器工具 ,它包括压缩、优化、混淆等功能。 资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。 图片优化。比如利用 PNG优化工具 对图片做压缩处理。推荐目前最先进的压缩工具Googlek开源库zopfli。如果应用在0版本以上,推荐使用 WebP图片格式。 避免重复或无用功能的第三方库。例如,百度地图接入基础地图即可、讯飞语音无需接入离线、图片库Glide\Picasso等。 插件化开发。比如功能模块放在服务器上,按需下载,可以减少安装包大小。 可以使用微信开源资源文件混淆工具——AndResGuard。一般可以压缩apk的1M左右大。

耗电优化

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

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

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

具体功能:
1、可以推迟的非面向用户的任务(如定期数据库数据更新);
2、当充电时才希望执行的工作(如备份数据);
3、需要访问网络或 Wi-Fi 连接的任务(如向服务器拉取配置数据);
4、零散任务合并到一个批次去定期运行;
5、当设备空闲时启动某些任务;
6、只有当条件得到满足, 系统才会启动计划中的任务(充电、WIFI...);

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

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

推迟 应用是否需要立即执行操作?例如,它可以等到设备充电才能将数据备份到云端吗?

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

谷歌在耗电优化这方面确实显得有些无力,希望以后可以退出更好的工具和解决方案,不然这方面的优化优先级还是很低。付出和回报所差太大。

内存优化准则

  1. 能不创建的对象就不创建
    比如字符串拼接,可以手动使用StringBuilder,而不是使用"+","+"被编译器优化后会每次创建StringBuilder对象,造成浪费;
    而且,尤其注意在主线程里不要过多创建对象。因为在GC时会锁住堆内存,此时请求分配的线程也会被挂起,这显然会导致主线程的卡顿。所以在一些主线程高频函数,如onDraw,onTouchEvent里不要去创建对象。

  2. 尽可能复用已经创建的对象
    还是StringBuilder的例子,基于一个StringBuilder可以通过SetLength(0)支持很多次的字符串拼接。
    多使用系统提供的对象池,比如线程池,Long、Integer、Short等包类型里的缓存值(通过valueOf取),列表view的复用等

  3. 防止内存泄漏
    内存一旦发生泄漏,意味着堆里有一块区域持续被不再使用的变量占据,这自然会导致可用内存减少而发生gc,甚至OOM。

代码建议

1. SparseArray代替HashMap

建议使用SparseArray代替HashMap,这里是Google建议的,因为SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱比如(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。

推荐使用match_parent,或者固定尺寸

不到不得已,不要使用wrap_content,,推荐使用match_parent,或者固定尺寸,配合gravity="center"。
因为 在测量过程中,match_parent和固定宽高度对应EXACTLY ,而wrap_content对应AT_MOST,这两者对比AT_MOST耗时较多。

使用Link优化项目

Lint 是Android Studio 提供的 代码扫描分析工具,它可以帮助我们发现代码结构/质量问题,同时提供一些解决方案,而且这个过程不需要我们手写测试用例。

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)

线程优化

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

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

  • 优点:
    1、减少在创建和销毁线程上所花的时间以及系统资源的开销。
    2、如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。
  • 需要注意的是:
    1、如果线程池中的数量为达到核心线程的数量,则直接会启动一个核心线程来执行任务。
    2、如果线程池中的数量已经达到或超过核心线程的数量,则任务会被插入到任务队列中标等待执行。
    3、如果(2)中的任务无法插入到任务队列中,由于任务队列已满,这时候如果线程数量未达到线程池规定最大值,则会启动一个非核心线程来执行任务。
    4、如果(3)中线程数量已经达到线程池最大值,则会拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法通知调用者。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容