App優(yōu)化--啟動(dòng)速度

轉(zhuǎn)自 : Android性能優(yōu)化之啟動(dòng)優(yōu)化實(shí)戰(zhàn)

Jetpack版Wan-Android項(xiàng)目地址:Android Jetpack架構(gòu)開發(fā)組件化應(yīng)用實(shí)戰(zhàn)

Flutter版Wan-Android項(xiàng)目地址:Flutter版Wan-Android

BoostMultiDex是一個(gè)用于Android低版本設(shè)備(4.X及以下,SDK < 21)快速加載多DEX的解決方案琅束,由抖音/Tiktok Android技術(shù)團(tuán)隊(duì)出品房铭。

AppStartFaster--啟動(dòng)器 一部分是冷啟動(dòng)任務(wù)分發(fā)罗售,一部分是Multdex冷啟動(dòng)優(yōu)化

AppLauncher是一個(gè)輕量的Android App的任務(wù)啟動(dòng)器掸哑。

前言

本文將帶領(lǐng)大家來看看啟動(dòng)優(yōu)化相關(guān)方面的介紹以及各種優(yōu)化的方法贾虽。希望你在讀完本章后會(huì)有所收獲杆怕。

相信很多同學(xué)都聽過八秒定律糠溜,八秒定律是在互聯(lián)網(wǎng)領(lǐng)域存在的一個(gè)定律笤虫,即指用戶訪問一個(gè)網(wǎng)站時(shí)旁瘫,如果等待網(wǎng)頁打開的時(shí)間超過了8秒,就有超過70%的用戶放棄等待琼蚯。足見啟動(dòng)的時(shí)間是多么的重要酬凳。放到移動(dòng)APP中,那就是應(yīng)用啟動(dòng)的時(shí)間不能太久遭庶,否則就會(huì)造成用戶的流失宁仔。

谷歌官方曾給出一篇App startup time的文章,這篇文章詳細(xì)介紹了關(guān)于啟動(dòng)優(yōu)化的切入點(diǎn)以及思路峦睡。感興趣的同學(xué)可以去看下翎苫。App Startup Time 這是官方地址。本篇文章也主要是官方思路的一個(gè)擴(kuò)展榨了。

啟動(dòng)分類

App的啟動(dòng)主要分為:冷啟動(dòng)煎谍、熱啟動(dòng)和溫啟動(dòng)。

冷啟動(dòng):

耗時(shí)最多龙屉,也是整個(gè)應(yīng)用啟動(dòng)時(shí)間的衡量標(biāo)準(zhǔn)呐粘。我們通過一張圖來看下冷啟動(dòng)經(jīng)歷的流程:

image

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">冷啟動(dòng)經(jīng)歷流程</figcaption>

熱啟動(dòng):

啟動(dòng)最快,應(yīng)用直接由后臺(tái)切換到前臺(tái)。

溫啟動(dòng):

啟動(dòng)較快事哭,是介于冷啟動(dòng)和熱啟動(dòng)之間的一種啟動(dòng)方式漫雷,溫啟動(dòng)只會(huì)執(zhí)行Activity相關(guān)的生命周期方法,不會(huì)執(zhí)行進(jìn)程的創(chuàng)建等操作鳍咱。

我們優(yōu)化的方向和重點(diǎn)主要是冷啟動(dòng)降盹。因?yàn)樗攀谴砹藨?yīng)用從被用戶點(diǎn)擊到最后的頁面繪制完成所耗費(fèi)的所有時(shí)間。下面我們通過一張流程圖來看下冷啟動(dòng)相關(guān)的任務(wù)流程:

