Android性能优化之启动速度优化

Android性能优化之启动速度优化

  Android app 启动速度优化,首先谈谈为何会走到优化这一步,若是一开始建立 app 项目的时候就把这个启动速度考虑进去,那么确定就不须要从新再来优化一遍了。这是由于在移动互联网时代,你们都追求快,什么功能都是先作出来再说,其余的能够先不考虑,先占据先机,或者验证是否值得作。那为何要这么作呢?我我的的观点有如下几点android

  • 若是 app 不能快速开发出来,先放出去验证一下可行性,可能连是否值得作都不知道,若是花很长时间作了一个对用户无价值的功能,那么还不如不作
  • 若是 app 不能快速作出来,可能被竞争对手捕获先机,那么可能错失最佳商业时机
  • 若是一开始就规定不能影响启动速度的这个目标,那么作功能的时候就会有束缚,快不起来
  • app 初期你们都忙着开发新功能,迭代新版本,没有时间停下来作优化
  • 同类型 app 变多,竞争对手变多,你们才开始关注启动性能,才开始作启动速度优化(有主动出击也有被动优化)

1、引发性能问题的缘由

  随着项目不断的快速迭代,每每会形成App启动卡慢现象,由于可能在App主进程启动阶段或者在主界面启动阶段放了不少初始化其余业务的逻辑,而这些业务落地可能一开始并不须要用到。本文从做者的亲身经历给你们阐述启动速度优化相关的点点滴滴,为启动速度优化提供一种思路给你们参考。git

2、为何要作启动速度优化

  App启动卡慢会影响一个App的卸载率和使用率。启动速度快会给人一种轻快的感受,减小用户等待时间。若是一个App从点击桌面图标到看到主界面花了10秒,请问你能接受么?忍耐很差的估计直接就卸载了,或者没等打开就直接Home键按出去,而后杀进程了。这样一来App卸载率提高了,使用率降低了。因此对于有大量用户的App来讲,这些性能细节是很重要的,毕竟用户就是钱啊。github

3、分析制定优化技术路线

3.1 分析启动性能瓶颈

  在具体的优化以前,首先咱们得找到须要优化的地方,怎么找?这就要求了解Android App的启动原理,咱们要知道一个App从点击桌面图标到咱们看到App的主界面整个过程当中通过了哪些步骤,哪些地方是咱们能够优化的地方。下图是App启动过程的一个大概描述。canvas

具体的代码流程,分析关键的函数耗时
安全

  图中onFirstDrawFinish和onWindowFocusChanged的先后顺序可能会颠倒,可是时间差不大。性能优化

3.2 制定优化方向

  从上面的分析能够看出,App启动过程当中咱们优化的地方包括主进程启动流程和主界面启动流程,主进程启动就是Application的建立过程,主界面启动就是MainActivity的建立过程。只须要分别对这两个部分进行优化便可。app

  1. Application中attachBaseContext最先被调用,随后是onCreate方法,尽可能在这两个方法中不要有耗时操做。
  2. MainActivity中重点关注onCreate,onResume,onWindowFocusChange,Activity启动完成结束标志这里采用没有使用生命周期函数,而是以主界面View的第一次绘制做为启动完成的标志,View被第一次绘制证实View即将展现出来被咱们看到。因此咱们在Activity根布局中加入一个自定义View,以它的onDraw方法第一次回调做为Activity启动完成的标志。异步

    public class FirstDrawListenView extends View {
         private boolean isFirstDrawFinish = false;
    
         private IFirstDrawListener mIFirstDrawListener;
    
         public FirstDrawListenView(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
    
         @Override
         protected void onDraw(Canvas canvas) {
             super.onDraw(canvas);
             if (!isFirstDrawFinish) {
                 isFirstDrawFinish = true;
                 if (mIFirstDrawListener != null) {
                     mIFirstDrawListener.onFirstDrawFinish();
                 }
             }
         }
    
         public void setFirstDrawListener(IFirstDrawListener firstDrawListener) {
             mIFirstDrawListener = firstDrawListener;
         }
    
         public interface IFirstDrawListener {
             void onFirstDrawFinish();
         }
     }

4、怎么统计数据查看优化先后的数据对比

  经过上面的分析,咱们能够统计进程启动各个阶段的耗时点,以及Activity启动各个阶段的耗时点(这个步骤须要额外在主布局中加入一个自定义的空View,监听它的onDraw方法的第一次回调),能够经过埋点数据收集这些数据,在优化以前能够先加入埋点数据,统计上报各个时间段的埋点,因此须要先发个版本验证一下优化以前的状况。统计数据的机制加入以后,就能够着手优化了,一边优化一边对比,能够很清楚看到优化先后的对比。ide

5、制定优化的目标

  因为App启动速度在不一样是设备上差异很大,因此目标不太好定,可是作事情总得要有个目标吧。首先咱们使用你们都熟悉的一个概念“秒开”,其次是冷启动热启动分开算,再次是分出不一样的机型(高端机,中端机型,低端机型),最后是须要先看看没优化以前的启动数据。这样就能够定义出相似下面的目标:函数

  1. 高端机型1秒内打开(好比小米5,Android6.0以上)
  2. 中端机型1.5秒内打开
  3. 低端机型2.5秒内打开

  上面是终极目标,真正优化的时候,要结合App实际数据以及团队实际状况来定本身的优化目标。

6、优化具体步骤

  通常来讲,快速优化最好的方式就是把没必要要提早作的操做放到异步线程中去作,也就是咱们常常作的异步加载。除了异步加载,一些真正有性能影响的代码须要作具体优化。下面依次介绍一些具体的优化实施步骤。

6.1 封装一个打印耗时点日志的辅助类

  优化的时候为了快速定位耗时的代码块,咱们须要在耗时代码块的先后加上日志,统计耗时具体的时间。这个能在Debug模式下帮助咱们快速分析定位到耗时的代码块,而后咱们在针对具体的耗时代码块去下手,看看怎么优化。

6.2 异步加载一:Application中加入异步线程

  在Application中封装两个方法:onSyncLoad(同步加载)和 onAsyncLoad(异步加载,在Thread中执行),把不须要同步加载的部分所有放到onAsyncLoad方法,须要同步的方法放到onSyncLoad中去作,就这种简单的分类就能够带来一个很好的优化效果。

public class StartUpApplication extends Application {

    @Override
    public void onCreate() {
        // 程序建立时调用,次方法应该执行应该尽可能快,不然会拖慢整个app的启动速度
        super.onCreate();
        onSyncLoadForCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        onSyncLoad();
        onAsyncLoad();
    }

    private void onSyncLoadForCreate() {
        AppStartUpTimeLog.isColdStart = true;   // 设置为冷启动标志
        AppLog.log("StartUpApplication onCreate");
        AppStartUpTimeLog.logTimeDiff("App onCreate start", false, true);
        BlockingUtil.simulateBlocking(500); // 模拟阻塞100毫秒
        AppStartUpTimeLog.logTimeDiff("App onCreate end");
    }

    private void onSyncLoad() {
        AppLog.log("StartUpApplication attachBaseContext");
        AppStartUpTimeLog.markStartTime("App attachBaseContext", true);
        BlockingUtil.simulateBlocking(200); // 模拟阻塞100毫秒
        AppStartUpTimeLog.logTimeDiff("App attachBaseContext end", true);
    }

    public void onAsyncLoad() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 异步加载逻辑
            }
        }, "ApplicationAsyncLoad").start();
    }
}

