本篇內(nèi)容介紹了“Android啟動(dòng)優(yōu)化的方法是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
創(chuàng)新互聯(lián)是一家專注于成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作與策劃設(shè)計(jì),尖草坪網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)10余年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:尖草坪等地區(qū)。尖草坪做網(wǎng)站價(jià)格咨詢:18982081108
啟動(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)歷的流程:
熱啟動(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ù)流程:
看上面的任務(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)的測量方式
這里主要介紹兩種方式: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)打?。?/p>
=====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文件:
將其設(shè)置給第一個(gè)打開的Activity,如MainActivity:
最后在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 QueuemDelayTasks = 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í)例可以額外加載類成員的引用類
“Android啟動(dòng)優(yōu)化的方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!