深入探索Android启动速度优化

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

在性能优化的整个知识体系中,最重要的就是稳定性优化,在上一篇文章 《深入探索Android稳定性优化》 中我们已经深入探索了Android稳定性优化的疆域。那么,除了稳定性以外,对于性能纬度来说,哪个方面的性能是最重要的呢?毫无疑问,就是应用的启动速度。下面,就让我们扬起航帆,一起来逐步深入探索Android启动速度优化的奥秘

一、启动优化的意义

如果我们去一家餐厅吃饭,在点餐的时候等了半天都没有服务人员过来,可能就没有耐心等待直接走了。

对于App来说,也是同样如此,如果用户点击App后,App半天都打不开,用户就可能失去耐心卸载应用

启动速度是用户对我们App的第一体验,打开应用后才能去使用其中提供的强大功能,就算我们应用的内部界面设计的再精美,功能再强大,如果启动速度过慢,用户第一印象就会很差

因此,拯救App的启动速度,迫在眉睫。

二、应用启动流程

1 、应用启动的类型

应用启动的类型总共分为如下三种:

  • 冷启动
  • 热启动
  • 温启动

下面,我们来详细分析下各个启动类型的特点及流程。

冷启动

从点击应用图标到UI界面完全显示且用户可操作的全部过程。

特点

耗时最多,衡量标准

启动流程

Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl

首先,用户进行了一个点击操作,这个点击事件它会触发一个IPC的操作,之后便会执行到Process的start方法中,这个方法是用于进程创建的,接着,便会执行到ActivityThread的main方法,这个方法可以看做是我们单个App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制

热启动

直接从后台切换到前台。

特点

启动速度最快。

温启动

只会重走Activity的生命周期,而不会重走进程的创建,Application的创建与生命周期等。

特点

较快,介于冷启动和热启动之间的一个速度。

启动流程

LifeCycle -> ViewRootImpl

ViewRootImpl是什么?

它是GUI管理系统与GUI呈现系统之间的桥梁。每一个ViewRootImpl关联一个Window,
ViewRootImpl 最终会通过它的setView方法绑定Window所对应的View,并通过其performTraversals方法对View进行布局、测量和绘制

2、冷启动分析及其优化方向

冷启动涉及的相关任务

冷启动之前
  • 首先,会启动App
  • 然后,加载空白Window
  • 最后,创建进程

需要注意的是,这些都是系统的行为,一般情况下我们是无法直接干预的。

随后任务
  • 首先,创建Application
  • 启动主线程
  • 创建MainActivity
  • 加载布局
  • 布置屏幕
  • 首帧绘制

通常到了界面首帧绘制完成后,我们就可以认为启动已经结束了。

优化方向

我们的优化方向就是 Application和Activity的生命周期 这个阶段,因为这个阶段的时机对于我们来说是可控的

三、启动耗时检测

1、查看Logcat

在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。

2、adb shell

使用adb shell获取应用的启动时间

// 其中的AppstartActivity全路径可以省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路径]

执行后会得到三个时间:ThisTime、TotalTime和WaitTime,详情如下:

ThisTime

表示最后一个Activity启动耗时。

TotalTime

表示所有Activity启动耗时。

WaitTime

表示AMS启动Activity的总耗时。

一般来说,只需查看得到的TotalTime,即应用的启动时间,其包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。

特点:

  • 1、线下使用方便,不能带到线上
  • 2、非严谨、精确时间

3、代码打点(函数插桩)

可以写一个统计耗时的工具类来记录整个过程的耗时情况。其中需要注意的有:

  • 在上传数据到服务器时建议根据用户ID的尾号来抽样上报
  • 在项目中核心基类的关键回调函数和核心方法中加入打点。

其代码如下所示:

/**
* 耗时监视器对象,记录整个过程的耗时情况,可以用在很多需要统计的地方,比如Activity的启动耗时和Fragment的启动耗时。
*/
public class TimeMonitor {

    private final String TAG = TimeMonitor.class.getSimpleName();
    private int mMonitord = -1;
    
    // 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
    private HashMap<String, Long> mTimeTag = new HashMap<>();
    private long mStartTime = 0;

    public TimeMonitor(int mMonitorId) {
        Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
        this.mMonitorId = mMonitorId;
    }

    public int getMonitorId() {
        return mMonitorId;
    }

    public void startMonitor() {
        // 每次重新启动都把前面的数据清除,避免统计错误的数据
        if (mTimeTag.size() > 0) {
        mTimeTag.clear();
        }
        mStartTime = System.currentTimeMillis();
    }

    /**
    * 每打一次点,记录某个tag的耗时
    */
    public void recordingTimeTag(String tag) {
        // 若保存过相同的tag,先清除
        if (mTimeTag.get(tag) != null) {
            mTimeTag.remove(tag);
        }
        long time = System.currentTimeMillis() - mStartTime;
        Log.d(TAG, tag + ": " + time);
        mTimeTag.put(tag, time);
    }

    public void end(String tag, boolean writeLog) {
        recordingTimeTag(tag);
        end(writeLog);
    }

    public void end(boolean writeLog) {
        if (writeLog) {
            //写入到本地文件
        }
    }

    public HashMap<String, Long> getTimeTags() {
        return mTimeTag;
    }
}

为了使代码更好管理,我们需要定义一个打点配置类,如下所示:

/**
* 打点配置类,用于统计各阶段的耗时,便于代码的维护和管理。
*/
public final class TimeMonitorConfig {

    // 应用启动耗时
    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}

此外,耗时统计可能会在多个模块和类中需要打点,所以需要一个单例类来管理各个耗时统计的数据