[圖片上傳失敗...(image-ed43d2-1611049860758)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">冷啟動(dòng)相關(guān)任務(wù)流</figcaption>

看上面的任務(wù)的流程圖谤辜,讀者朋友們覺得哪些是我們優(yōu)化的方向呢蓄坏?其實(shí)我們能做的只有Application和Activity的生命周期階段,因?yàn)槠渌亩际窍到y(tǒng)創(chuàng)建的我們沒法干預(yù)丑念,比如:啟動(dòng)App涡戳,加載空白Window,創(chuàng)建進(jìn)程等脯倚。這里面加載空白Window我們其實(shí)可以做一個(gè)假的優(yōu)化就是使用一張啟動(dòng)圖來替換空白Window渔彰,具體操作我們?cè)谙挛闹薪榻B。

啟動(dòng)的測(cè)量方式

這里主要介紹兩種方式:ADB命令和手動(dòng)打點(diǎn)推正。下面我們就來看下兩者的使用以及優(yōu)缺點(diǎn)恍涂。

ADB命令:

在Android Studio的Terminal中輸入以下命令

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

執(zhí)行之后控制臺(tái)中輸出如下內(nèi)容:

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

其中主要有三個(gè)字端:ThisTime、TotalTime和WaitTime植榕,分別解釋下這三個(gè)字端的含義:

ThisTime:最后一個(gè)Activity啟動(dòng)耗時(shí)

TotalTime:所有Activity啟動(dòng)耗時(shí)

WaitTime:AMS啟動(dòng)Activity的總耗時(shí)

ThisTime和TotalTime時(shí)間相同是因?yàn)槲覀兊腄emo中沒有Splash界面再沧,應(yīng)用執(zhí)行完Application后直接就開始了MainActivity了。所以正常情況下的啟動(dòng)耗時(shí)應(yīng)是這樣的:ThisTime < TotalTime < WaitTime

這就是ADB方式統(tǒng)計(jì)的啟動(dòng)時(shí)間尊残,細(xì)心的讀者應(yīng)該能想到了就是這種方式在線下使用很方便炒瘸,但是卻不能帶到線上,而且這種統(tǒng)計(jì)的方式是非嚴(yán)謹(jǐn)寝衫、精確的時(shí)間顷扩。

手動(dòng)打點(diǎn)方式:

手動(dòng)打點(diǎn)方式就是啟動(dòng)時(shí)埋點(diǎn),啟動(dòng)結(jié)束埋點(diǎn)竞端,取二者差值即可屎即。

我們首先需要定義一個(gè)統(tǒng)計(jì)時(shí)間的工具類:

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")
        }
    }
}

啟動(dòng)時(shí)埋點(diǎn)我們直接在Application的attachBaseContext中進(jìn)行打點(diǎn)。那么啟動(dòng)結(jié)束應(yīng)該在哪里打點(diǎn)呢事富?這里存在一個(gè)誤區(qū):網(wǎng)上很多資料建議是在Activity的onWindowFocusChange中進(jìn)行打點(diǎn)技俐,但是onWindowFocusChange這個(gè)回調(diào)只是表示首幀開始繪制了,并不能表示用戶已經(jīng)看到頁面數(shù)據(jù)了统台,我們既然做啟動(dòng)優(yōu)化雕擂,那么就要切切實(shí)實(shí)的得出用戶從點(diǎn)擊應(yīng)用圖標(biāo)到看到頁面數(shù)據(jù)之間的時(shí)間差值。所以結(jié)束埋點(diǎn)建議是在頁面數(shù)據(jù)展示出來進(jìn)行埋點(diǎn)贱勃。比如頁面是個(gè)列表那就是第一條數(shù)據(jù)顯示出來井赌,或者其他的任何view的展示谤逼。

class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        //開始打點(diǎn)
        LaunchRecord.startRecord()
    }
}

我們分別監(jiān)聽頁面view的繪制完成時(shí)間和onWindowFocusChanged回調(diào)兩個(gè)值進(jìn)行對(duì)比。

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")
    }
}

打印的數(shù)據(jù)為:

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

可以很明顯看到onDraw所需要的時(shí)長是大于onWindowFocusChanged的時(shí)間的仇穗。因?yàn)槲覀冞@個(gè)只是簡單的數(shù)據(jù)展示沒有進(jìn)行網(wǎng)絡(luò)相關(guān)請(qǐng)求和復(fù)雜布局所以差別不大流部。

