Android性能优化之启动优化实战

本文首发于微信公众号「Android开发之旅」,欢迎关注html

前言

本文将带领你们来看看启动优化相关方面的介绍以及各类优化的方法。但愿你在读完本章后会有所收获。python

相信不少同窗都听过八秒定律,八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,若是等待网页打开的时间超过了8秒,就有超过70%的用户放弃等待。足见启动的时间是多么的重要。放到移动APP中,那就是应用启动的时间不能过久,不然就会形成用户的流失。android

谷歌官方曾给出一篇App startup time的文章,这篇文章详细介绍了关于启动优化的切入点以及思路。感兴趣的同窗能够去看下。App Startup Time 这是官方地址。本篇文章也主要是官方思路的一个扩展。shell

启动分类

App的启动主要分为:冷启动、热启动和温启动。性能优化

冷启动:

耗时最多,也是整个应用启动时间的衡量标准。咱们经过一张图来看下冷启动经历的流程:bash

冷启动经历的流程

热启动:

启动最快,应用直接由后台切换到前台。微信

温启动:

启动较快,是介于冷启动和热启动之间的一种启动方式,温启动只会执行Activity相关的生命周期方法,不会执行进程的建立等操做。网络

咱们优化的方向和重点主要是冷启动。由于它才是表明了应用从被用户点击到最后的页面绘制完成所耗费的全部时间。下面咱们经过一张流程图来看下冷启动相关的任务流程:多线程

冷启动任务的流程

看上面的任务的流程图,读者朋友们以为哪些是咱们优化的方向呢?其实咱们能作的只有Application和Activity的生命周期阶段,由于其余的都是系统建立的咱们无法干预,好比:启动App,加载空白Window,建立进程等。这里面加载空白Window咱们其实能够作一个假的优化就是使用一张启动图来替换空白Window,具体操做咱们在下文中介绍。并发

启动的测量方式

这里主要介绍两种方式:ADB命令和手动打点。下面咱们就来看下二者的使用以及优缺点。

ADB命令:

在Android Studio的Terminal中输入如下命令

adb shell am start  -W packagename/[packagename].首屏Activity
复制代码

执行以后控制台中输出以下内容:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
Status: ok
Activity: com.optimize.performance/.MainActivity
ThisTime: 563
TotalTime: 563
WaitTime: 575
Complete
复制代码

其中主要有三个字端:ThisTime、TotalTime和WaitTime,分别解释下这三个字端的含义:

ThisTime:最后一个Activity启动耗时

TotalTime:全部Activity启动耗时

WaitTime:AMS启动Activity的总耗时

ThisTime和TotalTime时间相同是由于咱们的Demo中没有Splash界面,应用执行完Application后直接就开始了MainActivity了。因此正常状况下的启动耗时应是这样的:ThisTime < TotalTime < WaitTime

这就是ADB方式统计的启动时间,细心的读者应该能想到了就是这种方式在线下使用很方便,可是却不能带到线上,并且这种统计的方式是非严谨、精确的时间。

手动打点方式:

手动打点方式就是启动时埋点,启动结束埋点,取两者差值便可。

咱们首先须要定义一个统计时间的工具类:

class LaunchRecord {
​
    companion object {
​
        private var sStart: Long = 0
        
        fun startRecord() {
            sStart = System.currentTimeMillis()
        }
​
        fun endRecord() {
            endRecord("")
        }
​
        fun endRecord(postion: String) {
            val cost = System.currentTimeMillis() - sStart
            println("===$postion===$cost")
        }
    }
}
复制代码

启动时埋点咱们直接在Application的attachBaseContext中进行打点。那么启动结束应该在哪里打点呢?这里存在一个误区:网上不少资料建议是在Activity的onWindowFocusChange中进行打点,可是onWindowFocusChange这个回调只是表示首帧开始绘制了,并不能表示用户已经看到页面数据了,咱们既然作启动优化,那么就要切切实实的得出用户从点击应用图标到看到页面数据之间的时间差值。因此结束埋点建议是在页面数据展现出来进行埋点。好比页面是个列表那就是第一条数据显示出来,或者其余的任何view的展现。

