本文由魅族科技有限公司資深Android開發工程師degao(嵌入式企鵝圈原創團隊成員)撰寫,是degao在嵌入式企鵝圈發表的第一篇原創文章,毫無保留地總結分享其在領導魅族多個項目開發中的Android客戶端性能優化經驗,極具實踐價值!html
衆所周知,一個好的產品,除了功能強大,好的性能也必不可少。有調查顯示,近90%的受訪者會由於APP性能差而卸載,性能也是形成APP用戶沮喪的頭號緣由。java
那Android客戶端性能的指標都有哪些?如何發現和定位客戶端的性能問題?本文結合多個項目的開發實踐,給出了要關注的重要指標項目,以及定位和解決性能問題的通常步驟。python
性能優化應該貫穿於功能開發的所有周期,而不是作完一次後面便再也不關注。每次發佈版本前,最好能對照標準檢查下性能是否達標。android
記住:產品=性能×功能!
shell
1、 性能檢查項編程
1. 啓動速度性能優化
1)這裏的啓動速度指的是冷啓動的速度,即殺掉應用後從新啓動的速度,此項主要是和你的競品對比。微信
2)不該在Application以及Activity的生命週期回調中作任何費時操做,具體指標大概是你在onCreate,onResume,onStart等回調中所花費的總時間最好不要超過400ms,不然用戶在桌面點擊你的應用圖標後,將感受到明顯的卡頓。網絡
2. 界面切換app
1)應用操做時,界面和動畫不該有明顯卡頓;
2)可經過在手機上打開 設置->開發者選項->調試GPU過分繪製,而後操做應用查看gpu是否超線進行初步判斷;
3. 內存泄露
1)back退出不該存在內存泄露,簡單的檢查辦法是在退出應用後,用命令`adb shell dumpsys meminfo 應用包名`查看 `Activities Views` 是否爲零;
2)屢次進入退出後的佔用內存`TOTAL`不該變化太大;
4. onTrimMemory回調
1)應用響應此回調釋放非必須內存;
2)驗證可經過命令`adb shell dumpsys gfxinfo 應用包名-cmd trim 5`後,再)用命令`adb shell dumpsys meminfo 應用包名`查看內存大小。
5. 過分繪製
打開設置中的GPU過分繪製開關,各界面過分繪製不該超過2.5x;也就是打開此調試開關後,界面總體呈現淺色,特別複雜的界面,紅色區域也不該該超過全屏幕的四分之一;
6. lint檢查
1)經過Android Studio中的 Analyze->Inspect Code 對工程代碼作靜態掃描;找出潛在的問題代碼並修改;
2) 0 error & 0 warning,若是確實不能解決,需給出緣由。
7. 反射優化
1)在代碼中減小反射調用;
2)對頻繁調用的返回值進行Cache;
8. 穩定性
1)連續48小時monkey不該出現閃退,anr問題。
2)若是應用接入了數據埋點的sdk,好比百度統計sdk等,這些sdk都會將應用的崩潰信息上報回來,開發者應天天關注這些統計到的崩潰日誌,嚴格控制應用的崩潰率;
9. 耗電
1)應用進入後臺後不該異常消耗電量;
2)操做應用後,退出應用,讓應用處於後臺,一段時間後經過`adb shell dumpsys batterystats`查看電量消耗日誌看是否存在異常。
2、性能問題常見緣由
性能問題通常歸結爲三類:
1. UI卡頓和穩定性:這類問題用戶可直接感知,最爲重要;
2. 內存問題:內存問題主要表現爲內存泄露,或者內存使用不當致使的內存抖動。若是存在內存泄露,應用會不斷消耗內存,易致使頻繁gc使系統出現卡頓,或者出現OOM報錯;內存抖動也會致使UI卡頓。
3. 耗電問題:會影響續航,表現爲沒必要要的自啓動,不恰當持鎖致使系統沒法正常休眠,系統休眠後頻繁喚醒系統等;
3、UI卡頓常見緣由和分析方法
下面分別介紹出現這些問題的常見緣由以及分析這些問題的通常步驟。
1.卡頓常見緣由
1)人爲在UI線程中作輕微耗時操做,致使UI線程卡頓;
2) 佈局Layout過於複雜,沒法在16ms內完成渲染;
3)同一時間動畫執行的次數過多,致使CPU或GPU負載太重;
4) View過分繪製,致使某些像素在同一幀時間內被繪製屢次,從而使CPU或GPU負載太重;
5) View頻繁的觸發measure、layout,致使measure、layout累計耗時過多及整個View頻繁的從新渲染;
6) 內存頻繁觸發GC過多(同一幀中頻繁建立內存),致使暫時阻塞渲染操做;
7) 冗餘資源及邏輯等致使加載和執行緩慢;
8)工做線程優先級未設置爲
Process.THREAD_PRIORITY_BACKGROUND
致使後臺線程搶佔UI線程cpu時間片,阻塞渲染操做;
9) ANR;
2. 卡頓分析解決的通常步驟
1)解決過分繪製問題
>在設置->開發者選項->調試GPU過分繪製中打開調試,看對應界面是否有過分繪製,若是有先解決掉:
> 定位過渡繪製區域
> 利用Android提供的工具進行位置確認以及修改(HierarchyView , Tracer for OpenGL ES)
> 定位到具體的視圖(xml文件或者View)
> 經過代碼和xml文件分析過渡繪製的緣由
> 結合具體狀況進行優化
> 使用Lint工具進一步優化
2) 檢查是否有主線程作了耗時操做:
嚴苛模式(StrictMode),是Android提供的一種運行時檢測機制,用於檢測代碼運行時的一些不規範的操做,最多見的場景是用於發現主線程的IO操做。應用程序能夠利用StrictMode儘量的發現一些編碼的疏漏。
> 開啓 StrictMode
>> 對於應用程序而言,Android 提供了一個最佳使用實踐:儘量早的在
android.app.Application 或 android.app.Activity 的生命週期使能 StrictMode,onCreate()方法就是一個最佳的時機,越早開啓就能在更多的代碼執行路徑上發現違規操做。
>> 監控代碼
public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() .build()); } super.onCreate(); }
若是主線程有網絡或磁盤讀寫等操做,在logcat中會有"D/StrictMode"tag的日誌輸出,從而定位到耗時操做的代碼。
3)若是主線程無耗時操做,還存在卡頓,有很大多是必須在UI線程操做的一些邏輯有問題,好比控件measure、layout耗時過多等,此時可經過Traceview以及systrace來進行分析。
4)Traceview:Traceview主要用作熱點分析,找出最須要優化的點。
> 打開DDMS而後選擇一個進程,接着點擊上面的「Start Method Profiling」按鈕(紅色小點變爲黑色即開始運行),而後操做咱們的卡頓UI,而後點擊"Stop Method Profiling",會打開以下界面:
圖中展現了Trace期間各方法調用關係,調用次數以及耗時比例。經過分析能夠找出可疑的耗時函數並進行優化;
5)systrace:抓取trace:
> 執行以下命令:
$ cd android-sdk/platform-tools/systrace $ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
> 操做APP,而後會生成一個mynewtrace.html 文件,用Chrome打開。
> 圖示以下:
經過分析上面的圖,能夠找出明顯存在的layout,measure,draw的超時問題。
6)導入以下插件,可經過在方法上添加@DebugLog來打印方法的耗時:
build.gradle: buildscript { dependencies {
//用於方便調試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調用參數,以及執行時間;
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' } }
//用於方便調試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調用參數,以及執行時間;
apply plugin: 'com.jakewharton.hugo' java: @DebugLog public void test( int a ){ int b=a*a; }
4、內存性能分析優化
1.內存泄露
該問題目前在項目中通常用leakcanary基本就能搞定,配置起來也至關簡單:
build.gradle: dependencies { debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1 releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1 testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1 }
java: public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); LeakCanary.install(this); } }
一旦有內存泄露,將會在通知欄生成一條通知,點開可看到泄露的對象以及引用路徑:
2.內存抖動
若是代碼中存在在onDraw或者for循環等屢次執行的代碼中分配對象的行爲,會致使運行過程當中gc次數增多,影響ui流暢度。通常這些問題均可經過lint工具檢測出來。
5、耗電量優化建議
電量優化主要是注意儘可能不要影響手機進入休眠,也就是正確申請和釋放WakeLock,另外就是不要頻繁喚醒手機,主要就是正確使用Alarm。
6、一些好的代碼實踐
1. 節制地使用Service
2. 當界面不可見時釋放內存
3. 當內存緊張時釋放內存
4. 避免在Bitmap上浪費內存
對大圖片,先獲取圖片的大小信息,根據實際須要展現大小計算inSampleSize,最後decode;
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filename, options); // Calculate inSampleSize options.inSampleSize = reqHeight); calculateInSampleSize(options, reqWidth, // Decode bitmap with inSampleSize set options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filename, options); } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float) height / (float) reqHeight); } else { inSampleSize = Math.round((float) width / (float) reqWidth); } } return inSampleSize; }
5. 使用優化過的數據集合
6. 謹慎使用抽象編程
7. 儘可能避免使用依賴注入框架
不少依賴注入框架是基於反射的原理,雖然可讓代碼看起來簡潔,可是是有礙性能的。
8. 謹慎使用external libraries
9. 優化總體性能
10. 使用ProGuard來剔除不須要的代碼
android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'src/main/proguard-project.txt' signingConfig signingConfigs.debug } }
11. 慎用異常,異常對性能不利
拋出異常首先要建立一個新的對象。Throwable 接口的構造函數用名爲
fillInStackTrace() 的本地方法,fillInStackTrace() 方法檢查棧,收集調用跟蹤信
息。只要有異常被拋出,VM 就必要調整調用棧,由於在處理過程當中建立了一
個新對象。
異常只能用於錯誤處理,不該該用來控制程序流程。
如下例子很差:
try { startActivity(intentA); } catch () { startActivity(intentB); }
應該用下面的語句判斷:
if (getPackageManager().resolveActivity(intentA, 0) != null)
不要再循環中使用 try/catch 語句,應把其放在最外層,使用 System.arraycopy()代替 for 循環複製。
更多Android、Linux、嵌入式和物聯網原創技術分享敬請關注微信公衆號:嵌入式企鵝圈。