這里需要說明下:addOnDrawListener 需要大于API 16才可以使用,如果為了兼顧老版本用戶可以使用addOnPre DrawListener來代替纹坐。

手動(dòng)打點(diǎn)方式統(tǒng)計(jì)的啟動(dòng)時(shí)間比較精確而且可以帶到線上使用枝冀,推薦這種方式。但在使用的時(shí)候要避開一個(gè)誤區(qū)就是啟動(dòng)結(jié)束的埋點(diǎn)我們要采用Feed第一條數(shù)據(jù)展示出來來進(jìn)行統(tǒng)計(jì)耘子。同時(shí)addOnDrawListener要求API 16果漾,這兩點(diǎn)在使用的時(shí)候需要注意的。

優(yōu)化工具的選擇

在做啟動(dòng)優(yōu)化的時(shí)候我們可以借助三方工具來更好的幫助我們理清各個(gè)階段的方法或者線程谷誓、CPU的執(zhí)行耗時(shí)等情況绒障。主要介紹以下兩個(gè)工具,我在這里就簡單介紹下捍歪,讀者朋友們可以線下自己取嘗試下户辱。

TraceView:

TraceView是以圖形的形式展示執(zhí)行時(shí)間、調(diào)用棧等信息糙臼,信息比較全面焕妙,包含所有線程。

使用:

開始:Debug.startMethodTracing("name" )
結(jié)束:Debug.stopMethodTracing("" )

最后會(huì)生成一個(gè)文件在SD卡中弓摘,路徑為:Andrid/data/packagename/files。

因?yàn)閠raceview收集的信息比較全面痕届,所以會(huì)導(dǎo)致運(yùn)行開銷嚴(yán)重韧献,整體APP的運(yùn)行會(huì)變慢,這就有可能會(huì)帶偏我們優(yōu)化的方向研叫,因?yàn)槲覀儫o法區(qū)分是不是traceview影響了啟動(dòng)時(shí)間锤窑。

SysTrace:

Systrace是結(jié)合Android內(nèi)核數(shù)據(jù),生成HTML報(bào)告嚷炉,從報(bào)告中我們可以看到各個(gè)線程的執(zhí)行時(shí)間以及方法耗時(shí)和CPU執(zhí)行時(shí)間等渊啰。API 18以上使用,推薦使用TraceCompat申屹,因?yàn)檫@是兼容的API绘证。

使用:

開始:TraceCompat.beginSection("tag ")
結(jié)束:TraceCompat.endSection()

然后執(zhí)行腳本:

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

給大家解釋下各個(gè)字端的含義:

  • -b 收集數(shù)據(jù)的大小
  • -t 時(shí)間
  • -a 監(jiān)聽的應(yīng)用包名
  • -o 生成文件的名稱

Systrace開銷較小,屬于輕量級(jí)的工具哗讥,并且可以直觀反映CPU的利用率嚷那。這里需要說明下在生成的報(bào)告中,當(dāng)你看某個(gè)線程執(zhí)行耗時(shí)時(shí)會(huì)看到兩個(gè)字端分別好似walltime和cputime杆煞,這兩個(gè)字端給大家解釋下就是walltime是代碼執(zhí)行的時(shí)間魏宽,cputime是代碼真正消耗cpu的執(zhí)行時(shí)間腐泻,cputime才是我們優(yōu)化的重點(diǎn)指標(biāo)。這點(diǎn)很容易被大家忽視队询。

優(yōu)雅獲取方法耗時(shí)

上文中主要是講解了如何監(jiān)聽整體的應(yīng)用啟動(dòng)耗時(shí)派桩,那么我們?nèi)绾巫R(shí)別某個(gè)方法所執(zhí)行的耗時(shí)呢?

