近來對之前做優(yōu)化學(xué)習(xí)記錄的一些知識點進(jìn)行了以下簡單的總結(jié),主要集中在以下幾個方面:
1.Systrace
2.嚴(yán)格模式
3.非保護(hù)性廣播
4.Event Log 中的性能問題
5.幀率優(yōu)化
6.冷啟動流程
7.其他常見的優(yōu)化技巧
8.ANR
1、Systrace
- 截至目前為止,最實用、分析最精準(zhǔn)、最專業(yè)的工具,但上手門檻高
- Systrace分析重經(jīng)驗,平時養(yǎng)成經(jīng)常看systrace分析運行時間、線程,CPU工作頻率,CPU 搶占等習(xí)慣。
- TraceView雖然也能分析函數(shù)執(zhí)行時間,但相比systrace有諸多劣勢,且對于密集函數(shù)呼叫可能會高估執(zhí)行時間,對跨進(jìn)程調(diào)用可能會低估執(zhí)行時間,無法分析出并發(fā)的影響。故不能作為性能分析的主要工具,且常會誤導(dǎo)解決方向。適合在systrace分析后,想進(jìn)一步找線索時使用
建議配置
- Trace duration:3 ~ 10 秒
- Trace Buffer Size (kb):16384
- Commonly Used Tags:用默認(rèn)
一般情況除非需要看WebView內(nèi)部運作細(xì)節(jié),不然WebView也可以關(guān)掉
- Advanced Options
必選:CPU Frequency、CPU Idle
其他按需
2、嚴(yán)格模式
嚴(yán)格模式能查出跨進(jìn)程通訊而產(chǎn)生的磁盤讀寫,優(yōu)于代碼評審
解決掉嚴(yán)格模式問題有什么好處
- 減緩手機(jī)越用越慢或是突然卡頓的問題
- 提升應(yīng)用啟動時間及減少ANR發(fā)生概率。
數(shù)據(jù)庫存取時間長有一大部分來自以下三種原因:
- 數(shù)據(jù)庫資料持續(xù)增加,造成查詢/新增/刪除/修改時間拉長
- 數(shù)據(jù)庫存取遭遇其他應(yīng)用同時存取磁盤,而拉長完成時間
- 等待被其他應(yīng)用以同步鎖鎖住的數(shù)據(jù)庫
跨進(jìn)程調(diào)用產(chǎn)生的嚴(yán)格模式問題的日志樣式
- duration: 嚴(yán)格模式造成的時間延遲
- 可由堆棧第一個項判斷嚴(yán)格模式問題種類
- 在堆棧中出現(xiàn)“# via Binder call with stack”字樣代表該嚴(yán)格模式問題來自為跨進(jìn)程調(diào)用
StrictModepolicy violation; ~duration=30ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=22085639 violation=2
at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1296)
at android.database.sqlite.SQLiteConnection.applyBlockGuardPolicy(SQLiteConnection.java:1044)
…
at android.content.ContentResolver.query(ContentResolver.java:562)
at com.android.internal.telephony.ISub$Stub.onTransact(ISub.java:124)
at android.os.Binder.execTransact(Binder.java:582)
# via Binder call with stack:
android.os.StrictMode$LogStackTrace
at android.os.StrictMode.readAndHandleBinderCallViolations(StrictMode.java:1963)
at android.os.Parcel.readExceptionCode(Parcel.java:1665)
…
at android.app.ActivityThread.main(ActivityThread.java:6470)
…
將磁盤存取移到子線程執(zhí)行。但須注意以下子線程的設(shè)置:
- 建議以HanderThread或AsyncTask,讓子線程中的磁盤讀取以隊列方式執(zhí)行
- AsyncTask優(yōu)先級設(shè)置默認(rèn)是Process.THREAD_PRIORITY_BACKGROUND。假如以默認(rèn)的優(yōu)先級執(zhí)行使用AsyncTask讀取重要性高的資料時,其將被分配到較少比例的處理器資源,而影響處理速度。建議處理重要資料時,先調(diào)整AsyncTask的優(yōu)先級為Process.THREAD_PRIORITY_DEFAULT。
- 數(shù)據(jù)庫存取可考慮利用AsyncQueryHandler簡化子線程存取數(shù)據(jù)庫實作。AsyncQueryHandler的默認(rèn)優(yōu)先級設(shè)置已經(jīng)是Process.THREAD_PRIORITY_DEFAULT,不需更動
- 非必要的話,避免以直接以new Thread()來執(zhí)行磁盤存取。這方法容易在高并發(fā)運行時,產(chǎn)生大量線程拖慢系統(tǒng)速度
常見該解決的問題:在主線程調(diào)用SharedPreferences的commit()
Process: com.android.browser:service
Duration-Millis: 59
android.os.StrictMode$StrictModeDiskReadViolation: policy=647 violation=2
at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
at libcore.io.BlockGuardOs.access(BlockGuardOs.java:67)
at java.io.File.doAccess(File.java:283)
at java.io.File.exists(File.java:363)
at android.app.SharedPreferencesImpl.writeToFile(SharedPreferencesImpl.java:567)
at android.app.SharedPreferencesImpl.access$800(SharedPreferencesImpl.java:51)
at android.app.SharedPreferencesImpl$2.run(SharedPreferencesImpl.java:512)
at android.app.SharedPreferencesImpl.enqueueDiskWrite(SharedPreferencesImpl.java:533)
at android.app.SharedPreferencesImpl.access$100(SharedPreferencesImpl.java:51)
at android.app.SharedPreferencesImpl$EditorImpl.commit(SharedPreferencesImpl.java:455) //commit
at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider.b(RQDSRC:275)
at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider.a(RQDSRC:158)
at com.tencent.mtt.widget.androidwidget.QBAppWidgetProvider$1.handleMessage(RQDSRC:137)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:147)
at android.app.ActivityThread.main(ActivityThread.java:5451)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:970)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:765)
使用SharedPreferences.Editor.apply() 取代SharedPreferences.Editor.commit() 以避免在主線程上寫入磁盤
- commit()直接產(chǎn)生磁盤存取于執(zhí)行的線程中,會產(chǎn)生嚴(yán)格模式問題
- apply()則產(chǎn)生子線程來執(zhí)行磁盤存取
常見該解決的問題:主線程調(diào)用SQLite相關(guān)操作
Process: com.android.phone
Duration-Millis: 44
android.os.StrictMode$StrictModeDiskReadViolation: policy=647 violation=2
at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1137)
at android.database.sqlite.SQLiteConnection.applyBlockGuardPolicy(SQLiteConnection.java:1046)
at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:847)
at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:836)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:144)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:133)
at android.content.ContentResolver.query(ContentResolver.java:506)
at android.content.ContentResolver.query(ContentResolver.java:426) //query
at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.onApnChanged(DataStatusNotificationService.at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.access$300(DataStatusNotificationService.java:at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService$DataSettingsObserver.onChange(DataStatusNotification
at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.enableContentObservers(DataStatusNotificationService
at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService.access$000(DataStatusNotificationService.java:at com.qti.qualcomm.datastatusnotification.DataStatusNotificationService$1.onQcRilHookReady(DataStatusNotificationService.at com.qualcomm.qcrilhook.QcRilHook$6.onServiceConnected(QcRilHook.java:1513)
at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1241)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1258)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:147)
at android.app.ActivityThread.main(ActivityThread.java:5451)
3、非保護(hù)性廣播
Android 針對有一些廣播只能由系統(tǒng)發(fā)送的,并且提供了<protected-broadcast> 標(biāo)記讓系統(tǒng)級應(yīng)用在AndroidManifest.xml明確宣告。在系統(tǒng)運作起來之后,如果某個不具有系統(tǒng)權(quán)限的應(yīng)用試圖發(fā)送“保護(hù)性廣播”,AMS會拋出異常,提示"Permission Denial: not allowed to send broadcast"。
- 系統(tǒng)級應(yīng)用指的是Persistent 應(yīng)用或user id 為SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID、NFC_UID的應(yīng)用。
- 反之,系統(tǒng)級應(yīng)用發(fā)出的broadcast 必須宣告成”保護(hù)性廣播”,否則會觸發(fā)system server 記錄WTF 時間而產(chǎn)生寫入dropbox的磁盤存取,間接導(dǎo)致應(yīng)用的主線程卡頓
- 例外:凡是由谷歌、高通源代碼定義發(fā)送的廣播,即使有非保護(hù)性廣播問題,也不處理
StrictModepolicy violation; ~duration=236 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=22085639 violation=1
at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1256)
…
at com.android.server.am.ActivityManagerService.addErrorToDropBox(ActivityManagerService.java:14477)
…
at android.util.Log.wtf(Log.java:300)at android.util.Log.wtf(Log.java:290)
at com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:18473)
…
# via Binder call with stack:
…
at com.android.internal.telephony.imsphone.ImsPhone.sendImsNetworkStateBroadcast(ImsPhone.java:1568)
…
解決方法:在應(yīng)用的AndroidManifest.xml明確宣告成保護(hù)性廣播
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv=http://schemas.android.com/apk/prv/res/android
package="com.android.server.telecom"
coreApp="true"
android:sharedUserId="android.uid.system”>
…
<protected-broadcast android:name="android.intent.action.SHOW_MISSED_CALLS_NOTIFICATION" />
…
</manifest>
4、Event Log 中的性能問題
Log 格式可查詢/system/etc/event-log-tags
20003 dvm_lock_sample
(process|3),(main|1|5),(thread|3),(time|1|3),(file|3),(line|1|5),(ownerfile|3),(ownerline|1|5),(sample_percent|1|6)
52002 content_query_sample
(uri|3),(projection|3),(selection|3),(sortorder|3),(time|1|3),(blocking_package|3),(sample_percent|1|6)
52003 content_update_sample
(uri|3),(operation|3),(selection|3),(time|1|3),(blocking_package|3),(sample_percent|1|6)
52004 binder_sample
(descriptor|3),(method_num|1|5),(time|1|3),(blocking_package|3),(sample_percent|1|6)
- 查詢是否有運行時間長的binder transaction
I/binder_sample( 5245): [com.android.internal.appwidget.IAppWidgetService,16,588,com.android.systemui,100]
- 查詢是否有運行時間長的content provider 操作(可能還有bug)
I/content_query_sample(16701): [content://call_log/calls?allow_voicemails=true,_id/number/voicemail_uri,new = 1
AND type = ?,date DESC,906,com.android.contacts,100]
- 查詢是否等待synchronized 太久
I/dvm_lock_sample( 922): [system_server,1,PowerManagerService,523,PowerManagerService.java,1787,-,858,100]
- 界面啟動時間:am_activity_launch_time
1551 1573 I am_activity_launch_time: [0,248545817,com.android.browser/com.android.browser.BrowserLauncher,4432,4432]
//倒數(shù)第二個參數(shù)就是框架觸發(fā)這個界面啟動直到應(yīng)用畫完第一幀的時間
界面啟動時間過長,不僅使用者會感到卡頓,嚴(yán)重會導(dǎo)致Input/Key dispatch timeout ANR
代碼優(yōu)化要求:在一般功能性測試中,需在800ms 內(nèi)完成。以systrace分析問題,找出主要耗時點才進(jìn)行優(yōu)化。如:
- 簡化布局
- 簡化在主線程的長執(zhí)行時間的代碼
- 將磁盤讀寫移到子線程
- 若需要做跨進(jìn)程通訊,盡量移到子線程
- 數(shù)據(jù)庫查詢時間:content_query_sample
- 當(dāng)數(shù)據(jù)庫查詢時間> 500ms 會印出
- 當(dāng)數(shù)據(jù)庫查詢時間< 500ms 則隨機(jī)印出

- 跨進(jìn)程通信執(zhí)行時間:binder_sample
- 當(dāng)執(zhí)行時間> 500ms 會d印出
- 當(dāng)執(zhí)行時間< 500ms 則隨機(jī)印出

如何找到接口編號對應(yīng)的函數(shù)?(以ITelephony的接口編號40為例)
- ROM 編譯完后,所有AIDL 編譯出的Java 代碼,放在out/target/common/obj下。直接在該文件夾以及子文件夾查找ITelephony.java 文件:
1、打開ITelephony.java文件
2、找到FIRST_CALL_TRANSATION位置
3、所有的接口編號都由IBinder的第一個接口編號1號起算。在本例中,要找android.os.IBinder.FIRST_CALL_TRANSATION+39
- 某些模塊可能自行撰寫B(tài)inder 調(diào)用,不使用AIDL。此種情況需要從模塊代碼尋找。例如frameworks/base/core/java/android/app/IActivityManager.java



- 同步鎖等待時間:dvm_lock_sample,主線程不要被同步鎖卡住超過50ms。
- 等待時間> 500ms 會印出
- 等待時間< 500ms 則隨機(jī)印出

5、幀率優(yōu)化
- 屏幕刷新率(Refresh):屏幕內(nèi)在一秒刷新屏幕的速度
- 手機(jī)一般要求60Hz
- 幀率(Frame Rate):軟件系統(tǒng)一秒繪制的幀數(shù)
- 不同步問題
- 幀率 > 刷新率:導(dǎo)致畫面撕裂(screen tearing) 問題,有兩個或以上幀顯示在同一個frame buffer上
- 刷新率 > 幀率:如果畫面銜接不夠連續(xù),就會感覺卡頓。如果幀率= ? 刷新率,穩(wěn)定輸出,感覺就沒那么明顯

- V-sync (Vertical Synchronization)
- 保證屏幕刷新過程,內(nèi)容不會變更
- 屏幕刷新時,只從frame buffer 取
- Vsync信號來時,才觸發(fā)上層軟件刷新back buffer
- 不管MDP/GPU 多快,都應(yīng)適應(yīng)屏幕刷新的速度
- Back buffer 可以超過一個
1000ms/60 frames = 16.666 ms / frame
//在Vsync機(jī)制下,想保持流暢的體驗,每幀必須在16.6ms 內(nèi)完成
-
SurfaceFlinger
image.png Choreographer
- 工作流程:請求Vsync→ 收到Vsync→ 請求Vsync→ 收到Vsync
- 如果沒有再次請求Vsync,則無法收到

- Rasterization(光柵化)

- Display List
- 記錄(Record) 一個View 的繪制需求
- 在HWUI內(nèi)部處理,還需要進(jìn)行其他的預(yù)處理,例如判別顯示區(qū)域、產(chǎn)生HWlayer 等,才能轉(zhuǎn)換變成OpenGL 指令
- 只要View的屬性/內(nèi)容不變,就不需要重新產(chǎn)生Display List
改變的時機(jī)、頻率至關(guān)重要,多余的view invalidate、重新measure/layout 等,都會觸發(fā)display list改變(CPU運算)。有些對應(yīng)的OpenGL會相當(dāng)耗時,例如重新upload texture、重新產(chǎn)生HW layer 等。

- GPU 呈現(xiàn)模式分析
雖然工具叫GPU 呈現(xiàn)模式分析,但其實顯示的內(nèi)容不只是GPU 工作時間

GPU 呈現(xiàn)模式分析用在什么場合
- 對解決問題而言,基本沒用。信息太概略,無法知道問題在哪里,Systrace才能分析根因
- 以發(fā)現(xiàn)問題的角度而言,容易造成測試與研發(fā)誤解,花時間優(yōu)化不重要的事
- 有些幀處理的時間稍長,未必導(dǎo)致卡頓。例如第一幀一般準(zhǔn)備工作比較多。中間偶爾掉一幀,其實用戶看不出來
- 應(yīng)用只要有界面刷新,工具就會顯示長條。不容易看出滑動、動畫播放精準(zhǔn)的開始與結(jié)束點
在Triple Buffer 架構(gòu)下,應(yīng)用即使掉幀,也可能還有l(wèi)ayer buffer 能畫

上面的流程仔細(xì)體會。
- 硬件加速與渲染線程
UI 線程
- 每個View 透過draw(),將繪制的需求,透過DisplayListCanvas,記錄在RenderNode里的DisplayList
渲染線程:兼顧性能優(yōu)化、抽象層次
- Defer: 對RenderNode里DisplayList的渲染需求,進(jìn)行前處理,主要是進(jìn)行Batch與Merge,產(chǎn)生一群BatchBase,對應(yīng)一群渲染操作,減少后續(xù)GL draw call
- FrameBuilder:管理每幀的繪制任務(wù),調(diào)用各個LayerBuiler進(jìn)行
- LayerBuilder:管理每幀中,某一層的繪制任務(wù)。可能對應(yīng)目前的Surface 或一個FBO。將BatchBase透過BakedOpDispatcher調(diào)用到BakedOpRender、RenderState,再透過Glop真正執(zhí)行OpenGL 指令


以上的流程可以通過在systrace上加深體會。
- 避免過度繪制
- 避免大面積的過度繪制
- 去掉WindowBackground
- 若自行實現(xiàn)復(fù)雜的View,可在onDraw內(nèi),還可透過下列幾種方法,減少過度繪制
使用Canvas.clipRect(float left, float top, float right, float buttom) :設(shè)置顯示范圍
image.png- 使用Canvas.quickReject(float left, float top, float right, float bottom, EdgeTypetype) :若回傳true,代表參數(shù)所指定的矩形區(qū)域,完全在目前的canvas clip 外。此時就可忽略那些對應(yīng)的渲染需求。
- 使用Hardware Layer
適用時機(jī):布局復(fù)雜ViewGroup的大面積移動、移動過程中不會改變內(nèi)容
- 觀念:將每幀做的復(fù)雜繪制運算,提前在動畫啟動前做一次
- 避免誤用:產(chǎn)生hardware layer是耗時操作,不應(yīng)在動畫過程中改變或產(chǎn)生hardware layer。例如不應(yīng)使用在ListViewitem
// 啟動動畫時設(shè)為hardware layer
viewGroup.setLayerType(View.LAYER_TYPE_HARDWARE, null);
// 動畫結(jié)束時,記得關(guān)掉hardware layer,釋放內(nèi)存
viewGroup.setLayerType(View.LAYER_TYPE_NONE, null);
// View animation 過程中開啟hardware layer
ViewPropertyAnimator.alpha(0.5f).withLayer();
- 使用hardware layer必須保證動畫過程沒有內(nèi)容更新,否則會造成掉幀
- 開發(fā)者選項 > 顯示硬件層更新
只要hardware layer 產(chǎn)生或變更內(nèi)容,就會有綠色的閃爍
- Alpha 效果優(yōu)化
- View alpha 屬性的影響
必須先知道下層的元素是什么,再結(jié)合這個View進(jìn)行混色處理- 盡可能避免大面積的alpha動畫
Alpha 效果常伴隨過度繪制的問題- 減少alpha效果的影響
setAlpha()會造成硬件加速呼叫saveLayer()進(jìn)行復(fù)雜操作
呼叫setAlpha()的View優(yōu)化方向
- 不做其他繪制,例如設(shè)定background、復(fù)寫onDraw()
- 復(fù)寫hasOverlappingRendering()并回傳false
- 產(chǎn)生hardware layer
- ListView優(yōu)化
- 較大圖片的加載,不適合在每個item的getView()中讀取,會導(dǎo)致掉幀。一般解決做法是:
在子線程加載圖片。
若滑出這頁,則取消對應(yīng)的圖片加載,停止滑動時,才將圖片設(shè)進(jìn)ImageView。
6、冷啟動流程
- 前一個應(yīng)用的pause 算在啟動時間內(nèi)。如果是跨進(jìn)程啟動,pause 超過500ms 就會觸發(fā)pause timeout,強(qiáng)迫畫面切換。
- am_activity_launch_time只計算到IdleHandler觸發(fā)時的時間,并非完整的界面加載完時間。
- 從startActivity開始,歷經(jīng)前一個activity的pause,到下一個activity畫出第一個畫面的時間。故又稱display time。
- 若第一個activity用于跳轉(zhuǎn),也就是onCreate執(zhí)行完就startActivity并finish自己,則會計算到下一個activity 畫出第一個畫面的時間。
- am_activity_fully_drawn_time
- 應(yīng)用可自行在數(shù)據(jù)加載完,調(diào)用Activity.reportFullyDrawn(),告訴AMS 真正執(zhí)行完的時間,供自己調(diào)試用
- 這個跨進(jìn)程調(diào)用可能會增加不必要的卡頓,建議在子線程調(diào)用。
- 提升啟動速度的方法:減少插入主線程執(zhí)行的handler、減少跨進(jìn)程通信、減少下面幾個重點函數(shù)的執(zhí)行時間。
- 在Application.onCreate() 初始化太多模塊,拖慢進(jìn)程啟動時間。
- 在onCreate()、onResume()中post Message、Runnable到主線程,導(dǎo)致畫面繪制的Handler 被推遲執(zhí)行
- 不同模塊重復(fù)調(diào)用同樣的耗時接口,如AMS、WMS 接口。
- 在主線程調(diào)用數(shù)據(jù)庫的讀、寫或執(zhí)行bindService、provider 等需要等待其他線程的操作。
- 在主線程調(diào)用registerReceiver等容易被AMS耽誤執(zhí)行時間的接口。
- 在主線程初始化第三方SDK,而第三方SDK又有耗時操作。

7、其他常見的優(yōu)化技巧
- 用System.arraycopy() 取代循環(huán)語句賦值
final int size = 1000000;
int[] array1 = new int[size];
int[] array2 = new int[size];
// 注意:size 大時,才有明顯效果
System.arraycopy(array1, 0, array2, 0, size);
//for (inti= 0 ; i< size ; ++i)
// array2[i] = array1[i];
- ArrayList、Vector、HashMap、HashSet等,可以在初始化時,根據(jù)程序可能面對的數(shù)據(jù)量,指定初始容量,避免過程中的內(nèi)存重新分配與內(nèi)容復(fù)制。
ArrayList<Foo> a = new ArrayList<Foo>(20);
- 安卓不適合使用SoftReference。對象幾乎不會被GC,反而導(dǎo)致內(nèi)存問題
因為 SoftReference 無法提供足夠的信息可以讓 runtime 很輕松地決定 clear 它還是 keep 它。 Android推薦使用android.util.LruCache做Cache管理。
- 減少GC
- 避免在關(guān)鍵路徑中,頻繁分配內(nèi)存,以減少GC 可能帶來的卡頓。例如:View.onDraw()中新建新對象。
過度采用這個策略,將對象宣告為static,導(dǎo)致兩個問題:
- 內(nèi)存無法釋放:有些Java對象乍看雖小,但native 分配的內(nèi)存卻不小,例如將正則表達(dá)式Pattern宣告為static。
- static對象在類加載時就會初始化,即使最后代碼沒走到,也會多花執(zhí)行時間。
- 針對需要頻繁分配、釋放的小對象,采取Object Pool 方式優(yōu)化,減少虛擬機(jī)分配內(nèi)存時間、減少GC
- AsyncTask陷阱
- AsyncTask與Activity、Fragment 的生命周期不同步,不隨著onDestroy而消滅
- 若執(zhí)行任務(wù)長,可能會導(dǎo)致連續(xù)進(jìn)出Activity、Fragment 的情況,有太多線程運行,導(dǎo)致卡頓。更壞情況,例如用AsyncTask執(zhí)行非常耗時的ContentProviderquery,可能連續(xù)進(jìn)出幾次,就把provider 進(jìn)程的Binder 線程全占滿
- 可能onPostExecute() 執(zhí)行時,前一個Activity/Fragment 已經(jīng)銷毀,現(xiàn)在運行的,可能需要的數(shù)據(jù)不同。處理不好容易導(dǎo)致功能異常
- 自己撰寫的AsyncTask,若不是static 類,隱性持有外部的Activity、Fragment,會導(dǎo)致他們無法被GC
- AsyncTask產(chǎn)生的子線程,默認(rèn)的優(yōu)先級是THREAD_PRIORITY_BACKGROUND。若此任務(wù)需要盡快執(zhí)行,則這個優(yōu)先級會導(dǎo)致執(zhí)行完的時間不確定
- AsyncTask以ThreadPoolExecutor實現(xiàn),每個實例會產(chǎn)生線程池。應(yīng)用中過多的AsyncTask實例,會導(dǎo)致線程過多
- 避免應(yīng)用在后臺執(zhí)行不必要的任務(wù)
- 在后臺,仍然觸發(fā)畫面刷新
- 同一個應(yīng)用,多個Receiver 注冊同一個intent,如SCREEN_ON
- ContentObserver監(jiān)聽到Uri 變化后,就在后臺馬上query
- 舉例:遇到聯(lián)系人同步的情況,就是系統(tǒng)性災(zāi)難
- 應(yīng)該等到應(yīng)用回到前臺,才query
- 使用getInstalledApplications()、getInstalledPackages() 等類似的函數(shù),讀取各應(yīng)用的字符串,觸發(fā)大量I/O、CPU運算
- 線程優(yōu)先級不能亂設(shè)定
public class Process {
public static final intTHREAD_PRIORITY_DEFAULT = 0;
public static final intTHREAD_PRIORITY_LOWEST = 19;
public static final intTHREAD_PRIORITY_BACKGROUND = 10;
public static final intTHREAD_PRIORITY_FOREGROUND = -2;
public static final intTHREAD_PRIORITY_DISPLAY = -4;
public static final intTHREAD_PRIORITY_URGENT_DISPLAY = -8;
public static final intTHREAD_PRIORITY_AUDIO = -16;
public static final intTHREAD_PRIORITY_URGENT_AUDIO = -19;
public static final intTHREAD_PRIORITY_MORE_FAVORABLE = -1;
public static final intTHREAD_PRIORITY_LESS_FAVORABLE = +1;
}
- 設(shè)成THREAD_PRIORITY_BACKGROUND、LOWEST 等,必須是不緊急的任務(wù),不要求在UI立即顯示。
- 應(yīng)用不允許設(shè)成比THREAD_PRIORITY_DEFAULT 更高的優(yōu)先級,會影響自身與系統(tǒng)性能、搶占CPU、導(dǎo)致許多莫名的時序問題出現(xiàn)。
- View 層級簡化
- ViewStub
- 分頁加載(處理Tab、ViewPager的情況)
原始做法:一次性產(chǎn)生所有Fragment 與其下所有的View。啟動性能差、且剛啟動時由于大量UI 在初始化化,主線程過多任務(wù),應(yīng)用容易卡頓
- 改進(jìn)方法1:看不到的Fragment,推遲到開始滑動到那個Fragment 才產(chǎn)生
- 改進(jìn)方法2:Fragment 先產(chǎn)生,但View 推遲到開始滑動過去那個Fragment 才inflate、measure、layout
- 改進(jìn)方法3:Fragment 先產(chǎn)生,而View 在子線程inflate。若有多個Fragment,則依照與目前可視的Fragment,由近向遠(yuǎn)依次產(chǎn)生、或滑動到相鄰頁才產(chǎn)生。但inflate 后,不能馬上加入View階層,否則馬上會觸發(fā)在UI線程的measure與layout,導(dǎo)致卡頓。等到要開始滑動時,才加入View 階層。
- Constraint Layout
- Bitmap
常是內(nèi)存問題的來源
- 全屏ARGB 圖片:1920 x 1080 x 4 = 8294400 = 8 MB
- 1600萬像素照片:4608 x 3408 x 4 = 62816256 = 60MB
常是性能問題的來源
- 在UI線程加載大圖片
- 加載比實際顯示區(qū)域大的圖片
- 顯示區(qū)域與圖片大小不符
使用LRU (Least Recently Used) Cache
- 數(shù)據(jù)庫優(yōu)化
- 常運行、或短時間內(nèi)高頻度運行的類似語句,可以使用prepared statement,減少每次都要重新編譯SQL 語句再執(zhí)行的時間
- 減少transaction 次數(shù)
- bulkInsert() 的實現(xiàn),依賴自行控制transaction 提升性能
- 使用applyBatch(),能一次將不同種類的操作,打包處理
安卓默認(rèn)的Provider.applyBatch() 實現(xiàn),僅是一個個執(zhí)行,并未使用單次transaction 提升性能。
- applyBatch() 或bulkInsert() 單次調(diào)用,需要有筆數(shù)上限,筆數(shù)太多,需要拆分多個批次執(zhí)行。
- 單次binder transaction 允許傳遞的內(nèi)存,避免應(yīng)用發(fā)生transaction fail
- 鎖住數(shù)據(jù)庫的時間太長,會導(dǎo)致其他應(yīng)用無法訪問
- 建議:
- 傳遞數(shù)據(jù)量:不超過128 KB
- 執(zhí)行時間:不超過100ms
- 經(jīng)驗:一般應(yīng)用,一次小于50筆數(shù)據(jù)
- SQLite的index

- CPU調(diào)度
cpuctl (Android N 及之前默認(rèn)開啟)
控制不同線程之間搶占 CPU 的分配
- Android以 cpuctl 機(jī)制,設(shè)計了 foreground scheduling 與 background scheduling,當(dāng)兩種線程同時競爭 CPU 資源時,采95:5 的方式,優(yōu)先分配給 foreground scheduling
- OOM_ADJ <= 1 的安卓應(yīng)用:Foreground cpuset
- 其他安卓應(yīng)用:Background cpuset
8、ANR
- 三種ANR
- Input (key) dispatching timeout:System_server 送出輸入事件到某窗口后,應(yīng)用無法在 5秒內(nèi)完成,讓框架通知 System_server 此次輸入事件已處理完成。若在應(yīng)用啟動過程,在畫出第一幀(也就是 am_activity_launch_time 出現(xiàn)之前)窗口是無法響應(yīng)輸入的。
- Service timeout:System_server 送出 intent 到應(yīng)用啟動 service,過程中可能包含進(jìn)程啟動、應(yīng)用執(zhí)行 onCreate() + onStartCommand(),總時間無法在 X 秒內(nèi)完成并通知System_server
- 應(yīng)用在前臺時: X = 20 秒
- 應(yīng)用在后臺時: X = 200 秒
- Broadcast timeout: System_server 送出 intent 到應(yīng)用,過程中可能包含進(jìn)程啟動、應(yīng)用
執(zhí)行 onReceive(),總時間無法在 Y 秒內(nèi)完成并通知 System_server
- Foreground broadcast:Y = 10 秒
- 其他 broadcast:Y = 60 秒
假設(shè)在某段時間執(zhí)行如下:

從中可以看到導(dǎo)致ANR發(fā)生的原因可能并不是因為某個特定的服務(wù)或廣播時間超過限制了,而是由于主線程賽車導(dǎo)致的整體的時間的延長。
所以有一個基本觀念就是:
- 主線程運行任務(wù)多、執(zhí)行時間長,導(dǎo)致 ANR 的可能性就越大
- ANR 解決的是主線程塞車的問題
由上可知,ANR是比較難解且無法從 log 得知為何執(zhí)行突然變慢。到底是數(shù)據(jù)量大、還是被其他進(jìn)程卡住了、還是CPU 被其他應(yīng)用搶占了、還是系統(tǒng)關(guān)核限頻了,目前的解法就是:
- 抓 systrace
- 錯誤的線程優(yōu)先級設(shè)定
就是優(yōu)先級倒轉(zhuǎn)問題,使得主線程等待子線程執(zhí)行
- 若子線程設(shè)成 THREAD_PRIORITY_BACKGROUND,則更有可能導(dǎo)致 CPU 資源被搶走。故無法避免情況,至少要將子線程設(shè)成 THREAD_PRIORITY_DEFAULT
- AsyncTask 默認(rèn)的優(yōu)先級是 THREAD_PRIORITY_BACKGROUND,需要特別留意
下面看一個優(yōu)先級倒轉(zhuǎn)的例子

上圖中T1最先開始運行并獲得了R鎖,之后T2、T3、T4也都運行,之后T3、T4等待R鎖,此時由于T1優(yōu)先級比較低,按照上面我們講的95:5的CPU分配比例,不需要R鎖的T2,就開始搶占T1運行的時間,導(dǎo)致T3、T4的等待時間將完全取決于低優(yōu)先級的T2,如果此時還有更多的低優(yōu)先的線程在搶占資源那么T3、T4的執(zhí)行將會很糟糕。
看段Log從中可以看到一些線程的設(shè)置信息
"OnLineMonitor" prio=5 tid=17 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x12e41dc0 self=0xe61fd200
| sysTid=14587 nice=0 cgrp=default sched=0/0 handle=0xcf3fb970
| state=S schedstat=( 303078149 1485237159 1965 ) utm=23 stm=7 core=2 HZ=100
| stack=0xcf2f9000-0xcf2fb000 stackSize=1038KB
| held mutexes=
kernel: (couldn't read /proc/self/task/14587/stack)
native: #00 pc 00018de0 /system/lib/libc.so (syscall+28)
native: #01 pc 000b76a1 /system/lib/libart.so
(art::ConditionVariable::WaitHoldingLocks(art::Thread*)+88)
native: #02 pc 003e6c89 /system/lib/libart.so (art::GoToRunnable(art::Thread*)+308)
native: #03 pc 003e6b21 /system/lib/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+8)
native: #04 pc 0075d969 /system/framework/arm/boot-framework.oat
(Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+144)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:793)
at android.app.IActivityManager$Stub$Proxy.getProcessMemoryInfo(IActivityManager.java:6324)
at java.lang.reflect.Method.invoke(Native method)
at c8.ky._1invoke(Interception.java:-1)
- nice=0 代表 THREAD_PRIORITY_DEFAULT, nice = 10 代表 THREAD_PRIORITY_BACKGROUND
- cgrp=defult 代表 foreground CPU scheduling,cgrp=bg_non_interactive 代表 background CPU scheduling
- 在主線程調(diào)用系統(tǒng)服務(wù),例如 AMS、WMS、PMS 等,可能在系統(tǒng)高負(fù)載、高并發(fā)運行下遇到ANR,原因在于這些接口一般是阻塞式的,而安卓框架內(nèi)有太多同步鎖、彼此影響。故主線程應(yīng)盡量減少這些接口的調(diào)用
常見可能會變得很慢的調(diào)用:registerReceiver、sendBroadcast

