先講點題外話
簡述Activity的幾種啟動模式
- standard標準啟動模式,也是Activity的啟動模式,以這種模式啟動的Activity會新new一個Activity對象并放入Activity堆棧,在這種模式下允許一個Activity類有多個實例,并且可互相疊加
- singleTop模式,在一個Activity堆棧中允許存在多個實例,比如啟動一個Activity,如果該Activity不存在,那么就類似standard模式;如果當前堆棧中已經(jīng)存在一個Activity實例,但是不在棧頂,那也會新new一個實例,然后put到棧頂;如果當前已經(jīng)有Activity在棧頂,那就不會再new一個新的Activity,而是直接回調(diào)這個Activity的onNewIntent
- singleTask模式,在一個Activity堆棧中只允許存在一個Activity的實例,比如啟動一個Activity,如果這個Activity不存在,則跟standard模式一樣,生成新的實例,然后put到堆頂;如果這個Activity已經(jīng)存在于棧中,那么會把該Activity之上的Activity都destroy掉,然后把該Activity顯示出來,并回調(diào)onNewIntent方法
- singleInstance模式是只允許有一個實例,而且是運行在自己單獨的一個Activity堆棧中的,并且這個堆棧只允許有這個Activity,不能有其他的Activity
如何計算Activity啟動時間
- 如果你的手機有root過,那么就可以切換到system_process進程查看ActivityManager打印的系統(tǒng)log:
09-15 22:58:51.624 1193-1266/system_process I/ActivityManager: Displayed com.xtc.watch/.view.account.login.activity.WelcomeActivity: +1s150ms (total +4s743ms)
以上打印出了所謂的thisTime和totalTime,thisTime是指當前Activity的啟動時間,正常情況下,如果從桌面啟動一個Activity,那么thisTime==totalTime,但是通常app會有一個不加載布局文件的閃屏頁面,然后再跳轉(zhuǎn)到相應的Activity,這時候thisTime僅僅是代表最后一個Activity的啟動時間,而totalTime還包括而totalTime是指APP進程啟動時長,閃屏頁面的啟動時長以及閃屏頁面的消失,新Activity的啟動時長之和,所以關注APP的啟動時間,我們通常關注的是totalTime
- 通過程序打印log來計算啟動時長,在Application的onCreate方法的第一句開始計算,然后到進入指定Activity的onWindowFocus里面停止計算,這之間的時間差就是啟動耗時
TraceView識別耗時方法
-
對于APP啟動來說,啟動耗時包括Android系統(tǒng)啟動APP進程加上APP啟動界面的耗時時長,我們可做的優(yōu)化是APP啟動界面的耗時,也就是說從Application的onCreate到主界面的onWindowFocusChanged的這一段時間,所以我們可以用Debug.startMethodTracing()和Debug.stopMethodTracing()來抓取這段時間內(nèi)的方法耗時,在手機sd卡根目錄會聲場一個.trace的文件,用monitor的TraceView打開就能看到以下分析圖解:
traceview.jpeg
如上圖,縱軸是各個線程,橫軸是時間,下半部分是函數(shù)執(zhí)行耗時以及函數(shù)堆棧信息,通常分析的時候,我們可以先看下縱軸,跟APP相關的運行線程多不多,如果多的話可能會有線程競爭問題導致主線程卡頓(普通線程的優(yōu)先級和主線程的優(yōu)先級是一樣的,如果線程開太多的話,cpu可能會掛起主線程而先去執(zhí)行其他線程),具體再去看函數(shù)的調(diào)用情況,有幾個指標需要注意下:
- Incl Cpu Time指的是函數(shù)執(zhí)行占用cpu的總時間的百分比和總時間,這里指的總時間是包括函數(shù)里面所有調(diào)用子函數(shù)的耗時之和
- Excl Cpu Time指的是單個函數(shù)執(zhí)行占用cpu的時間,例如函數(shù)a()里面又調(diào)用了函數(shù)b() ,c(),d(),這里的時間僅僅是a的執(zhí)行時間和不包括b,c,d的耗時
- Incl Real Time是函數(shù)實際運行的總時間
- Excl Real Time是函數(shù)自己實際運行的時間,不包括該函數(shù)體內(nèi)調(diào)用的其他子函數(shù)的運行時間
- Call是函數(shù)被調(diào)用的次數(shù),如果一個函數(shù)被調(diào)用的非常多次,那說明這里耶可能存在異常
- 函數(shù)調(diào)用次數(shù)和cpu time以及real time的一些比例信息,這些指標可以看出一個函數(shù)的平均每次執(zhí)行的耗時信息
以上的這些指標都可以排序,而我們就可以很方便的查看到底哪些方法耗時最長,哪些方法被調(diào)用次數(shù)最多,哪些方法平均耗時最大,函數(shù)堆棧信息還可以查看函數(shù)的Parant和Children,Parent代表這個函數(shù)的父函數(shù),也就是說這個函數(shù)被包括在哪一個方法里面,children是指這個函數(shù)體里面又調(diào)用了哪些方法,可以一層一層的跟蹤下去
Systreace識別主線程卡頓問題,View的加載情況
- 用Trace.beginSection和Trace.endSection來抓取這之間的一些信息,具體是用Monitor去抓取一段時間內(nèi)的trace信息,然后會在指定的目錄生成一個html文件,用谷歌瀏覽器輸入chrome://tracing,然后load這個html文件就可以把信息可視化,就像下面一樣:
systrace.jpeg
通常我們先看Alert信息,這里面就是一些警告信息,給你一些提示,在systrace的分析文件中,可以看到有問題的Frame(紅色代表嚴重,黃色代表警告,綠色代表正常),點擊有問題的Frame,可以具體放大查看這個Frame都做了一些什么事情:
frame.jpeg
從圖中可以看到,一個View的measure,layout的耗時情況,inflat xml文件耗時等等,這樣很容易就找到具體哪一個View加載耗時最長,而且這個View的加載耗時都是消耗的哪一個過程中(inflat,measure,layout,draw),還可以看到加載圖片的耗時,問題點找到了,我們就能針對性的去做優(yōu)化了,例如xml布局扁平化,ViewStub的使用,降低圖片的分辨率,做圖片緩存,圖片縮放,設置工作線程的優(yōu)先級為后臺的優(yōu)先級,避免和主線程產(chǎn)生大量的線程進程等等這些問題,總之明確一點:只要問題找到了,那么改起來就簡單了,關鍵是問題的分析過程;Systrace還可以查看UI Thread的執(zhí)行情況,在哪一個時段是處于Running狀態(tài),在哪一個時段是出于Sleeping狀態(tài),如果UI Thread出于Sleeping狀態(tài),那么在這個時間段內(nèi)cpu是在執(zhí)行什么線程,我們就可以考慮是不是可以把這個工作線程延遲執(zhí)行,這樣就能盡可能保證UI Thread大多是Running狀態(tài),而不是斷斷續(xù)續(xù)的,因為frame的刷新頻率一旦低于16ms,那么我們?nèi)庋劬湍芨杏X到界面卡頓,這是一個很不好的體驗,降低卡頓就應該盡量保證frame的刷新頻率控制在16ms以內(nèi),所以這就要求在準備frame的工作執(zhí)行不能超過16ms
造成啟動速度慢的常見原因
- 在Application的onCreate里面做了太多的初始化操作,例如第三方庫的初始化,其實很多第三方庫并不是APP啟動了就馬上需要初始化,我們完全可以用懶加載的方式,等用到了再去初始化也不遲
- 過于復雜的功能邏輯初始化操作,例如賬戶登陸需要去進行網(wǎng)絡請求驗證密碼,驗證通過后再去服務器拉去一大串的賬戶數(shù)據(jù),然后再通過json解析,保存數(shù)據(jù)庫,再刷新到界面,在APP啟動的時候,我們可以先從數(shù)據(jù)庫去搜索數(shù)據(jù),把界面線顯示出來,然后再去請求網(wǎng)絡更新數(shù)據(jù)
- Activity布局層次嵌套過深,xml布局嵌套過深灰導致加載這個布局的時長加大,因為xml布局的繪制是不斷的遞歸遍歷到各個View的根結(jié)點,保證扁平化的布局可以有效的縮短布局加載時間;使用ViewStub,因為ViewStub只要你不調(diào)用inflat,它是不會去加載View的,在Activity啟動后,并不是每一個View都需要馬上加載,有一些View根本是GONE,這些View完全可以用ViewStub來實現(xiàn),等用到的時候再去inflat即可;
- UI線程執(zhí)行太多耗時操作,數(shù)據(jù)庫操作,文件操作,開過多的線程執(zhí)行(用RxJava很容易導致這個問題),JSON解析,Bitmap的加載
- Measure/Layout took a significant time, contributing to jank. Avoid triggering layout during animations(避免在View執(zhí)行動畫過程中出發(fā)View的layout,否則可能會造成卡頓)
- UI Thread的優(yōu)先級是默認優(yōu)先級,而new Thread的優(yōu)先級也是默認的,所以要是有過多的工作線程可能會造成線程競爭,cpu可能掛起UI Thread而去執(zhí)行其他的work thread,所以work thread應該設置為background的級別,降低線程競爭的概率
- 在加載View的過程中不要同時去請求數(shù)據(jù)并更新到View上,在同一時刻做太多的事情也會導致cpu處理不過來而造成卡頓,我們可以等View加載完成之后采取請求數(shù)據(jù)更新,或者在Activity初始化好了之后再去做其他的數(shù)據(jù)更新操作(onWindowFocusChanged)
- 誤以為在Activity的onResume里面去做一些耗時操作可以優(yōu)化Activity的啟動,事實上Activity在執(zhí)行到onResume的時候它的初始化操作還沒有執(zhí)行完成呢,如果在這里面執(zhí)行耗時操作,不會有任何優(yōu)化效果,應該在Activity第一次被focus的時候(onWindowFocusChanged),這時候Activity已經(jīng)是完全初始化好了,你可以試下在onWindowFocusChanged去獲取View的高度是可以獲取到的,但是在onResumen里面去獲取View的高度依然還是0
APP閃屏頁面實現(xiàn)
- 為了實現(xiàn)點擊秒開的效果,我們往往會實現(xiàn)APP閃屏頁面,所謂的閃屏頁面就是一個不加載布局文件的Activity,但是可以設置它的theme里面的window background成啟動歡迎頁面(圖片分辨率不要太大,否則加載時間會比較長),這樣就能達到點擊app,馬上就能看到啟動頁面,由于Activity不用setContentView,所以啟動閃屏頁面的速度也很快,然后再由閃屏頁面跳轉(zhuǎn)到歡迎頁面,然后再進入主界面,其實這樣綜合下來,啟動時間是變長了,因為在Activity之間切換的時候要先pause上一個activity然后再create下一個Activity,這樣會增加一些耗時,不過閃屏頁面給用戶的是點擊了立馬就啟動APP的感覺,所以即時啟動總時長多個兩三秒也是可以接受的
Activity的啟動流程(具體要開源碼)
- Activity或者ContextImpl的startActivity
- Instrumentation的execStartActivity
- ActivityManagerService的startActivity->startActivityAsUser
- ActivityStarter的startActivityMayWait->startActivityLocked->startActivityUnchecked
- ActivityStackSupervisor的resumeFocusedStackTopActivityLocked->resumeFocusedStackTopActivityLocked
- ActivityStack的resumeTopActivityUncheckedLocked->resumeTopActivityInnerLocked
- ActivityStackSupervisor 的startSpecificActivityLocked->realStartActivityLocked
- ActivityManager的scheduleLaunchActivity->handleLaunchActivity->performLaunchActivity->handleResumeActivity到這里一個Activity的啟動流程就基本結(jié)束,太特么復雜了。。。
- 從Activity的啟動流程來分析我們可以得知啟動一個Activity需要去匹配到你要啟動的Activity(匹配ResolveInfo),這里涉及到顯示啟動和隱式啟動,顯示啟動的話比較快,不用再去匹配Intent里面的IntentFilter;然后再監(jiān)測Activity所在的進程是否有啟動,沒有啟動的話就fock一個進程出來接下去再做初始化Application并調(diào)用相關方法,例如onCreate,然后通過反射的方式創(chuàng)建Activity對象,再調(diào)用Activity的各個生命周期;所以APP的啟動時間是包括APP進程啟動時長(無法優(yōu)化),Application的執(zhí)行時間和Activity的執(zhí)行時間(這兩部分是可以優(yōu)化的),另外在啟動Activity之前會設置Theme,這里可能也會造成耗時,例如theme里面設置了一張分辨率較高的background會導致decode這張圖片的時間變長