class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        //开始打点
        LaunchRecord.startRecord()
    }
}
复制代码

咱们分别监听页面view的绘制完成时间和onWindowFocusChanged回调两个值进行对比。

class MainActivity : AppCompatActivity() {
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
​
        mTextView.viewTreeObserver.addOnDrawListener {
            LaunchRecord.endRecord("onDraw")
        }
​
    }
​
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LaunchRecord.endRecord("onWindowFocusChanged")
    }
}
复制代码

打印的数据为:

===onWindowFocusChanged===322
===onDraw===328

复制代码

能够很明显看到onDraw所须要的时长是大于onWindowFocusChanged的时间的。由于咱们这个只是简单的数据展现没有进行网络相关请求和复杂布局因此差异不大。

这里须要说明下:addOnDrawListener 须要大于API 16才可使用,若是为了兼顾老版本用户可使用addOnPre DrawListener来代替。

手动打点方式统计的启动时间比较精确并且能够带到线上使用,推荐这种方式。但在使用的时候要避开一个误区就是启动结束的埋点咱们要采用Feed第一条数据展现出来来进行统计。同时addOnDrawListener要求API 16,这两点在使用的时候须要注意的。

优化工具的选择

在作启动优化的时候咱们能够借助三方工具来更好的帮助咱们理清各个阶段的方法或者线程、CPU的执行耗时等状况。主要介绍如下两个工具,我在这里就简单介绍下,读者朋友们能够线下本身取尝试下。

TraceView:

TraceView是以图形的形式展现执行时间、调用栈等信息,信息比较全面,包含全部线程。

使用:

开始:Debug.startMethodTracing("name" )
结束:Debug.stopMethodTracing("" )
复制代码

最后会生成一个文件在SD卡中,路径为:Andrid/data/packagename/files。

由于traceview收集的信息比较全面,因此会致使运行开销严重,总体APP的运行会变慢,这就有可能会带偏咱们优化的方向,由于咱们没法区分是否是traceview影响了启动时间。

SysTrace:

Systrace是结合Android内核数据,生成HTML报告,从报告中咱们能够看到各个线程的执行时间以及方法耗时和CPU执行时间等。API 18以上使用,推荐使用TraceCompat,由于这是兼容的API。

使用:

开始:TraceCompat.beginSection("tag ")
结束:TraceCompat.endSection()
复制代码

而后执行脚本:

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app
复制代码

给你们解释下各个字端的含义:

  • -b 收集数据的大小
  • -t 时间
  • -a 监听的应用包名
  • -o 生成文件的名称

Systrace开销较小,属于轻量级的工具,而且能够直观反映CPU的利用率。这里须要说明下在生成的报告中,当你看某个线程执行耗时时会看到两个字端分别好似walltime和cputime,这两个字端给你们解释下就是walltime是代码执行的时间,cputime是代码真正消耗cpu的执行时间,cputime才是咱们优化的重点指标。这点很容易被你们忽视。

优雅获取方法耗时

上文中主要是讲解了如何监听总体的应用启动耗时,那么咱们如何识别某个方法所执行的耗时呢?

咱们常规的作法和上文中同样也是打点,如:

public class MyApp extends Application {
​
    @Override
    public void onCreate() {
        super.onCreate();
​
        initFresco();
        initBugly();
        initWeex();
    }
​
    private void initWeex(){
        LaunchRecord.Companion.startRecord();
        InitConfig config = new InitConfig.Builder().build();
        WXSDKEngine.initialize(this, config);
        LaunchRecord.Companion.endRecord("initWeex");
    }
​
    private void initFresco() {
        LaunchRecord.Companion.startRecord();
        Fresco.initialize(this);
        LaunchRecord.Companion.endRecord("initFresco");
    }
​
    private void initBugly() {
        LaunchRecord.Companion.startRecord();
        CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
        LaunchRecord.Companion.endRecord("initBugly");
    }
}
复制代码

控制台打印:

=====initFresco=====278
=====initBugly=====76
=====initWeex=====83
复制代码

可是这种方式致使代码不够优雅,而且侵入性强并且工做量大,不利于后期维护和扩展。

下面我给你们介绍另一种方式就是AOP。AOP是面向切面变成,针对同一类问题的统一处理,无侵入添加代码。