我們常規(guī)的做法和上文中一樣也是打點(diǎn)蚌斩,如:

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(), "注冊(cè)時(shí)申請(qǐng)的APPID", false);
        LaunchRecord.Companion.endRecord("initBugly");
    }
}

控制臺(tái)打用蟆:

=====initFresco=====278
=====initBugly=====76
=====initWeex=====83

但是這種方式導(dǎo)致代碼不夠優(yōu)雅,并且侵入性強(qiáng)而且工作量大凳寺,不利于后期維護(hù)和擴(kuò)展鸭津。

下面我給大家介紹另外一種方式就是AOP。AOP是面向切面變成肠缨,針對(duì)同一類問題的統(tǒng)一處理逆趋,無侵入添加代碼。

我們主要使用的是AspectJ框架晒奕,在使用之前呢給大家簡單介紹下相關(guān)的API:

  • Join Points 切面的地方:函數(shù)調(diào)用闻书、執(zhí)行,獲取設(shè)置變量脑慧,類初始化
  • PointCut:帶條件的JoinPoints
  • Advice:Hook 要插入代碼的位置魄眉。
  • Before:PointCut之前執(zhí)行
  • After:PointCut之后執(zhí)行
  • Around:PointCut之前之后分別執(zhí)行

具體代碼如下:

@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));

    }
}

控制臺(tái)打印結(jié)果如下:

MyApp.initFresco() == cost ==288
MyApp.initBugly() == cost ==76
MyApp.initWeex() == cost ==85

但是我們沒有在MyApp中做任何改動(dòng),所以采用AOP的方式來統(tǒng)計(jì)方法耗時(shí)更加方便并且代碼無侵入性闷袒。具體AspectJ的使用學(xué)習(xí)后續(xù)文章來介紹坑律。

異步優(yōu)化

上文中我們主要是講解了一些耗時(shí)統(tǒng)計(jì)的方法策略,下面我們就來具體看下如何進(jìn)行啟動(dòng)耗時(shí)的優(yōu)化囊骤。

在啟動(dòng)分類中我們講過應(yīng)用啟動(dòng)任務(wù)中有一個(gè)空白window晃择,這是可以作為優(yōu)化的一個(gè)小技巧就是Theme的切換,使用一個(gè)背景圖設(shè)置給Activity也物,當(dāng)Activity打開后再將主題設(shè)置回來宫屠,這樣會(huì)讓用戶感覺很快。但其實(shí)從技術(shù)角度講這種優(yōu)化并沒有效果滑蚯,只是感官上的快浪蹂。

首先現(xiàn)在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>

將其設(shè)置給第一個(gè)打開的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()中將其設(shè)置會(huì)原來的主題:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)
        }

    }

這樣就完成了Theme主題的切換告材。

下面我們說下異步優(yōu)化坤次,異步優(yōu)化顧名思義就是采用異步的方式進(jìn)行任務(wù)的初始化。新建子線程(線程池)分擔(dān)主線稱任務(wù)并發(fā)的時(shí)間斥赋,充分利用CPU浙踢。

如果使用線程池那么設(shè)置多少個(gè)線程合適呢?這里我們參考了AsyncTask源碼中的設(shè)計(jì)灿渴,獲取可用CPU的數(shù)量洛波,并且根據(jù)這個(gè)數(shù)量計(jì)算一個(gè)合理的數(shù)值胰舆。

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();
            }
        });

    }

這樣我們就將所有的任務(wù)進(jìn)行異步初始化了。我們看下未異步的時(shí)間和異步的對(duì)比:

未異步時(shí)間:======210
異步的時(shí)間:======3

