本文由魅族科技有限公司資深A(yù)ndroid開發(fā)工程師degao(嵌入式企鵝圈原創(chuàng)團(tuán)隊(duì)成員)撰寫,是degao在嵌入式企鵝圈發(fā)表的第一篇原創(chuàng)文章,毫無保留地總結(jié)分享其在領(lǐng)導(dǎo)魅族多個(gè)項(xiàng)目開發(fā)中的Android客戶端性能優(yōu)化經(jīng)驗(yàn),極具實(shí)踐價(jià)值!
眾所周知,一個(gè)好的產(chǎn)品,除了功能強(qiáng)大,好的性能也必不可少。有調(diào)查顯示,近90%的受訪者會(huì)因?yàn)锳PP性能差而卸載,性能也是造成APP用戶沮喪的頭號(hào)原因。
那Android客戶端性能的指標(biāo)都有哪些?如何發(fā)現(xiàn)和定位客戶端的性能問題?本文結(jié)合多個(gè)項(xiàng)目的開發(fā)實(shí)踐,給出了要關(guān)注的重要指標(biāo)項(xiàng)目,以及定位和解決性能問題的一般步驟。
性能優(yōu)化應(yīng)該貫穿于功能開發(fā)的全部周期,而不是做完一次后面便不再關(guān)注。每次發(fā)布版本前,最好能對(duì)照標(biāo)準(zhǔn)檢查下性能是否達(dá)標(biāo)。
記?。寒a(chǎn)品=性能×功能!
一、 性能檢查項(xiàng)
1. 啟動(dòng)速度
1)這里的啟動(dòng)速度指的是冷啟動(dòng)的速度,即殺掉應(yīng)用后重新啟動(dòng)的速度,此項(xiàng)主要是和你的競品對(duì)比。
2)不應(yīng)在Application以及Activity的生命周期回調(diào)中做任何費(fèi)時(shí)操作,具體指標(biāo)大概是你在onCreate,onResume,onStart等回調(diào)中所花費(fèi)的總時(shí)間最好不要超過400ms,否則用戶在桌面點(diǎn)擊你的應(yīng)用圖標(biāo)后,將感覺到明顯的卡頓。
2. 界面切換
1)應(yīng)用操作時(shí),界面和動(dòng)畫不應(yīng)有明顯卡頓;
2)可通過在手機(jī)上打開 設(shè)置->開發(fā)者選項(xiàng)->調(diào)試GPU過度繪制,然后操作應(yīng)用查看gpu是否超線進(jìn)行初步判斷;
3. 內(nèi)存泄露
1)back退出不應(yīng)存在內(nèi)存泄露,簡單的檢查辦法是在退出應(yīng)用后,用命令adb shell dumpsys meminfo 應(yīng)用包名查看Activities Views是否為零;
2)多次進(jìn)入退出后的占用內(nèi)存TOTAL不應(yīng)變化太大;
4. onTrimMemory回調(diào)
1)應(yīng)用響應(yīng)此回調(diào)釋放非必須內(nèi)存;
2)驗(yàn)證可通過命令adb shell dumpsys gfxinfo 應(yīng)用包名-cmd trim 5后,再)用命令adb shell dumpsys meminfo 應(yīng)用包名查看內(nèi)存大小。
5. 過度繪制
打開設(shè)置中的GPU過度繪制開關(guān),各界面過度繪制不應(yīng)超過2.5x;也就是打開此調(diào)試開關(guān)后,界面整體呈現(xiàn)淺色,特別復(fù)雜的界面,紅色區(qū)域也不應(yīng)該超過全屏幕的四分之一;
6. lint檢查
1)通過Android Studio中的 Analyze->Inspect Code 對(duì)工程代碼做靜態(tài)掃描;找出潛在的問題代碼并修改;
2) 0 error & 0 warning,如果確實(shí)不能解決,需給出原因。
7. 反射優(yōu)化
1)在代碼中減少反射調(diào)用;
2)對(duì)頻繁調(diào)用的返回值進(jìn)行Cache;
8. 穩(wěn)定性
1)連續(xù)48小時(shí)monkey不應(yīng)出現(xiàn)閃退,anr問題。
2)如果應(yīng)用接入了數(shù)據(jù)埋點(diǎn)的sdk,比如百度統(tǒng)計(jì)sdk等,這些sdk都會(huì)將應(yīng)用的崩潰信息上報(bào)回來,開發(fā)者應(yīng)每天關(guān)注這些統(tǒng)計(jì)到的崩潰日志,嚴(yán)格控制應(yīng)用的崩潰率;
9. 耗電
1)應(yīng)用進(jìn)入后臺(tái)后不應(yīng)異常消耗電量;
2)操作應(yīng)用后,退出應(yīng)用,讓應(yīng)用處于后臺(tái),一段時(shí)間后通過adb shell dumpsys batterystats查看電量消耗日志看是否存在異常。
二、性能問題常見原因
性能問題一般歸結(jié)為三類:
1. UI卡頓和穩(wěn)定性:這類問題用戶可直接感知,最為重要;
2. 內(nèi)存問題:內(nèi)存問題主要表現(xiàn)為內(nèi)存泄露,或者內(nèi)存使用不當(dāng)導(dǎo)致的內(nèi)存抖動(dòng)。如果存在內(nèi)存泄露,應(yīng)用會(huì)不斷消耗內(nèi)存,易導(dǎo)致頻繁gc使系統(tǒng)出現(xiàn)卡頓,或者出現(xiàn)OOM報(bào)錯(cuò);內(nèi)存抖動(dòng)也會(huì)導(dǎo)致UI卡頓。
3. 耗電問題:會(huì)影響續(xù)航,表現(xiàn)為不必要的自啟動(dòng),不恰當(dāng)持鎖導(dǎo)致系統(tǒng)無法正常休眠,系統(tǒng)休眠后頻繁喚醒系統(tǒng)等;
三、UI卡頓常見原因和分析方法
下面分別介紹出現(xiàn)這些問題的常見原因以及分析這些問題的一般步驟。
1.卡頓常見原因
1)人為在UI線程中做輕微耗時(shí)操作,導(dǎo)致UI線程卡頓;
2) 布局Layout過于復(fù)雜,無法在16ms內(nèi)完成渲染;
3)同一時(shí)間動(dòng)畫執(zhí)行的次數(shù)過多,導(dǎo)致CPU或GPU負(fù)載過重;
4) View過度繪制,導(dǎo)致某些像素在同一幀時(shí)間內(nèi)被繪制多次,從而使CPU或GPU負(fù)載過重;
5) View頻繁的觸發(fā)measure、layout,導(dǎo)致measure、layout累計(jì)耗時(shí)過多及整個(gè)View頻繁的重新渲染;
6) 內(nèi)存頻繁觸發(fā)GC過多(同一幀中頻繁創(chuàng)建內(nèi)存),導(dǎo)致暫時(shí)阻塞渲染操作;
7) 冗余資源及邏輯等導(dǎo)致加載和執(zhí)行緩慢;
8)工作線程優(yōu)先級(jí)未設(shè)置為
Process.THREAD_PRIORITY_BACKGROUND
導(dǎo)致后臺(tái)線程搶占UI線程cpu時(shí)間片,阻塞渲染操作;
9) ANR;
2. 卡頓分析解決的一般步驟
1)解決過度繪制問題
>在設(shè)置->開發(fā)者選項(xiàng)->調(diào)試GPU過度繪制中打開調(diào)試,看對(duì)應(yīng)界面是否有過度繪制,如果有先解決掉:
> 定位過渡繪制區(qū)域
> 利用Android提供的工具進(jìn)行位置確認(rèn)以及修改(HierarchyView , Tracer for OpenGL ES)
> 定位到具體的視圖(xml文件或者View)
> 通過代碼和xml文件分析過渡繪制的原因
> 結(jié)合具體情況進(jìn)行優(yōu)化
> 使用Lint工具進(jìn)一步優(yōu)化
2) 檢查是否有主線程做了耗時(shí)操作:
嚴(yán)苛模式(StrictMode),是Android提供的一種運(yùn)行時(shí)檢測(cè)機(jī)制,用于檢測(cè)代碼運(yùn)行時(shí)的一些不規(guī)范的操作,最常見的場景是用于發(fā)現(xiàn)主線程的IO操作。應(yīng)用程序可以利用StrictMode盡可能的發(fā)現(xiàn)一些編碼的疏漏。
> 開啟 StrictMode
>> 對(duì)于應(yīng)用程序而言,Android 提供了一個(gè)最佳使用實(shí)踐:盡可能早的在
android.app.Application 或 android.app.Activity 的生命周期使能 StrictMode,onCreate()方法就是一個(gè)最佳的時(shí)機(jī),越早開啟就能在更多的代碼執(zhí)行路徑上發(fā)現(xiàn)違規(guī)操作。
>> 監(jiān)控代碼
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();
}
如果主線程有網(wǎng)絡(luò)或磁盤讀寫等操作,在logcat中會(huì)有”D/StrictMode”tag的日志輸出,從而定位到耗時(shí)操作的代碼。
3)如果主線程無耗時(shí)操作,還存在卡頓,有很大可能是必須在UI線程操作的一些邏輯有問題,比如控件measure、layout耗時(shí)過多等,此時(shí)可通過Traceview以及systrace來進(jìn)行分析。
4)Traceview:Traceview主要用做熱點(diǎn)分析,找出最需要優(yōu)化的點(diǎn)。
> 打開DDMS然后選擇一個(gè)進(jìn)程,接著點(diǎn)擊上面的“Start Method Profiling”按鈕(紅色小點(diǎn)變?yōu)楹谏撮_始運(yùn)行),然后操作我們的卡頓UI,然后點(diǎn)擊”Stop Method Profiling”,會(huì)打開如下界面:
圖中展示了Trace期間各方法調(diào)用關(guān)系,調(diào)用次數(shù)以及耗時(shí)比例。通過分析可以找出可疑的耗時(shí)函數(shù)并進(jìn)行優(yōu)化;
5)systrace:抓取trace:
> 執(zhí)行如下命令:
$?cd?android-sdk/platform-tools/systrace
$?python?systrace.py?--time=10?-o?mynewtrace.html?sched?gfx?view?wm
> 操作APP,然后會(huì)生成一個(gè)mynewtrace.html 文件,用Chrome打開。
> 圖示如下:
通過分析上面的圖,可以找出明顯存在的layout,measure,draw的超時(shí)問題。
6)導(dǎo)入如下插件,可通過在方法上添加@DebugLog來打印方法的耗時(shí):
build.gradle:
buildscript?{
dependencies?{
//用于方便調(diào)試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調(diào)用參數(shù),以及執(zhí)行時(shí)間;
classpath?'com.jakewharton.hugo:hugo-plugin:1.2.1'
}
}
//用于方便調(diào)試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調(diào)用參數(shù),以及執(zhí)行時(shí)間;
apply?plugin:?'com.jakewharton.hugo'
java:
@DebugLog
public?void?test(?int?a?){
int?b=a*a;
}
四、內(nèi)存性能分析優(yōu)化
1.內(nèi)存泄露
該問題目前在項(xiàng)目中一般用leakcanary基本就能搞定,配置起來也相當(dāng)簡單:
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);
}
}
一旦有內(nèi)存泄露,將會(huì)在通知欄生成一條通知,點(diǎn)開可看到泄露的對(duì)象以及引用路徑:
2.內(nèi)存抖動(dòng)
如果代碼中存在在onDraw或者for循環(huán)等多次執(zhí)行的代碼中分配對(duì)象的行為,會(huì)導(dǎo)致運(yùn)行過程中gc次數(shù)增多,影響ui流暢度。一般這些問題都可通過lint工具檢測(cè)出來。
五、耗電量優(yōu)化建議
電量優(yōu)化主要是注意盡量不要影響手機(jī)進(jìn)入休眠,也就是正確申請(qǐng)和釋放WakeLock,另外就是不要頻繁喚醒手機(jī),主要就是正確使用Alarm。
六、一些好的代碼實(shí)踐
1. 節(jié)制地使用Service
2. 當(dāng)界面不可見時(shí)釋放內(nèi)存
3. 當(dāng)內(nèi)存緊張時(shí)釋放內(nèi)存
4. 避免在Bitmap上浪費(fèi)內(nèi)存
對(duì)大圖片,先獲取圖片的大小信息,根據(jù)實(shí)際需要展示大小計(jì)算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. 使用優(yōu)化過的數(shù)據(jù)集合
6. 謹(jǐn)慎使用抽象編程
7. 盡量避免使用依賴注入框架
很多依賴注入框架是基于反射的原理,雖然可以讓代碼看起來簡潔,但是是有礙性能的。
8. 謹(jǐn)慎使用external libraries
9. 優(yōu)化整體性能
10. 使用ProGuard來剔除不需要的代碼
android?{
buildTypes?{
release?{
minifyEnabled?true
shrinkResources?true
proguardFiles?getDefaultProguardFile('proguard-android.txt'),?'src/main/proguard-project.txt'
signingConfig?signingConfigs.debug
}
}
11. 慎用異常,異常對(duì)性能不利
拋出異常首先要?jiǎng)?chuàng)建一個(gè)新的對(duì)象。Throwable 接口的構(gòu)造函數(shù)用名為
fillInStackTrace() 的本地方法,fillInStackTrace() 方法檢查棧,收集調(diào)用跟蹤信
息。只要有異常被拋出,VM 就必要調(diào)整調(diào)用棧,因?yàn)樵谔幚磉^程中創(chuàng)建了一
個(gè)新對(duì)象。
異常只能用于錯(cuò)誤處理,不應(yīng)該用來控制程序流程。
以下例子不好:
try?{
startActivity(intentA);
}?catch?()?{
startActivity(intentB);
}
應(yīng)該用下面的語句判斷:
if?(getPackageManager().resolveActivity(intentA,?0)?!=?null)
不要再循環(huán)中使用 try/catch 語句,應(yīng)把其放在最外層,使用 System.arraycopy()代替 for 循環(huán)復(fù)制。