咱们主要使用的是AspectJ框架,在使用以前呢给你们简单介绍下相关的API:

  • Join Points 切面的地方:函数调用、执行,获取设置变量,类初始化
  • PointCut:带条件的JoinPoints
  • Advice:Hook 要插入代码的位置。
  • Before:PointCut以前执行
  • After:PointCut以后执行
  • Around:PointCut以前以后分别执行

具体代码以下:

@Aspect
public class AOPJava {
​
    @Around("call(* com.optimize.performance.MyApp.**(..))")
    public void applicationFun(ProceedingJoinPoint joinPoint) {
​
        Signature signature = joinPoint.getSignature();
        String name = signature.toShortString();
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
​
        Log.d("AOPJava", name + " == cost ==" + (System.currentTimeMillis() - time));
​
    }
}
复制代码

控制台打印结果以下:

MyApp.initFresco() == cost ==288
MyApp.initBugly() == cost ==76
MyApp.initWeex() == cost ==85
复制代码

可是咱们没有在MyApp中作任何改动,因此采用AOP的方式来统计方法耗时更加方便而且代码无侵入性。具体AspectJ的使用学习后续文章来介绍。

异步优化

上文中咱们主要是讲解了一些耗时统计的方法策略,下面咱们就来具体看下如何进行启动耗时的优化。

在启动分类中咱们讲过应用启动任务中有一个空白window,这是能够做为优化的一个小技巧就是Theme的切换,使用一个背景图设置给Activity,当Activity打开后再将主题设置回来,这样会让用户感受很快。但其实从技术角度讲这种优化并无效果,只是感官上的快。

首先如今res/drawable中新建lanucher.xml文件:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@android:color/white"/>
    <item>
        <bitmap
            android:src="@mipmap/你的图片"
            android:gravity="fill"/>
    </item>
</layer-list>
复制代码

将其设置给第一个打开的Activity,如MainActivity:

<activity android:name=".MainActivity"
    android:theme="@style/Theme.Splash">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
​
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
复制代码

最后在MainActivity中的onCreate的spuer.onCreate()中将其设置会原来的主题:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        }
​
    }
复制代码

这样就完成了Theme主题的切换。

下面咱们说下异步优化,异步优化顾名思义就是采用异步的方式进行任务的初始化。新建子线程(线程池)分担主线称任务并发的时间,充分利用CPU。

若是使用线程池那么设置多少个线程合适呢?这里咱们参考了AsyncTask源码中的设计,获取可用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));
​
    @Override
    public void onCreate() {
        super.onCreate();
​
        ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initFresco(); 
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initWeex();
            }
        });
​
    }
复制代码

这样咱们就将全部的任务进行异步初始化了。咱们看下未异步的时间和异步的对比:

未异步时间:======210
异步的时间:======3
复制代码

能够看出这个时间差仍是比较明显的。这里还有另一个问题就是,好比异步初始化Fresco,可是在MainActivity一加载就要使用而Fresco是异步加载的有可能这时候尚未加载完成,这样就会抛异常了,怎么办呢?这里教你们一个新的技巧就是使用CountDownLatch,如:

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));
​
    //1表示要被知足一次countDown
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);
​
    @Override
    public void onCreate() {
        super.onCreate();
​
        ExecutorService pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initFresco();
                //调用一次countDown
                mCountDownLatch.countDown();
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });
​
        pool.submit(new Runnable() {
            @Override
            public void run() {
                initWeex();
            }
        });
​
        try {
            //若是await以前没有调用countDown那么就会一直阻塞在这里
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
    }

复制代码

这样就会一直阻塞在await这里,直到Fresco初始化完成。

以上这种方式你们以为如何呢?能够解决异步问题,可是个人Demo中只有三个须要初始化的任务,在咱们真实的项目中可不止,因此在项目中咱们须要书写不少的子线程代码,这样显然是不够优雅的。部分代码须要在初始化的时候就要完成,虽然可使用countDowmLatch,可是任务较多的话,也是比较麻烦的,另外就是若是任务之间存在依赖关系,这种使用异步就很难处理了。

