Android深度性能优化--启动优化

694 阅读7分钟

本文整理自网络课程

应用的启动速度对一个APP来说至关重要,会直接影响到用户体验,如果启动速度过慢会导致用户的流失,本文就启动速度优化分析,为优化启动速度提供一些思路。

 

一、获取启动时间

1、adb命令获取

启动方式分两种:冷启动和热启动

冷启动:应用启动时后台无应用进程,需新创建进程分配给应用,Application会重新创建并执行生命周期;

热启动:应用启动时后台已存在应用进程,一般Home键退出,Back键退出,任务列表中存在进程任务,Application不会重新创建;

获取启动时间adb命令

adb shell am start -W [packageName]/[packageName.xxActivity]

       

ThisTime:最后一个Activity启动耗时;

TotalTime:启动时经历的所有Activity启动耗时;

WaiteTime:AMS启动所有Activity的总时间(包括启动目标应用前的当前Activity的onPause方法耗时和TotalTime)

可以看出冷启动热启动耗时差距很大;

谈到的启动耗时一般都是冷启动耗时,所以我们一般着重优化冷启动耗时;

该方式只能统计到应用启动耗时时间,无法精确定位是哪个方法耗时;


2、时间戳打点

public class LaunchTimer {


    private static long sTime;


    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }
    public static void endRecord() {
        endRecord("");
    }
    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - sTime;
        LogUtils.i(msg + "cost " + cost);
    }

该方式一般为Application初始化attachBaseContext方法打启动开始时间戳,应用用户可操作界面完全展示可操作后打结束时间戳,两时间差即为启动耗时;

二、启动优化工具

1、Traceview

traceview 是Android 自带性能工具,可图形化展示方法调用时间,调用栈,还可以查看所有线程信息,对分析方法耗时,调用链是非常好的工具。

1.1 使用方法

代码埋点:

开始收集,传入自定义文件名

Debug.startMethodTracing("app_trace");

结束收集

Debug.stopMethodTracing();

默认文件生成在/sdcard/Android/data/com.xxx.xx/files/app_trace.trace

使用Android Studio打开该文件

    

 

区域1用于选择时间范围,滑动可以选择查看时间范围内线程运行

区域2可以查看到有多少线程和执行时间,点击每个线程区域3可显示每个线程对应的执行方法;

区域3可以查看堆栈信息

Call Chart:查看方法时间段和时间消耗,从上往下依次为调用顺序,系统api一般为橙色,应用自身方法为绿色,第三方api一般是蓝色;

Flame Chart:Call Chart的倒序调用;

       

Top Down:该区域可以看每个方法内部调用了哪些方法,对查看调用堆栈十分滴方便,此处注意几个概念;

Total:方法执行总耗时;Self:方法内部除了调用的子方法总耗时;Children:调用的子方法总耗时;Total = Self + Children

Wall Clock Time:该方法在线程内部执行总耗时;

Thread Time:该方法CPU消耗执总耗时;

Bottom Up:显示一个方法的被调用列表,和Top Down相反;

2、Systrace

Systrace 是平台提供的一款工具,用于记录短期内的设备活动。该工具会生成一份报告,其中汇总了 Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。这份报告可帮助我们了解如何以最佳方式改善应用性能;

官方文档:developer.android.google.cn/studio/prof…

2.1 使用方法

收集开始,传入自定义tag

TraceCompat.beginSection("AppOnCreate");

收集结束

TraceCompat.endSection();

使用python命令生成报告

cd到Android/sdk/platform-tools/systrace

执行python命令,python环境没有配置好的话可使用Home Brew install一下

python systrace.py -b 32768 -t 10 -a 包名 -o browser.html sched gfx view wm am app

具体参数可参考:developer.android.google.cn/studio/prof…

执行命令后,启动应用,待生成报告后打开报告

       

A键左移,D键右移,S键缩小,W键扩大

区域1为CPU区域,可查看系统分配CPU情况,我们优化方向就是尽可能不让CPU空闲率过高;

区域2为应用进程所有线程的方法消耗时间分配,可以方便的查询到耗时过长的方法;

3、AOP

如果想获取启动过程中每个方法的耗时,可以用埋点的方式进行获取,但是侵入性高,埋点麻烦,需要在每个方法前后进行打点,AOP方式可以解决这个问题。

3.1 使用方法

project build.gradle 中添加插件

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

app build.gradle中添加依赖

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

首先介绍几个概念

Join Points:程序运行时的执行点,可以作为切面的地方,比如方法调用执行、获取设置变量、类初始化

PointCut:带条件的Join Points

Advice:一种Hook,需要插入代码的位置,Before(PointCut之前),After(PointCut之后),Around(PointCut前后都要执行)