6.3 异步加载二:MainActivity中加入异步线程

  这一步骤与Application的优化思路同样,也是封装onSyncLoad和onAsyncLoad方法对现有代码进行一个分类,可是这两个方法的调用时机要晚一点,是在主界面首屏绘制完成的时候调用。这个步骤也须要new一个Thead,属于额外的开销,不过这不影响咱们总体性能。

6.4 延迟加载功能:首屏绘制完成以后加载

  还有些操做必需要在UI线程作,可是不须要那么快速就作,这里放到首屏绘制完成以后,咱们以前在主布局中加入一个空的View来监听它的第一次onDraw回调,咱们经过接口的方式把这个事件接到咱们的MainActivity中去(Activity中实现接口的onFirstDrawFinish方法)。为了让用户尽快看到主界面,咱们就能够把一些须要在UI线程执行,可是又不须要那么快的执行的操做放到onFirstDrawFinish中去。

6.5 动态加载布局:主布局文件优化

  把主界面中不须要第一次就用到的布局所有使用动态加载的方式来处理,使用ViewStub或者直接在使用时动态addView的方式。

6.6 主布局文件深度优化

  若是作了上面这些优化仍是会发现进入主界面仍是有些慢,那么须要重点关注主布局文件了。主布局文件的复杂度直接影响到了Activity的加载速度,这个时候须要对主布局文件进行深度优化了。Activity在加载布局的时候,会对整个布局文件进行解析,测量(measure),布局(layout)和绘制(draw),因此设计简单合理的布局尤其重要。布局的优化不作详细介绍,网上不少文章的。几个重要的优化以下:

  1. 减小布局层级
  2. 减小首次加载View的数量
  3. 减小过分绘制

  若是须要看看主布局加载具体用了多少时间,须要用自定ViewGroup做为根布局根元素,而后监控它的onInflateFinished,onMeasure,onLayout,onDraw方法,经过咱们以前写好的打印时间日志的辅助类,打印一些关键日志,能够分析出具体的耗时的步骤,还能够定位哪一个View加载耗时最长。

6.7 功能代码深度优化

  前面的优化步骤中,咱们有部分耗时操做放到了首屏绘制onFirstDrawFinish以后来作了,这里会带来一个体验上的问题,虽然进入主界面变快了,可是可能进入以后短暂的时间类UI线程是阻塞的,若是有其余的UI操做可能会卡主,由于onFirstDrawFinish中挂了不少耗时的操做,须要等这些作完以后UI线程才能空闲。因此咱们还须要对一些功能代码进行优化,确保其真正用时少。另外咱们异步加载线程中的操做是有必定的安全风险的,若是有些操做很耗时,可能致使咱们进入主界面须要用到数据时尚未准备好,因此异步加载咱们要注意代码块的顺序,若是有些很是耗时的操做考虑用单独的线程去处理。

7、总结

  优化是一条持续之路,经过优化咱们能够了解到影响启动性能的因素有哪些,这样咱们平时在编码的过程当中就会多注意本身的代码性能。本文从全局的角度去看待整个启动性能优化,看起来好像还挺容易,可是可能实际过程当中优化并不会很顺利,不一样的设备上可能表现不同,有时候可能启动一个服务都会耗时。因此要想真正的不耗时,那就是大招:删除它吧。

8、项目地址

模拟耗时点,打印日志观察生命周期函数回调状况

https://github.com/PopFisher/AppStartUpSpeedOpt

相关文章
相关标签/搜索