可以看出這個(gè)時(shí)間差還是比較明顯的蹬挤。這里還有另外一個(gè)問題就是缚窿,比如異步初始化Fresco,但是在MainActivity一加載就要使用而Fresco是異步加載的有可能這時(shí)候還沒有加載完成焰扳,這樣就會(huì)拋異常了倦零,怎么辦呢?這里教大家一個(gè)新的技巧就是使用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();
                //調(diào)用一次countDown
                mCountDownLatch.countDown();
            }
        });

        pool.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });

        pool.submit(new Runnable() {
            @Override
            public void run() {
                initWeex();
            }
        });

        try {
            //如果await之前沒有調(diào)用countDown那么就會(huì)一直阻塞在這里
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

這樣就會(huì)一直阻塞在await這里扫茅,直到Fresco初始化完成。

以上這種方式大家覺得如何呢育瓜?可以解決異步問題葫隙,但是我的Demo中只有三個(gè)需要初始化的任務(wù),在我們真實(shí)的項(xiàng)目中可不止躏仇,所以在項(xiàng)目中我們需要書寫很多的子線程代碼恋脚,這樣顯然是不夠優(yōu)雅的。部分代碼需要在初始化的時(shí)候就要完成焰手,雖然可以使用countDowmLatch糟描,但是任務(wù)較多的話,也是比較麻煩的书妻,另外就是如果任務(wù)之間存在依賴關(guān)系船响,這種使用異步就很難處理了。

針對(duì)上面這些問題躲履,我給大家介紹一種新的異步方式就是啟動(dòng)器灿意。核心思想就是充分利用CPU多核,自動(dòng)梳理任務(wù)順序崇呵。核心流程:

  • 任務(wù)代碼Task化,啟動(dòng)邏輯抽象為Task
  • 根據(jù)所有任務(wù)依賴關(guān)系排序生成一個(gè)有向無環(huán)圖
  • 多線程按照排序后的優(yōu)先級(jí)依次執(zhí)行
TaskDispatcher.init(PerformanceApp.)TaskDispatcher dispatcher = TaskDispatcher.createInstance()dispatcher.addTask(InitWeexTask())
        .addTask(InitBuglyTask())
        .addTask(InitFrescoTask())
        .start()dispatcher.await()LaunchTimer.endRecord()

最后代碼會(huì)變成這樣馅袁,具體的實(shí)現(xiàn)有向無環(huán)圖邏輯因?yàn)榇a量很多域慷,不方便貼出來,大家可以關(guān)注公眾號(hào)獲取汗销。

使用有向無環(huán)圖可以很好的梳理出每個(gè)任務(wù)的執(zhí)行邏輯犹褒,以及它們之間的依賴關(guān)系

延遲初始化

關(guān)于延遲初始化方案這里介紹兩者方式,一種是比較常規(guī)的做法弛针,另外一個(gè)是利用IdleHandler來實(shí)現(xiàn)叠骑。

常規(guī)做法就是在Feed顯示完第一條數(shù)據(jù)后進(jìn)行異步任務(wù)的初始化。比如:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)

        mTextView.viewTreeObserver.addOnDrawListener {
            // initTask()
        }

    }

這里有個(gè)問題就是更新UI是在Main線程執(zhí)行的削茁,所以做初始化任務(wù)等耗時(shí)操作時(shí)會(huì)發(fā)生UI的卡頓宙枷,這時(shí)我們可以使用Handler.postDelay(),但是delay多久呢掉房?這個(gè)時(shí)間是不好控制的。所以這種常規(guī)的延遲初始化方案有可能會(huì)導(dǎo)致頁面的卡頓慰丛,并且延遲加載的時(shí)機(jī)不好控制卓囚。

IdleHandler方式就是利用其特性,只有CPU空閑的時(shí)候才會(huì)執(zhí)行相關(guān)任務(wù)诅病,并且我們可以分批進(jìn)行任務(wù)初始化哪亿,可以有效緩解界面的卡頓。代碼如下:

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);
    }

}

我們?cè)诮缑骘@示的后進(jìn)行調(diào)用:

override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(R.style.AppTheme)
        super.onCreate(savedInstanceState)

        mTextView.viewTreeObserver.addOnDrawListener {
            val delayInitDispatcher = DelayInitDispatcher()
            delayInitDispatcher.addTask(DelayInitTaskA())
                    .addTask(DelayInitTaskB())
                    .start()
        }
    }

