Performance optimization of Android to start the actual optimization

Foreword

This article will take you to see a variety of optimization methods and introduce relevant aspects of startup optimization. We hope you will gain after reading this chapter.

I believe that many students have heard of the law of eight seconds, eight seconds law is a law exists in the Internet field, meaning that when a user accesses a Web site, if the wait for the page open for more than 8 seconds, and there are more than 70% of users give up waiting. Which shows how important it is to start time yes. APP into mobile, the application startup time that is not too long, otherwise it will cause the loss of users.

Google officials have given an App startup time of the article, which details the entry point and start thinking about optimization. Interested students can go to the next. App Startup Time This is the official address. This article is mainly an extension of official thinking.

Start classification

App startup divided into: cold start, warm start and warm start.

Cold start:

Most time-consuming, but also the entire application startup measure of time. Our next cold start process experienced by a map view:

Performance optimization of Android to start the actual optimization

Hot Start:

Start fastest, application switching directly from background to foreground.

Warm start:

Start fast, is the range between the start mode of cold start and hot start, warm start will only execute Activity related to the life-cycle approach, it does not perform the process of creation and other operations.

We optimize the direction and focus mainly cold start. Because it is the time of the application on behalf of all the clicks from the user to the last page it takes to complete the drawing. Let's start a cold-related tasks through a process flow chart of view:

Performance optimization of Android to start the actual optimization

See flow chart above tasks, readers who think what we optimized direction? In fact, we can do only Application and Activity life cycle stages, because other systems are created we can not interfere, such as: Start App, load blank Window, create a process and so on. There we can load blank Window in fact, doing so is a fake optimization using the startup plans to replace a blank Window, specific operations we introduce below.

Start of measurement

This introduces two ways: ADB command and manually RBI. Here we use both lower and their strengths and weaknesses on the run.

ADB command:

Enter the following command in the Terminal in Android Studio

adb shell am start  -W packagename/[packagename].首屏Activity

After executing the console output the following:

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实例可以额外加载类成员的引用类

总结

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

Well, the article to end here if you feel the article fairly useful, may wish to recommend them to your friends.

Guess you like

Origin blog.51cto.com/14332859/2446567