Android性能优化之启动速度优化
前言:
文本主要會介紹三大塊:
1.簡略介紹APP啟動的完整流程,對整個流程有所了解,才知道在哪里可以進行優化。
2.一些常用的APP啟動優化的方案,主要分為三大塊優化方向。
3.一些不常見的APP啟動優化的方案,甚至包含一些FW層的代碼改動,有的可能是對應用開發者無效的,但是對于車載開發是有用的。
一安卓APP啟動完整流程分析(冷啟動)
圖1:
主要分為三個階段:
1.1 桌面點擊APP圖標,通知到AMS去完成應用進程的創建的流程
1.安卓系統啟動后,拉起的第一個應用是桌面應用。它由SystemServer負責創建,并持有AMS的binder引用。
2.點擊桌面圖標后,Launcher會通過binder通知AMS啟動該APP。
3.AMS會根據傳遞過來的信息查詢APP應用進程在后臺是否存在。如果在后臺,則屬于熱啟動,如果不在后臺,則屬于冷啟動。優化的重點一般都是冷啟動。
4.如果APP進程不存在。AMS主要會做兩件事:
一:AMS首先會讀取對應APP的Manifest信息(此配置信息是存在于AMS中,手機啟動或者應用安裝時讀取到內存中的),然后根據MainActivity的主題設置,讀取其背景圖并展示到屏幕上。
二:通過socket的方式通知Zygote去fork產生APP進程。
如圖2所示:
1.2 應用進程創建后的流程
5.APP進程創建后,會通過執行ActivityThread中的main方法。此方法主要做了兩件事,
第一會進行Looper的初始化;
第二會通過attach方法通知AMS,進行進程的綁定,此時也會把APP創建的binder傳遞給AMS。
6.AMS中完成注冊綁定后,會通過binder通知APP進行application的綁定。APP端binder的接收者是ApplicationThread(下同)。ApplicationThread會被調用bindApplication方法,然后通過handler通知ActivityThread去調用handleBindApplication方法。
7.handleBindApplication方法負責應用初始化的所有流程。主要流程圖如下圖所示:
圖3:
首先,在方法中,通過ContextImpl.createAppContext(this, data.info)去加載DEX文件以及資源。
具體加載DEX的方法在LoadedApk的createOrUpdateClassLoaderLocked方法中:
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {...//生成ClassloadermDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,libraryPermittedPath, mBaseClassLoader,mApplicationInfo.classLoaderName, sharedLibraries, nativeSharedLibraries);mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader);...//把上面生成的mDefaultClassLoader賦值給mClassLoaderif (mClassLoader == null) {mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader,new ApplicationInfo(mApplicationInfo));}}第二,去執行Application的的attachBaseContext方法。
第三,執行installProvider方法,加載App中的各個ContentProvider
第四,執行Application的onCreate()方法
1.3 Activity的首屏展示流程
8.AMS通知APP創建Application后,還會通知APP進程去啟動activity。這時候ApplicationThread接收到之后,會通知ActivityThread完成Activity的啟動流程。這個根據android版本不同有區別,android12之后傳遞的是ClientTransaction。該對象包含一系列事務,對應的會通知構建activity的各個流程。
9.ActivityThread中會分別執行handleLaunchActivity,handleStartActivity,handleResumeActivity等方法,對應的會執行Activity的onCreate,onStart,onResume方法。
10.在handleResumeActivity方法中,執行完resume方法后,判斷如何未關聯到window上,則會把DecorView加到到ViewManager上。
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, String reason) {...if (!performResumeActivity(r, finalStateRequest, reason)) {return;}...if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;if (r.mPreserveWindow) {a.mWindowAdded = true;r.mPreserveWindow = false;// Normally the ViewRoot sets up callbacks with the Activity// in addView->ViewRootImpl#setView. If we are instead reusing// the decor view we have to notify the view root that the// callbacks may have changed.ViewRootImpl impl = decor.getViewRootImpl();if (impl != null) {impl.notifyChildRebuilt();}}if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;wm.addView(decor, l);} else {// The activity will get a callback for this {@link LayoutParams} change// earlier. However, at that time the decor will not be set (this is set// in this method), so no action will be taken. This call ensures the// callback occurs with the decor set.a.onWindowAttributesChanged(l);}}// If the window has already been added, but during resume// we started another activity, then don't yet make the// window visible.} else if (!willBeVisible) {if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");r.hideForNow = true;}...}11.ViewManager的最終實現類是WindowManagerGlobal,在addView(DecorView)時,會創建ViewRootImpl負責后續繪制的完整流程。
12.添加完成后。ViewRootImpl會生成渲染任務TraversalRunnable,在收到垂直信號量之后執行。任務執行完成后,則首屏就顯示在屏幕上了。
二.如何排查啟動卡頓問題
2.1 使用SystemTrace的方式進行分析
2.1.1 如何得到冷啟動總耗時的準確數據
首先的確保殺死應用,adb shell pkill beanlab (包名匹配到beanlab就殺掉)
然后使用 adb shell am start -W [包名]/[包名.Activity] 。
啟動APP,查詢App的啟動時間
查詢結果中,對應的時間參數詳細解析如下:
ThisTime:對應activity啟動耗時;
TotalTime:應用自身啟動耗時 = ThisTime + 應用application等資源啟動時間
WaitTime:系統啟動應用耗時 = TotalTime + 系統資源啟動時間
2.1.2 如何觀察冷啟動耗時的各個時間段的準確數據
通過抓取systrace可以從圖上觀察到 bindApplication ->activityStart->activityResume->Choreographer#doFrame
也可以添加自己的方法抓取,注意開始和結束必須成對出現
Trace.beginSection("你的方法");
Trace.endSection();
2.1.3 如何得到冷啟動結束的回調
首先是display time:從Android KitKat版本開始,Logcat中會輸出從程序
啟動到某個Activity顯示到畫面上所花費的時間。這個方法比較適合測量程序的啟動
時間。
篩選AcctivityManager: Displayed
ActivityManager: Displayed com.beantechs.beanlab/.ui.HomeActivity: +842ms 這個時間 和 adb啟動分析的 TotalTime 一致
2.2 使用matrix框架
三.如何進行啟動優化
第一章時,我們了解了一個APP啟動的完整流程。對這個流程分析一下,我們可以住要分成以下三塊:
1.從用戶點擊圖標,到通知系統去創建APP進程。
2.APP進程創建后,通知AMS并且進行綁定并走Application的所有流程。
3.啟動Activity的流程。
所以如何進行啟動優化,也主要按照這三塊分類去講解。
3.1 優化APP進程創建之前的卡頓問題
這一塊由于主要運行在系統層面,所以我們可優化的點不多。雖然我們不能徹底解決,但是還是可以一定程度上優化用戶的體驗。
3.1.1 通過預制圖進行體驗感覺上的優化。
第一章的時候我們講過,AMS會根據傳遞過來的信息,會在啟動APP進程之前,加載一張APP的背景圖。所以我們可以通過提前加載預制圖,讓用戶感官上知道我們APP已經啟動。目前市場上大的APP都有設置預制圖,比如支付寶的預制圖就是以下這張:
配置方法如下:
在Manifest中,給Main的Activity設置theme即可。
<activityandroid:name=".SplashActivity"android:theme="@style/LoadingAppTheme"tools:replace="android:label"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.DEFAULT" /></intent-filter> </activity> <style name="LoadingAppTheme" parent="Theme.AppCompat.Light.NoActionBar"><!-- Customize your theme here. --><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item><item name="windowNoTitle">true</item><item name="windowActionBar">false</item><item name="android:windowContentOverlay">@null</item><item name="android:listDivider">@drawable/divider_line</item><item name="android:windowFullscreen">true</item><item name="android:windowBackground">@drawable/launcher_previewwindow</item> </style>android12及以上的版本,支持黑白屏動畫形式的展示,具體的使用方式這里就不展開了,原理是一樣的。
將現有的啟動畫面實現遷移到 Android 12 及更高版本?
3.2 優化應用初始化耗時問題
應用初始化的優化,主要其實就是就是對bindApplication這個方法的優化,通過打點日志分析,我們發現其實主要有以下幾個耗時點,也是對應的我們的優化點。
3.2.1 優化APK加載
第一章時我們知道,加載DEX是在ContextImpl.createAppContext()的時候,自然也是啟動主流程上。加載DEX的流程是從APK包中解壓,然后ODEX優化,最后加載到內存當中。自然的,如果DEX越少,那么解壓的就越少,ODEX優化的越少,速度也就越快。
所以我們優化的主要方法是拆分APP,也是通過組件化,插件化的方式去進行加載。
系統需要在啟動的時候讀取哪些內容呢?主要有兩個部分,dex文件和資源文件。所以我們可以把一個很大的APK,按照業務拆分成多個小的APK。其中主APK中的Dex文件和資源弄的很小,其余業務APK放到asset文件夾中。等到APP啟動后,再去解壓加載業務APK。因為主APK很小,所以啟動速度自然就會快得多。而等到應用啟動后,再去通過懶加載的方式,逐漸加載其它模塊的業務APK,因為在后臺加載,所以也不會影響用戶正常的操作,這就是我們組件化啟動優化的方案。
結合實際項目,大多數APP其實都不大,所以并不太需要通過這種模塊化的方案進行優化,所以就不詳細講如何優化了。有興趣的可以看一下Shadow,RePlugin等框架。
3.2.2 ContentProvider優化
通過第一章的圖3我們可以知道,在Application的啟動流程中,會依次執行attachBaseContext,ContentProvider的onCreate,Application的onCreate方法。
所以ContentProvider的onCreate方法中,一定不能有耗時操作,否則會拖慢運行速度。
舉例:
public class XXXProvider{...@Overridepublic boolean onCreate() {//context init...//db initdbUtil = DatabaseUtil.getInstance();dbUtil.open();return true;}@Overridepublic Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {return dbUtil.fetchAll();}... } 我們可以知道,加載數據庫是耗時操作,則我們應該挪到其他步驟當中。解決問題:
ContentProvider的onCreate()是啟動流程當中的,所以我們可以把加載數據庫的耗時操作放到query中,或者等到啟動完成后延時加載。
public class XXXProvider{...@Overridepublic boolean onCreate() {return true;}@Overridepublic Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {if(dbUtil==null){dbUtil = DatabaseUtil.getInstance();dbUtil.open();}return dbUtil.fetchAll();}... }3.2.3?Application中onCreate優化
進行onCreate優化,核心是要梳理onCreate中我們到底做了什么。通過梳理可以發現,onCreate方法中,我們經常要初始化各種框架,比如Bugly.init(),LeakCanary.init()等等,這些框架雖然每個耗時并不多,但是因為是串行執行,累加起來,總耗時反而不少。
舉例:
這里以某款APP為例,我們先查看其systrace文件(該文件在5.2當中)
其中bindApplication方法中有一部分如下圖所示:
圖中顯示有加載多個不同SDK類的操作,而且是串行的結構,所以我們可以推斷,應用在bindApplication的時候做了太多的初始化操作并且并行執行,導致耗時較多,而且bindApplication對應的是我們項目中Application的onCreate方法。
解決問題:
所以我們可以進行以下幾點的優化:
1.部分任務不要主線程執行的,可以挪到子線程執行。
2.有些任務在子線程執行,但是依賴主線程某個任務執行完才可以。這種我們可以等到對應主線任務執行完再把任務加入到子線程池中。
當然,如果相互依賴的邏輯復雜,上面的方式就不太合適了,我們可以使用一個已經封裝好的任務拓撲依賴框架來解決這問題:android-startup
使用簡介:
首先build.gradle中添加依賴:
implementation 'io.github.idisfkj:android-startup:1.1.0'創建待執行的啟動任務類:
class SampleFirstStartup : AndroidStartup<String>() {//是否主線程執行override fun callCreateOnMainThread(): Boolean = true//是否依賴主線程任務override fun waitOnMainThread(): Boolean = false//執行的初始化操作override fun create(context: Context): String? {// todo somethingreturn this.javaClass.simpleName}//依賴哪些其他任務override fun dependenciesByName(): List<String>? {return null}}然后我們在啟動的時候,把這些StartUp組裝一下就可以了。
class SampleApplication : Application() {override fun onCreate() {super.onCreate()StartupManager.Builder().addStartup(SampleFirstStartup()).addStartup(SampleSecondStartup()).addStartup(SampleThirdStartup()).addStartup(SampleFourthStartup()).build(this).start().await()} }或者在manifest中注冊也可以
<providerandroid:name="com.rousetime.android_startup.provider.StartupProvider"android:authorities="${applicationId}.android_startup"android:exported="false"><meta-dataandroid:name="com.rousetime.sample.startup.SampleFourthStartup"android:value="android.startup" /></provider>3.2.4 其它初始化時的耗時操作
1.SharedPreferences涉及到IO操作,所以如果SP存儲數據較大的話,阻塞時間會較長
SharedPreferences sp = getSharedPreferences("1", 0);sp.getString("1", "1");3.3 解決Activity加載問題
3.3.1:主線程不執行耗時操作
通過第一章的流程講解,我們可以知道在第一幀繪制之前,主線程會執行onCreate,onStart,onResume三個方法,所以這三個方法中,一定不能有耗時的方法。
如果一定需要主線程執行的,可以使用IdelHandler的方式解決(IdelHandler會在主線程不忙時執行)。
比如我們在加載數據的時候,就可以通過IdelHandler的方式來代替:
實例如下:
原代碼:
@Override protected void onResume() {super.onResume();...loadData();... }private void loadData() {...組裝請求..發送請求...解析數據... }改成:
@Override protected void onResume() {super.onResume();...MessageQueue.IdleHandler idleHandler = () -> {loadData();return false;};getMainLooper().getQueue().addIdleHandler(idleHandler);... }private void loadData() {...組裝請求...發送請求...解析數據... }3.3.2:預加載頁面
我們通過第二章的工具可以發現,Activity的幾個生命周期中,onCreate方法一般是最為耗時的,而onCreate方法中,最為耗時的一般是setContentView(int)方法。
通過深入閱讀源碼可以可以發現,這其中最為耗時的部分就是把復雜的xml轉化為ViewGroup對象。
這種問題,我們可以通過下面的方案來解決:
1. 使用AsyncLayoutInflater。
2.我可以做一個這樣的操作,在Application的onCreate方法的任務中,添加一個這樣的任務,子線程中把xml轉化為ViewGroup。這樣執行到Activity的onCreate方法中時,我們可以直接使用ViewGroup對象,從而節省了XML解析的時間。(如果布局文件中含有fragment不能采用此方案)
3.轉Compose。Compose的話沒有解析XML的時間,也不受到過多布局層級的影響。
3.3.3:局部加載優先顯示框架或者占位圖
還是通過第二章的工具,我們發現在項目中,measure/layout/draw也是很耗時的(其中一半measure是最耗時的)。這是因為我們布局太復雜了,導致界面渲染的時候需要反復計算,從而耗費時間。
所以針對這種情況,我們可以主要有4個方法來解決:
1.非主框架的部分使用ViewStub加載,等到主框架加載并顯示出來后,再去加載內容的部分。我們經過測算,在第四幀之后,框架是可以完全顯示出來的,所以在第四幀之后進行ViewStub內容的加載,是最為合適的。
2.降低布局層次嵌套和復雜度。復雜布局使用約束布局,盡量少使用weight屬性等等。
3.解決過度繪制問題
4.優先加載占位圖,等到內容顯示好之后,在把占位圖隱藏掉。
3.3.4:預加載數據
頁面加載好了之后,自然就是請求網絡加載數據了。
通過網絡加載數據,大多數都是通過OKHttp進行請求。如果想更快的展示出來并且不在乎有效性,那么可以開啟使用緩存,并且設置緩存有效時間。
但是這樣也有個問題,如果設置了有效期,那么有效期內該請求全部都使用緩存。此時就無法獲取最新的數據了,哪怕非首次請求也不行,因為服務并不會為你單獨開辟一個新的接口。
我們可以實現這樣的一個小需求:“首次請求使用緩存,非首次請求不使用緩存,并且還能把收到的響應更新到首次請求的緩存中”,這樣就能比較好的解決上面所說的問題了。
具體例子可以參考我的另外一篇文章中的6.2。OKHttp原理講解之責任鏈模式及擴展_失落夏天的博客-CSDN博客_android okhttp責任鏈
3.4 其它優化
除了針對我們自身代碼的優化,還有什么別的優化空間嗎?當然有
3.4.1 Baseline Profile
這是google2022年開發者大會新提出的方案。
其核心原理是安卓7.0以后Android支持JIT,AOT并存的混合編譯模式。
兩者各有優勢,JIT即時編譯,雖然運行速度慢,但不需要編譯時間。而AOT需要編譯,后續運行速度快。
一般情況下,首次啟動的時候會使用JIT編譯,因為AOT需要轉換,會導致首次啟動耗時。后續使用的時候,安卓系統會根據使用頻率計算出那些高頻使用的代碼,轉換為AOT的方式進行編譯加載,保證后續這塊代碼的運行速度。
而Baseline Profile就是需要我們自己把這些高頻使用代碼,提前打包到APK中,這樣首次啟動的時候,安卓會通過AOT轉換為ODEX代碼,以后使用這些可執行文件,速度上就會更快。
3.4.2 Hardcoder
這是騰訊開源的一個框架,核心原理是在需要手機性能時,主動通知系統去提升CPU頻率,從而提升手機性能。而不需要性能時,則通知系統降頻,避免手機電量的浪費。
https://github.com/Tencent/Hardcoder
3.4.3?Embryo方案
這是一加手機的一個方案,簡單解釋下就是在后臺預創建一個進程,提前加載好資源。這樣等到這個APP真的啟動的時候,就可以直接使用,而不是重新創建了。
這里其實我有一個更簡單有效的方案,我們知道,APP冷啟動的時候,是System_server通知Zygote去fork應用進程的,應用進程創建后,再回掉通知System_server進程。至少在這段時間內,是沒有綁定任何應用層信息的,也就是下圖紅框中的部分。
?所以,就像APP中的預加載一樣,我們為什么不能在系統層預加載一個APP進程呢?等到真的有APP應用創建需求的時候,直接去使用這個APP進程,而不是走創建流程創建一個。因為走的是socket通信,以及fork進程需要時間,所以整個流程有可能可以節省多達100ms的流程。這個方案還在調研探索中,目前理論上是可行的。
3.4.4?Redex方案
這個是facebook提出的一個方案,其實核心本質和proguard有一些類似,混淆字節碼,讓加載的DEX文件變的更小,則加載變得就更快。redex還有一個突出的亮點就是除了混淆之外,還能做一定的字節碼層面的優化,比如A調用B,B調用C的場景,直接改成A調用C,這樣減少了一層方法棧,執行速度肯定是更快的。
當然,這個的目標就不單純的只是解決啟動速度了,而是讓APP運行的更快樂。
但是這個方法已經已經推出了很久了,到目前為止并不是很流行,其原因也是這樣的操作容易引起各種各樣的問題。
四.聲明和備注
4.1 聲明
文章中的原理和資料來自于網上搜索的資料,以及針對安卓源碼的調試所得。基于的安卓版本是12。如有描述不準確的地方,或者好的建議,歡迎指出來。
總結
以上是生活随笔為你收集整理的Android性能优化之启动速度优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis集群之多主多从
- 下一篇: 短信生成器部分思路----Android