這樣就可以利用系統(tǒng)空閑時(shí)間來延遲初始化任務(wù)了贤笆。

懶加載

懶加載就是有些Task只有在特定的頁面才會(huì)使用蝇棉,這時(shí)候我們就沒必要將這些Task放在Application中初始化了,我們可以將其放在進(jìn)入頁面后在進(jìn)行初始化芥永。

其他方案

提前加載SharedPreferences篡殷,當(dāng)我們項(xiàng)目的sp很大的時(shí)候初次加載很耗內(nèi)存和時(shí)間的,我們可以將其提前在初始化Multidex(如果使用的話)之前進(jìn)行初始化恤左,充分利用此階段的CPU贴唇。

啟動(dòng)階段不啟動(dòng)子進(jìn)程,子進(jìn)程會(huì)共享CPU資源飞袋,導(dǎo)致主CPU資源緊張戳气,另外一點(diǎn)就是在Application生命周期中也不要啟動(dòng)其他的組件如:service、contentProvider巧鸭。

異步類加載方式瓶您,如何確定哪些類是需要提前異步加載呢?這里我們可以自定義classload纲仍,替換掉系統(tǒng)的classload呀袱,在我們的classload中打印日志,每個(gè)類在加載的時(shí)候都會(huì)觸發(fā)的log日志郑叠,然后在項(xiàng)目中運(yùn)行一遍夜赵,這樣就拿到了所有需要加載的類了,這些就是需要我們異步加載的類乡革。

  • Class.forName()只加載類本身及其靜態(tài)變量的引用類
  • new實(shí)例可以額外加載類成員的引用類

總結(jié)

本文主要是講解了啟動(dòng)耗時(shí)的檢測(cè)寇僧,從整體流程的耗時(shí)到各個(gè)方法的耗時(shí)以及線程的耗時(shí),也介紹了工具的選擇和使用沸版,介紹了啟動(dòng)時(shí)間的優(yōu)化嘁傀,異步加載、延遲加載视粮、懶加載等等细办,從常規(guī)方法到更優(yōu)解,講解了很多方式方法蕾殴,希望能給大家提供一些新的思路和解決問題的方式笑撞。也希望大家能在自己的項(xiàng)目中實(shí)戰(zhàn)總結(jié)岛啸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市娃殖,隨后出現(xiàn)的幾起案子值戳,更是在濱河造成了極大的恐慌,老刑警劉巖炉爆,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堕虹,死亡現(xiàn)場離奇詭異,居然都是意外死亡芬首,警方通過查閱死者的電腦和手機(jī)赴捞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來郁稍,“玉大人赦政,你說我怎么就攤上這事∫” “怎么了恢着?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長财破。 經(jīng)常有香客問我掰派,道長,這世上最難降的妖魔是什么左痢? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任靡羡,我火速辦了婚禮,結(jié)果婚禮上俊性,老公的妹妹穿的比我還像新娘略步。我一直安慰自己,他們只是感情好定页,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布趟薄。 她就那樣靜靜地躺著,像睡著了一般典徊。 火紅的嫁衣襯著肌膚如雪杭煎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天宫峦,我揣著相機(jī)與錄音,去河邊找鬼玫鸟。 笑死导绷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的屎飘。 我是一名探鬼主播妥曲,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贾费,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了檐盟?” 一聲冷哼從身側(cè)響起褂萧,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎葵萎,沒想到半個(gè)月后导犹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡羡忘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年谎痢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卷雕。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡节猿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出漫雕,到底是詐尸還是另有隱情滨嘱,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布浸间,位于F島的核電站太雨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏发框。R本人自食惡果不足惜躺彬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梅惯。 院中可真熱鬧宪拥,春花似錦、人聲如沸铣减。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葫哗。三九已至缔刹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劣针,已是汗流浹背校镐。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捺典,地道東北人鸟廓。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親引谜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子牍陌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容