Android性能優化--手機篇

衆所周知,一個好的產品,除了功能強大,好的性能也必不可少。有調查顯示,近90%的受訪者會因爲APP性能差而卸載,性能也是造成APP用戶沮喪的頭號原因。
那Android客戶端性能的指標都有哪些?如何發現和定位客戶端的性能問題?本文結合多個項目的開發實踐,給出了要關注的重要指標項目,以及定位和解決性能問題的一般步驟。
性能優化應該貫穿於功能開發的全部週期,而不是做完一次後面便不再關注。每次發佈版本前,最好能對照標準檢查下性能是否達標。
記住:產品=性能×功能!
一、 性能檢查項
1. 啓動速度
1)這裏的啓動速度指的是冷啓動的速度,即殺掉應用後重新啓動的速度,此項主要是和你的競品對比。
2)不應在Application以及Activity的生命週期回調中做任何費時操作,具體指標大概是你在onCreate,onResume,onStart等回調中所花費的總時間最好不要超過400ms,否則用戶在桌面點擊你的應用圖標後,將感覺到明顯的卡頓。
2. 界面切換
1)應用操作時,界面和動畫不應有明顯卡頓;
2)可通過在手機上打開 設置->開發者選項->調試GPU過度繪製,然後操作應用查看gpu是否超線進行初步判斷;



3. 內存泄露
1)back退出不應存在內存泄露,簡單的檢查辦法是在退出應用後,用命令`adb shell dumpsys meminfo 應用包名`查看 `Activities Views` 是否爲零;
2)多次進入退出後的佔用內存`TOTAL`不應變化太大;


4.  onTrimMemory回調
1)應用響應此回調釋放非必須內存;
2驗證可通過命令`adb shelldumpsys gfxinfo 應用包名-cmd trim 5`後,再)用命令`adb shell dumpsys meminfo 應用包名`查看內存大小

5. 過度繪製
1)打開設置中的GPU過度繪製開關,各界面過度繪製不應超過2.5x;也就是打開此調試開關後,界面整體呈現淺色,特別複雜的界面,紅色區域也不應該超過全屏幕的四分之一;

6. lint檢查:
1)通過Android Studio中的 Analyze->Inspect Code 對工程代碼做靜態掃描;找出潛在的問題代碼並修改;
2) 0 error & 0warning,如果確實不能解決,需給出原因。


7. 反射優化:
1)在代碼中減少反射調用;
2)對頻繁調用的返回值進行Cache;

8.  穩定性:
1)連續48小時monkey不應出現閃退,anr問題。
2)如果應用接入了數據埋點的sdk,比如百度統計sdk,友盟統計sdk等,這些sdk都會將應用的崩潰信息上報回來,開發者應每天關注這些統計到的崩潰日誌,嚴格控制應用的崩潰率;

9.  耗電:
1)應用進入後臺後不應異常消耗電量;
2)操作應用後,退出應用,讓應用處於後臺,一段時間後通過`adb shell dumpsysbatterystats`查看電量消耗日誌看是否存在異常。
 
二、性能問題常見原因

性能問題一般歸結爲三類:
1. UI卡頓和穩定性:這類問題用戶可直接感知,最爲重要;
2. 內存問題:內存問題主要表現爲內存泄露,或者內存使用不當導致的內存抖動。如果存在內存泄露,應用會不斷消耗內存,易導致頻繁gc使系統出現卡頓,或者出現OOM報錯;內存抖動也會導致UI卡頓。
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 voidonCreate() {
 if (DEVELOPER_MODE) {
 StrictMode.setThreadPolicy(newStrictMode.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.htmlsched 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;
}
 
四、內存性能分析優化
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 {
 
  @Overridepublic void onCreate() {
   super.onCreate();
   LeakCanary.install(this);
  }
}
一旦有內存泄露,將會在通知欄生成一條通知,點開可看到泄露的對象以及引用路徑:

2.內存抖動
如果代碼中存在在onDraw或者for循環等多次執行的代碼中分配對象的行爲,會導致運行過程中gc次數增多,影響ui流暢度。一般這些問題都可通過lint工具檢測出來。
 
五、耗電量優化建議
電量優化主要是注意儘量不要影響手機進入休眠,也就是正確申請和釋放WakeLock,另外就是不要頻繁喚醒手機,主要就是正確使用Alarm。
六、一些好的代碼實踐
1. 節制地使用Service
2. 當界面不可見時釋放內存
3. 當內存緊張時釋放內存
4. 避免在Bitmap上浪費內存
對大圖片,先獲取圖片的大小信息,根據實際需要展示大小計算inSampleSize,最後decode;
public static BitmapdecodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to checkdimensions
final BitmapFactory.Options options = newBitmapFactory.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 intcalculateInSampleSize(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. 謹慎使用externallibraries
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 循環複製。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章