序文
この記事では、最適化の様々な方法を参照し、起動の最適化の関連する側面を紹介する行くことができます。私たちは、あなたがこの章を読んだ後得ることを願っています。
私は多くの学生が8秒の法則を聞いたことがあることを信じて、8秒の法律は法律は、ユーザは、Webサイトにアクセスしたときにことを意味し、インターネットの分野に存在している場合は8秒以上のために開いているページを待つ、とがあるユーザーの70%以上待ってあきらめます。それはイエスの時間を開始することがいかに重要であるかを示しています。モバイルへのAPP、長すぎるではないアプリケーションの起動時間は、それ以外の場合は、ユーザーの損失が発生します。
Googleの関係者は、エントリポイントを詳述し、最適化を考え始めるの記事、のアプリケーションの起動時間を与えています。興味のある学生は、次に行くことができます。アプリの起動時間は、これは公式のアドレスです。この記事では主に公式の考え方を拡張したものです。
分類を開始
コールドスタート、ウォームスタートとウォームスタート:アプリの起動を分け。
コールドスタート:
ほとんどの時間がかかり、また時間の全体のアプリケーションの起動対策。マップビューで経験した私たちの次のコールドスタートプロセス:
ホットスタート:
アプリケーション背景から前景に直接切り替え、最速起動します。
ウォームスタート:
高速起動、コールドスタートとホットスタートのスタートモードの間の範囲であり、唯一のライフサイクル・アプローチに関連した活動を実行しますウォームスタートは、それが作成し、他の操作の処理を行いません。
私たちは、方向を最適化し、主にコールドスタートを集中します。それが最後のページへのユーザーからのすべてのクリックの代わりに、アプリケーションの時間があるので、それは、描画を完了するのにかかります。ビューのプロセスフロー・チャートを通じて冷たい関連のタスクを起動してみましょう:
タスク上記のフローチャートを参照してください、読者は誰我々は方向を最適化するものと思いますか?その上で、スタートのApp空白のウィンドウをロードし、プロセスを作成して:実際には、我々は、他のシステムが作成されているので、我々のような、干渉することができない、唯一のアプリケーションとアクティビティのライフサイクル段階を行うことができます。そこ我々はそうすることが、我々は以下の紹介空白のウィンドウ、特定の操作を置き換えるために、スタートアップの計画を使用して偽の最適化で、実際には空白のウィンドウをロードすることができます。
計測のスタート
ADBコマンドと手動RBI:これは、2つの方法を紹介します。ここでは、実行時に低く、その長所と短所の両方を使用します。
ADBコマンド:
Androidのメーカーで、ターミナルで次のコマンドを入力します。
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实例可以额外加载类成员的引用类
总结
本文主要是讲解了启动耗时的检测,从整体流程的耗时到各个方法的耗时以及线程的耗时,也介绍了工具的选择和使用,介绍了启动时间的优化,异步加载、延迟加载、懒加载等等,从常规方法到更优解,讲解了很多方式方法,希望能给大家提供一些新的思路和解决问题的方式。也希望大家能在自己的项目中实战总结。
さて、あなたは記事がかなり便利に感じる場合は、ここで終了する記事は、あなたの友人にそれらをお勧めしたいことがあります。