/**
* 采用单例管理各个耗时统计的数据。
*/
public class TimeMonitorManager {

    private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;

    public synchronized static TimeMonitorManager getInstance() {
        if (mTimeMonitorManager == null) {
            mTimeMonitorManager = new TimeMonitorManager();
        }
        return mTimeMonitorManager;
    }

    public TimeMonitorManager() {
        this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
    }

    /**
     * 初始化打点模块
    */
    public void resetTimeMonitor(int id) {
        if (mTimeMonitorMap.get(id) != null) {
            mTimeMonitorMap.remove(id);
        }
        getTimeMonitor(id);
    }

    /**
    * 获取打点器
    */
    public TimeMonitor getTimeMonitor(int id) {
        TimeMonitor monitor = mTimeMonitorMap.get(id);
        if (monitor == null) {
            monitor = new TimeMonitor(id);
            mTimeMonitorMap.put(id, monitor);
        }
        return monitor;
    }
}

主要在以下几个方面需要打点:

  • 应用程序的生命周期节点
  • 启动时需要初始化的重要方法,例如数据库初始化,读取本地的一些数据。
  • 其他耗时的一些算法

例如,启动时在Application和第一个Activity加入打点统计:

Application 打点

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}

@Override
public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}

第一个Activity打点

@Override
protected void onCreate(Bundle savedInstanceState) {
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
    super.onCreate(savedInstanceState);
    
    initData();
    
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}

@Override
protected void onStart() {
    super.onStart();
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}

特点

精确,可带到线上,但是代码有侵入性,修改成本高

注意事项

  • 1、在上传数据到服务器时建议根据用户ID的尾号来抽样上报

  • 2、onWindowFocusChanged只是首帧时间,App启动完成的结束点应该是真实数据展示出来的时候(通常来说都是首帧数据),如列表第一条数据展示,记得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上可以使用addOnDrawListener),它会把任务延迟到列表显示后再执行,例如,在Awesome-WanAndroid项目的主页就有一个RecyclerView实现的列表,启动结束的时间就是列表的首帧时间,也即列表第一条数据展示的时候。这里,我们直接在RecyclerView的适配器ArticleListAdapter的convert(onBindViewHolder)方法中加上如下代码即可:

      if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
          mHasRecorded = true;
          helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
              @Override
              public boolean onPreDraw() {
                  helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
                  LogHelper.i("FeedShow");
                  return true;
              }
          });
      }
    

具体的实例代码可在 这里查看

为什么不使用onWindowFocusChanged这个方法作为启动结束点?

因为用户看到真实的界面是需要有网络请求返回真实数据的,但是onWindowFocusChanged只是界面绘制的首帧时机,但是列表中的数据是需要从网络中下载得到的,所以应该以列表的首帧数据作为启动结束点。

4、AOP(Aspect Oriented Programming) 打点

面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。

1、作用

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低提高程序的可重用性,同时大大提高了开发效率

2、AOP核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理。

2、切面(Aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象

3、连接点(JoinPoint)

被拦截到的点(方法、字段、构造器)。

4、切入点(PointCut)

对JoinPoint进行拦截的定义。

5、通知(Advice)

拦截到JoinPoint后要执行的代码,分为前置、后置、环绕三种类型。

3、准备:接入AspectJx进行切面编码

首先,为了在Android使用AOP埋点需要引入AspectJ,在项目根目录的build.gradle下加入:

classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'

然后,在app目录下的build.gradle下加入:

apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'

4、AOP埋点实战

JoinPoint一般定位在如下位置

  • 1、函数调用
  • 2、获取、设置变量
  • 3、类初始化

使用PointCut对我们指定的连接点进行拦截,通过Advice,就可以拦截到JoinPoint后要执行的代码。Advice通常有以下几种类型:

  • 1、Before:PointCut之前执行
  • 2、After:PointCut之后执行
  • 3、Around:PointCut之前、之后分别执行

首先,我们举一个小栗子:

@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}

在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有Activity中on开头的方法

其中execution是处理Join Point的类型,在AspectJx中共有两种类型,如下所示:

  • 1、call:插入在函数体里面
  • 2、execution:插入在函数体外面
如何统计Application中的所有方法耗时?
@Aspect
public class ApplicationAop {

    @Around("call (* com.json.chao.application.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
    }
}

在上述代码中,我们需要注意 不同的Action类型其对应的方法入参是不同的,具体的差异如下所示:

  • 当Action为Before、After时,方法入参为JoinPoint
  • 当Action为Around时,方法入参为ProceedingPoint
Around和Before、After的最大区别:

ProceedingPoint不同于JoinPoint,其提供了proceed方法执行目标方法

5、总结AOP特性

  • 1、无侵入性
  • 2、修改方便,建议使用

4、启动速度分析工具 — TraceView

1、使用方式

  • 1、代码中添加:Debug.startMethodTracing()、检测方法、Debug.stopMethodTracing()。(需要使用adb pull将生成的.trace文件导出到电脑,然后使用Android Studio的Profiler进行加载)
  • 2、打开 Profiler -> CPU -> 点击 Record -> 点击 Stop -> 查看Profiler下方Top Down/Bottom Up 区域,以找出耗时的热点方法

2、Profile CPU

使用 Profile 的 CPU 模块可以帮我们快速找到耗时的热点方法,下面,我们来详细来分析一下这个模块。

1、Trace types

Trace types 有四种,如下所示。

1、Trace Java Methods

会记录每个方法的时间、CPU信息。对运行时性能影响较大。

2、Sample Java Methods

相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值