3.2 语法介绍

/*第一种*/
  @Pointcut("execution(* android.app.Activity.on**(..))")
    public void executionActivityOn(){


    }


    @Before("executionActivityOn()")
    public void executionActivityOnBefore(JoinPoint joinPoint){
        Log.d(TAG, "executionActivityOnBefore: ");
    }
/*第二种*/
   @Before("execution(* android.app.Activity.on**(..))")
    public void executionActivityOnBefore(JoinPoint joinPoint){
        Log.d(TAG, "executionActivityOnBefore: ");
    

3.3 获取方法耗时

@Around("call(* com.test.performance.PerformanceApp.init**(..))")
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();
    }
    LogUtils.i("app method : "+name + " cost " + (System.currentTimeMillis() - time));
}

PerformanceApp.java

@Override
public void onCreate() {
    super.onCreate();
    initAMap();
    initBugly();
    initFresco();
    initJPush();
    initStetho();
    initUmeng();
    initWeex();
    }

输出结果

三、启动异步优化

1、启动Theme优化

设置首屏主题背景

<style name="Theme.Splash" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowBackground">@drawable/lanucher</item>
    <item name="windowActionBar">false</item>
    <item name="android:windowNoTitle">true</item>
    <item name="windowNoTitle">true</item>
</style>
@drawable/lanucher
<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="@mipmap/splash"
            android:gravity="fill"/>
    </item>
</layer-list>

首屏OnCreate切换主题

setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);

2、启动多线程优化

核心思想:将各任务放置单独线程中并行执行;

创建线程池,根据CPU核数创建核心线程数

// 线程等待锁
private CountDownLatch mCountDownLatch = new CountDownLatch(3);


// CPU核数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 核心线程数
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));


ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
    @Override public void run() {
        initAMap();
        initBugly();
        initFresco();
        mCountDownLatch.countDown();
    }
});


service.submit(new Runnable() {
    @Override public void run() {
        initJPush();
        initStetho();
        initUmeng();
        mCountDownLatch.countDown();
    }
});


service.submit(new Runnable() {
    @Override public void run() {
        initWeex();
        mCountDownLatch.countDown();
    }
}

CountDownLatch

使一个线程等待其他线程各自执行完毕后再执行后续方法,可保证所有线程执行完毕后进入主页面;

3、使用自建启动器

使用线程池异步加载代码不太优雅,通用性不高,维护性较高;

3.1 启动器原理

  • 充分利用CPU多核能力,自动梳理并顺序执行任务;
  • 代码Task化,将启动任务抽象成各个task;
  • 根据所有任务依赖关系排序生成一个有向无环图;
  • 多线程按照线程优先级顺序执行;

3.2 流程图

       

本节介绍下启动器原理,后期单独针对启动器分析一下;

4、使用IdleHandler

IdleHandler 可以用来提升性能,主要用在能够在当前线程消息队列空闲时做些事情(譬如 UI 线程在显示完成后,如果线程空闲我们就可以提前准备其他内容)的情况下,不过最好不要做耗时操作。具体用法如下。

//getMainLooper().myQueue()或者Looper.myQueue()
Looper.myQueue().addIdleHandler(new IdleHandler() {  
    @Override  
    public boolean queueIdle() {  
        //你要处理的事情
        return false;    
    }  
});

5、其它优化方案

5.1 提前加载SharedPreferences

在multidex之前CPU是空闲的,加载系统类是可行的,所以可充分利用这段时间加载SharedPreferences;

5.2 启动阶段不启动子进程

初始化子进程会消耗CPU资源,在启动阶段会导致主进程CPU资源紧张,导致启动阶段资源紧张初始化过慢;

5.3 启动阶段不启动Service ContentProvider

在Application 生命周期方法attachBaseContext方法和onCreate方法中间还会执行ContentProvider生命周期方法,如果在application初始化过程中启动了service或者contentprovider会执行ContentProvider生命周期方法,非常耗时。

5.4 提前异步类加载

对启动阶段用到的类进行提前异步加载,加载方法有两种:

  • Class.forName() 只加载类本身及其静态变量的引用类
  • new 类实例 可以额外加载类成员变量的引用类

5.5 启动阶段抑制GC

5.6 CPU锁频


后续笔记
《Android深度性能优化--APP内存优化》
《Android深度性能优化--APP布局优化》
《Android深度性能优化--APP卡顿优化》
《Android深度性能优化--APP线程优化》
《Android深度性能优化--APP网络优化》
《Android深度性能优化--APP电量优化》
《Android深度性能优化--APP Crash优化》

持续关注添加微信公众号:xyx的笔记