针对上面这些问题,我给你们介绍一种新的异步方式就是启动器。核心思想就是充分利用CPU多核,自动梳理任务顺序。核心流程:

  • 任务代码Task化,启动逻辑抽象为Task
  • 根据全部任务依赖关系排序生成一个有向无环图
  • 多线程按照排序后的优先级依次执行
TaskDispatcher.init(PerformanceApp.)TaskDispatcher dispatcher = TaskDispatcher.createInstance()dispatcher.addTask(InitWeexTask())
        .addTask(InitBuglyTask())
        .addTask(InitFrescoTask())
        .start()dispatcher.await()LaunchTimer.endRecord()
复制代码

最后代码会变成这样,具体的实现有向无环图逻辑由于代码量不少,不方便贴出来,你们能够关注公众号获取。

使用有向无环图能够很好的梳理出每一个任务的执行逻辑,以及它们之间的依赖关系

延迟初始化

关于延迟初始化方案这里介绍二者方式,一种是比较常规的作法,另一个是利用IdleHandler来实现。

常规作法就是在Feed显示完第一条数据后进行异步任务的初始化。好比:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        
        mTextView.viewTreeObserver.addOnDrawListener {
            // initTask()
        }
​
    }

复制代码

这里有个问题就是更新UI是在Main线程执行的,因此作初始化任务等耗时操做时会发生UI的卡顿,这时咱们可使用Handler.postDelay(),可是delay多久呢?这个时间是很差控制的。因此这种常规的延迟初始化方案有可能会致使页面的卡顿,而且延迟加载的时机很差控制。

IdleHandler方式就是利用其特性,只有CPU空闲的时候才会执行相关任务,而且咱们能够分批进行任务初始化,能够有效缓解界面的卡顿。代码以下:

public class DelayInitDispatcher {
​
    private Queue<Task> mDelayTasks = new LinkedList<>();
​
    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if (mDelayTasks.size() > 0) {
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };
​
    public DelayInitDispatcher addTask(Task task) {
        mDelayTasks.add(task);
        return this;
    }
​
    public void start() {
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }
​
}

复制代码

咱们在界面显示的后进行调用:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        
        mTextView.viewTreeObserver.addOnDrawListener {
            val delayInitDispatcher = DelayInitDispatcher()
            delayInitDispatcher.addTask(DelayInitTaskA())
                    .addTask(DelayInitTaskB())
                    .start()
        }
    }
复制代码

这样就能够利用系统空闲时间来延迟初始化任务了。

懒加载

懒加载就是有些Task只有在特定的页面才会使用,这时候咱们就不必将这些Task放在Application中初始化了,咱们能够将其放在进入页面后在进行初始化。

其余方案

提早加载SharedPreferences,当咱们项目的sp很大的时候初次加载很耗内存和时间的,咱们能够将其提早在初始化Multidex(若是使用的话)以前进行初始化,充分利用此阶段的CPU。

启动阶段不启动子进程,子进程会共享CPU资源,致使主CPU资源紧张,另一点就是在Application生命周期中也不要启动其余的组件如:service、contentProvider。

异步类加载方式,如何肯定哪些类是须要提早异步加载呢?这里咱们能够自定义classload,替换掉系统的classload,在咱们的classload中打印日志,每一个类在加载的时候都会触发的log日志,而后在项目中运行一遍,这样就拿到了全部须要加载的类了,这些就是须要咱们异步加载的类。

  • Class.forName()只加载类自己及其静态变量的引用类
  • new实例能够额外加载类成员的引用类

总结

本文主要是讲解了启动耗时的检测,从总体流程的耗时到各个方法的耗时以及线程的耗时,也介绍了工具的选择和使用,介绍了启动时间的优化,异步加载、延迟加载、懒加载等等,从常规方法到更优解,讲解了不少方式方法,但愿能给你们提供一些新的思路和解决问题的方式。也但愿你们能在本身的项目中实战总结。

推荐阅读:

App性能概览与平台化实践理论

Android性能优化之布局优化实战

如何监测Android应用卡顿?这篇就够了

扫描下方二维码关注公众号,及时获取文章推送。

扫一扫 关注公众号
相关文章
相关标签/搜索