##基礎知識
- 底層觸發回收機制時機:
GC_FOR_MALLOC 堆上分配對象時內存不足觸發
GC_CONCURRENT 堆內存達到一定量(即快滿了)時觸發
GC_EXPLICIT 主動觸發,System.gc、VMRuntime.gc或收到SIGUSR1信號
~
GC日誌如下
D/dalvikvm( 7030) : GC_CONCURRENT feed 1049K,60% free 2341K/9351K ,external 3502K/6261K,paused 3ms 3ms
~
GC_CONCURRENT是GC類型,此外還有GC_FOR_MALLOC、GC_EXTERNAL_ALLOC、GC_HPROF_DUMO_HEAP、GC_EXPLICIT
feed 1049K 表示這次GC回收了多少內存
60% free 2341K/9351K 描述Heap內存的信息,本次回收後60%的Heap可用,存活對象大小爲2341K,Heap大小爲9351K
external 3502K/6261K 描述Native Momory信息(存放位圖數據或堆外內存), 表示已經分配了3502K內存,當分配到6261K的時候會觸發下一次GC
paused 3ms 3ms 表示GC暫停時間
-
UI卡頓:60fps=1000ms / 16ms
0~100 毫秒的延遲會讓用戶感知到瞬時的卡頓;
100~300 毫秒的延遲會讓用戶感覺遲緩;
300~1000 毫秒的延遲讓用戶感覺“手機卡死了”;
1000 毫秒以上的延遲會讓用戶想去幹別的事情 -
硬件加速(GPU)的繪製效率遠高於軟件繪製(CPU),但存在以下缺陷:耗電高、某些接口不兼容、內存佔用大(Open GL至少需要8M內存)
-
引用(強軟弱虛)
軟引用:垃圾收集器運行時可能會也可能不會釋放軟引用,但在保證虛擬機內存不足之時會清理它
弱引用:與軟引用的區別是,弱引用生命週期更短,且無論虛擬機內存是否不足都會被清理
虛引用:只能用於跟蹤即將對被引用對象進行的收集,在任何時候都可能被垃圾回收器回收 -
ANR:
1.主線程5s內沒有處理完輸入事件
2.主線程10s沒處理完BroadCastReceiver.onReceive
3.主線程在Service各生命週期函數時20s沒處理完
ANR IN :發生ANR的具體類
PID:發生ANR的進程,系統會在此時生成trace文件
Reason:當前ANR的類型以及導致ANR的原因
CPU usage:CPU的使用情況,在ANR發生前後都會產生一個CPU usage一共打印兩次CPU使用情況
- OOM:試圖申請的內存+已分配的內存>虛擬機允許的最大內存
內存泄漏:無用對象持續佔用內存或得不到及時釋放
案例:https://tech.meituan.com/oom_analysis.html
-
App閃退常見原因:NullPointer、OOM、數組越界、類型轉換異常、數字轉換異常、Activity/Service找不到、實體對象沒有序列化等
-
Force Close常見原因:Error、OOM(內存溢出)、StackOverFlowError、RuntimeException空指針異常
-
Native 層Crash
SIGILL 執行了非法指令、可執行文件本身出錯、堆棧溢出等情況
SIGABRT 調用abort函數生成
SIGBUS 訪問非法地址,包括內存地址對齊出錯
SIGFPE 算法運算錯誤,如溢出及除0等
SIGSEGV 訪問不屬於自己的存儲空間或訪問只讀存儲空間
SIGPIPE 管道異常,這個信號通常在進程間通信時產生
磁盤性能問題
性能瓶頸:磁盤的順序讀寫基本速度都非常快了,問題基本都在隨機讀寫
- 隨機讀因爲沒有預讀(read-ahead)的優化性能差很多
- 隨機寫會造成大量的失效頁和寫入放大問題
- 數據庫的性能問題,本質上也是IO性能問題
常見問題:
- 卡頓
- ANR
- IO Wait很高的時候,很可能主線程執行了耗時IO操作
- 大量的數據讀寫、數據庫操作、硬件操作也可能造成ANR
Tips:寫入放大情景:我們知道磁盤一塊有512k,塊中每頁4k,當需要寫入4k時系統恰好找到一個512k塊中有一頁4k的無效頁。那麼系統會將這512k塊數據讀出再擦除這512k塊,之後再把讀出的數據+要寫入的4k數據寫入這個512k的塊中。這樣原本的寫入4k變成了【閃存讀512k】-【緩存改4k】-【閃存擦除512k】-【閃存寫入512k】。寫入放大實驗:1、正常寫入1M文件,記錄耗時。2、先用6k的小文件寫滿存儲,再刪除(系統標記爲無效頁,並未真正刪除類似垃圾桶功能),這樣保證存儲中沒有乾淨的存儲塊,最好寫入1M的文件,記錄耗時,兩相對比。
工具推薦:
- Systrace:IO操作耗時過長問題
- Strict Mode:主線程IO問題
- IO Monitor:主線程IO問題、多餘IO操作、Buffer過小問題
- SQL IO Monitor:主線程IO問題、數據庫IO問題(全表掃描、不合理事務等造成)
優化建議:
- 通過緩存避免重複讀寫次數(變量保存、延遲寫入等)
- IO操作、數據庫操作儘量放入子線程
- ByteArrayOutputStream > ObjectOutputStream
- 合理設置Buffer大小
- 加解壓 ZipFile + BufferedOutputStream;ZipInputStream適合網絡數據解壓、損壞壓縮包解壓、大量的小文件加解壓
- 避免頻繁開關數據庫,數據庫打開後等到真正不需要再關閉或應用退出再關閉
- Bitmap的加載,decodeStream + BufferedInputStream > decodeFile;decodeResource > decodeResourceStream
- SP的commit都是一次文件的打開讀寫關閉,儘量在必要時在進行;同時用apply代替commit
- 索引能加快查詢速度,但同樣能導致插入更新緩慢,需合理使用
- Buffer的讀寫每次的緩存數據使用8K,可以提供一定的效率
內存性能問題
性能瓶頸:
- OOM、Low Memory Killer會殺死進程,GC會導致App卡頓
- Activity泄露問題極爲嚴重,通常有mContext間接引用造成、this$0間接引用造成、Activity直接引用造成
常見問題:
- 內存泄露 OOM(Crash)
- 隨着功能的反覆執行,Heap內存一直在持續增長。通常是內存泄露,適合用LeakCannary工具進行白盒測試分析
- 每次啓動應用後,Heap內存相比以前版本穩定增長。通常出現在啓動後待機或使用某功能後,可能是由新功能及代碼改動引入固定內存增長。使用用Heap Dump進行多版本或功能使用前後的對比
- Heap Alloc變化不大,但進程的Dalvik Heap Pss內存明顯增加。通常由於分配了大量小對象造成的內存碎片。
- GC引起的卡頓
- 大量的new臨時變量或new Bitmap等大內存等操作需要注意
- 代碼執行時出現頻繁GC,Heap Alloc內存大幅度波動。即內存抖動,通常是分配了許多臨時變量或數組還有AsyncTask,隨後被迅速回收。適合使用Heap Viewer/Allocation Tracker等工具查看具體分配的對象
工具推薦:
- top/procrank :內存佔用過大、內存泄
- meminfo : Native內存泄漏、是否存在Activity/ApplicationContext泄漏、數據庫緩存命中率低
- MAT/Finder/JHAT : Java層的重複內存、不合理圖片解碼、內存泄漏
- libc_malloc_deBug_leak.so : Ntive內存泄漏
- LeakCannary/StrictMode/LeakInspector : Activity內存泄漏
- 騰訊APT : 內存佔用過大、內存泄漏
- GC Log from Logcat/GC Log 生成圖表 : 人工觸發GC for Explicit而導致的卡頓,Heap內存不足觸發GC for Alloc 而導致的卡頓
- Systrace : GC導致的卡頓
- Allocation Tracer : 申請內存次數過多和過大,輔助定位GC Log發現的問題
- chrome devtool : HS的內存問題
優化建議:
- 使用輕量級的數據結構 ArrayMap、SparseArray
- 儘量避免使用Enum枚舉(枚舉最大優勢是類型安全,但佔用內存比普通定義常量大得多,可用註解來檢查類型安全);儘量避免基本類型的拆裝箱(Integer、Long 、Float…)
- String的拼接使用StringBuilder
- 內存對象的重複利用:對象池;利用系統自帶的資源;StringBuilder、StringBuffer
- 儘量不要再循環中創建大量的臨時變量–內存抖動
- 引入SDK庫和調用新的系統API需要考慮成本
- dex文件有很多優化空間,如仔細統計並調整dex文件的順序往往能節約1M以上的dex mmap內存
【內存泄露】
- this$0 間接引用 導致 Activity內存泄漏;
解決方案:在Activity關閉時在onDestroy中,解除內部類和外部類的引用關係或使用弱引用 - Thread 直接引用 導致Activity內存泄漏
解決方案:使用完解除Thread和Activity的引用關係,即Thread使用完移除Activity或使用弱引用 - mContext 間接引用 導致Activity內存泄漏
解決方案:使用後解除mContext引用或使用弱引用 - this$0 間接引用 導致Activity內存泄漏
解決方案:用static來截斷匿名類引用,或使用弱引用 - 匿名內部類/非靜態內部類(尤其是放在Activity中的):非靜態內部類會持有外部類的引用,若非靜態內部類的實例是靜態的則其生命週期也是跟app一樣長,導致外部類(尤其爲Activity)無法釋放
解決方案:可將內部類置爲靜態類則不會持有外部類引用,或內部類設計成單例,傳入資源用弱引用包裹,Context用ApplicationContext或用後置空 - 單例、Application的生命週期跟app一樣長,要注意其所持有的對象無法釋放。eg:單例對象持有Activity的mContext對象導致Activity無法釋放。
- 非View類Context儘量使用ApplicationContext替代(startActivity、Layout.Inflation、Dialog必須使用Activity的Context)
- Timer、Handler、AsyncTask等異步操作,導致 Activity內存泄漏
解決方案:onDestroy進行關閉,清除。handler爲內部類會持有外部類(尤其Activity)的引用,而activity結束時若handler的消息還沒處理完則導致內存泄漏【handler置爲靜態類,且內部若持有外部對象則置爲弱引用WeakReference】 - 常駐內存問題,相同的圖片保存一份,去重
- 避免靜態變量持有大量數據引用。靜態變量不會被回收,長期維持對象的引用阻止垃圾回收,如果持有對象是Bitmap等大數據對象就很容造成內存不足
- 注意資源/監聽器/Cursor對象/Recerver對象/Sensor對象/File對象等的關閉、註銷、移除,臨時資源主動回收(加載後忘記釋放的Bitmap、臨時生成的Byte數組和文件緩衝區)。注意需要close後置空,並且注意try-catch中使用的資源在finally中進行關閉處理。
內部類的正確用法(Handler爲例)
private static Class InnerHandler extends Handler{
private final WeakReference<HandlerActivity> mActivity;
public InnerHandler(HandlerActivity activity){
mActivity = activity;
}
@Override
public void handleMessage(){
HandlerActivity activity = mActivity.get();
if(activity != null){...}
}
}
單例中使用Context的正確方式
public class SingleInstance{
private Context mContext;
private static SingleInstance sInstance;
private SingleInstance(Context context){
mContext = context;
}
public static SingleInsatance getInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstance(context.getApplicationContext());
}
return sInstance;
}
}
【圖片優化】
- 格式:WebP(Android4.0+才支持) > JPEG(不支持透明) > PNG
- 圖片壓縮:ImageAlpha有損壓縮、TinyPNG有損壓縮、ImageOptim無損壓縮
- ARGB565/ARGB4444/ALPHA_8的使用
- 改變圖片大小:inSampleSize指數冪的縮放,inScaled、inDensity、inTargetDensity更爲細緻
BitmapFactory.Options options = new BitmapFractory.Options();
//inSample縮放
options.inSampleSize = 4;
//inScaled、inDensity、inTargetDensity組合
options.inScaled = true;
options.inDensity = srcWidth;
options.inTargetDensity = dstWidth;
BitmapFactory.decodeStream(is,null,optins);
- inBitmap複用內存
- 不用的使用recycle
- 三級緩存
【WebView內存問題】
WebView一經加載其內存不可釋放,除非進程死亡;而且WebView有很多坑可能導致應用崩潰。可將WebView放在一個單獨的進程中,通過AIDL與主進程進行通信。WebView進程關閉即可銷燬內存,但不宜每次退出都進行關閉,因爲進程的啓動和銷燬也是耗時操作,每次進出都進行新建銷燬進程會導致卡頓問題,需要有一個策略來處理。
CPU性能問題
性能瓶頸:
- CPU資源冗餘使用,由於算法問題、重複查、排序、刪除等浪費CPU資源
- CPU資源爭搶,搶主線程的CPU資源,搶音視頻的CPU資源,多線程平等互爭資源
- CPU資源利用率低,主要在於磁盤IO、網絡IO、鎖操作、sleep操作等導致CPU無法得到有效利用,空跑。
常見問題:
- 卡頓
- 檢查UI線程是否有可能有耗時的操作
- overdraw 佈局層數是否太多?
- 動畫是否層數太多?是否過於複雜?是否onDraw調用次數不合理頻繁觸發measure、layout、onDraw
- ANR(Crash)
- 查看CPU是否很忙,是否因爲其他進程或子線程佔用CPU時間片過多導致主線程時間片過少
- 死鎖、其他線程持有鎖,導致主線程等待超時
工具推薦:
- adb命令
adb shell top 列出進程的各種數據
adb shell ps 處理進程的身份標識
cat /proc/pid/stat 查看CPU信息
adb shell dumpsys cpuinfo 獲取PCU信息 - PerfBox : FPS、Activity啓動速度
- Systrace:分析繪製時流程導致的卡頓,能大約定位是GC、IO、貼圖太大,還是沒用ViewHolder的問題
- Traceview:能深入定位分析各種流暢度與時延問題,但時只能初步定位XML佈局和OpenGL繪製的性能問題
- Trepn
- Gfxinfo/Slickr : 定位硬件加速下的性能問題
- Hierarchy Viewer : 定位XML佈局導致的性能問題
- Tracer for Open GL/Adreno/UXTune : 具體定位繪製性能問題
- Chrome DevTool : 定位具體H5卡頓問題
優化建議:
- CPU資源爭搶,減少非核心需求的CPU消耗,或降低非核心線程的優先級
- 優化算法,避免重複浪費CPU資源
- 將CPU消耗轉換爲GPU消耗(使用RenderScript、OpenGL來進行復雜的繪製)
- 使用合適的數據類型(int、float、long),使用合適的容器(ArrayMap、SparseArray、ConcurrentHashMap)
- 使用緩存和預處理來提升算法效率,(緩存提升效率其實是內存空間換時間)
- 使用多線程提高效率的時候注意選擇合適的線程數
【流暢性–佈局優化】
- 儘量使用RelativeLayout和LinearLayout佈局,不用AbsoluteLayout。考慮用RelativeLayout代替LinearLayout減少佈局層次,同層級則建議用LinearLayout(性能略高)
- include 重用佈局,include使用其他屬性的時候需要設置width、height屬性
- ViewStub view的延遲加載,推遲佈局加載。當佈局需要顯示的時候纔會去加載佈局(佈局的顯隱我們經常用setVisible來搞定,其實用ViewStub的inflation來控制顯示性能會好一些,但不適合需要進行顯隱切換的佈局)
- merge 減少不必要的層級,多用來替代FrameLayout
- 去掉多餘的背景顏色(包括Activity背景燈),減少過度繪製
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@null</item>
- 圖標+文字的佈局可以考慮TextView的compound drawables來實現,會比TextView+ImageView更高效一些
- 使用layout_weight屬性會導致LinearLayout繪製的時候需要被測量兩次,影響性能,尤其在ListView/GridView的子Item中尤其明顯。此外weight+weight_sum屬性可以用來減少佈局層數的情況下可以考慮使用。
- 文字有多種樣式可以考慮Html.fromHtml()和Spannable來實現,而不是寫多個TextView拼接
- 佈局複雜導致層次多可以考慮用自定義ViewGroup來實現,可以考慮用LayoutAnimationController實現佈局動畫
【流暢性–ListView/RecyclerView優化】
- 複用convertView,即HoldView的使用
- 異步加載圖片
- 快速滑動時SCROLL_STATE_FLING不顯示/加載圖片或消耗資源較大的view,停止後處於SCROLL_STATE_IDLE和SCROLL_STATE_TOUCH_SCROLL再進行加載顯示
- item儘可能地減少使用控件和佈局層次,儘可能地複用控件,這樣可以減少ListView的內存使用,減少滑動時gc次數。ListView的背景色與cacheColorHint設置相同顏色可以提高滑動時的渲染性能
- getView不做複雜的邏輯計算
網絡性能問題
網絡有三種狀態 FullPower全力運行、Low Power低狀態、Standby等待狀態,特點:升到Full快,降下來慢
性能瓶頸:
- 業務成功率
- 業務網絡延遲。DNS部分可以採用IP直連、域名重用、HttpDNS等方式;TCP短連接會導致每次連接都發生三次握手,可利用長連接
- 業務帶寬成本。減少用戶使用app的帶寬成本,不必要的網絡請求放在wifi環境下等策略
工具推薦:
- Wireshark – 網絡性能問題分析定位
- fiddler – 主要針對Http,能模擬Http的錯誤和延時返回
- SPDY/HTTP2.0/QUIC – 網絡協議,利用FastTcpOpen減少握手次數,利用UDP更好地適應網絡抖動
- WebPageTest.org – 做web應用的數據上報,提供LoadTime、StartRender、SpeedIndex、DOM Elements等耗時
- ATC – 最專業的弱網絡模擬工具,能模擬窄帶寬、延時、丟包、損壞包、包亂序等情況
優化建議:
- Webview或一些實時性不強的網絡採用網絡緩存;
解決方案:設置HTTP的Cache-control使得其保存資源文件;public/private/no-cache/no-store/max-age/max-stale
然後設置HTTP請求時的緩存模式;LOAD_CACHE_ONLY/LOAD_DEFAULT/LOAD_CACHE_NORMAL/LOAD_NO_CACHE/LOAD_CACHE_ELSE_NETWORK - 大數據網絡傳輸可先進行壓縮下載後再進行解壓使用
- 調整服務器RcvBuffer的MSS優化【分片策略】,緩解網絡延遲
- 超時和重試,單個請求設置超時時間,適當考慮失敗重試。分片下載,下載失敗只重新獲取失敗部分
- 可考慮避免DNS解析直接用IP
- 根據業務合併網絡請求或預先獲取網絡數據
- CDN的使用
- 儘量合併非必要實時請求,控制請求的頻率。
- 數據壓縮:POST的Body採用GZip;請求頭壓縮採用SPDY和HTTP2.0直接 壓縮
電量
組成App電量消耗各部分定義:
● CPU的電量消耗
● wake lock的電量消耗
● 數據傳輸的電量消耗
● WIFI運行的電量消耗
● GPS的電量消耗
● other sensors的電量消耗
● 蜂窩通信電量消耗
● 屏幕電量消耗
● 信號電量消耗
● WIfi電量消耗
● CPU空閒時電量消耗
● 藍牙模塊電量消耗
推薦工具:
- adb shell dumpsys batteryinfo
- PowerStat2.0(騰訊)
- Battery Historian 2.0(Google 需要Android5.0以上系統)
獲取方式:
adb shell dumpsys batterystats --enable full-wake-history
adb shell dumpsys batterystats --reset
adb bugreport > bugreport.txt
將txt轉成html可視化,historian.py從https://github.com/google/battery-historian下載
python historian.py -a bugreport.txt > bugreport.html
優化建議:
1、網絡方面
a、發起網絡請求時機,把可以延遲執行的網絡請求延後合併發起,減少網絡Http建立次數
b、減少移動網絡被激活時間和次數:發現返回數據相同時,延遲下次請求 時間。避免頻繁間隔請求,採用批處理一次性請求。使用預存取技術提取獲取一些數據避免後面頻繁再次發起網絡請求
c、數據處理:網絡 傳輸進行壓縮。進行大數據下載用GZIP下載。使用高效率的數據格式和解析方法,推薦JSON和Protobuf
d、慎用輪訓方式去執行網絡請求
e、減少推送消息的次數和頻率
f、網絡狀態,處理具體業務前,養成判斷當前網絡 狀態的習慣
2、界面相關
a、離開界面後停止對應的耗電活動
b、應用進入後臺禁止異常耗電
3、定位相關
a、使用GPS後記得關閉
b、定位要求不高時用網絡定位代替GPS定位
c、慎用持續定位
d、慎用被動定位
Android6.0之後的兩種省電模式Doze 和 App Standby
Doze模式進入:未連接電源 + 屏幕關閉
Doze模式的五種狀態:
- ACTIVE:手機處於激活狀態
- INACTIVE:手機剛脫離激活狀態
- IDLE_PENDING:IDLE預備態
- IDLE:空閒狀態
- IDLE_MAINTENANCE:處理掛起任務
IDLE狀態下的限制:
● 斷開網絡連接
● 忽略Wake Lock
● 標準鬧鐘AlarmManager定時任務延遲
● 系統不會掃描熱點WIFI
● 禁止同步工作
● 停止JobScheduler任務調度
Apk瘦身
Analyze Apk:將APK拖入Android Studio即可觀察當前包的結構及各部分佔用的大小
瘦身建議:
- 資源壓縮,字體壓縮FontZip、解壓庫AndrodiUn7Zip
- 利用Proguard來做代碼混淆、除去冗餘代碼、無用代碼冗餘資源
- 一般圖片資源只做一套高清圖,個別有問題再用9png解決,最後纔會根據不同分辨率做不同的圖的方案
- 圖片格式的選擇以及壓縮
- redex 字節碼優化,可以在優化包Size的同時提高字節碼加載性能,提示App速度。主要優化項目有減少和壓縮字符串、消除冗餘代碼、內聯函數、Interdex(一種冷啓動方法)
- 對最終的Apk包進行7Zip極限壓縮
啓動速度
冷啓動:App進程還沒有創建的情況下啓動App
熱啓動:App啓動時,後臺已有App進程(後臺掛起),eg:按Home鍵退出App
溫啓動:介於冷啓動和溫啓動之間,eg:系統由於某種原因回收App,用戶重新啓動App等
啓動時間度量:
adb shell am start -W pkg_name/activity 打印【TotalTime】 應用啓動耗時 ,包括 新進程啓動、Applicaton初始化、Activity啓動時間;【ThisTime】一般和Total Time相同,除非應用啓動時開了一個透明的Activity做預處理再加載主Activity;【WaitTime】總的耗時,包括啓動耗時和前一個應用Activity pause時間
優化建議:
- 做的初始化 工作儘量減輕,採用延遲加載、異步加載或懶加載(按需加載),attachBaseContext、onCreate中不做耗時操作
- Activity要進行 佈局 優化;避免加載 或編碼Bitmap,依賴Bitmap的View延遲更新 ;避免硬盤或網絡操作 阻塞UI繪製;避免在UI線程 中進行資源初始化,可延遲或異步處理
- 爲Activity設置windowBackground,在View加載完成前可以顯示這張 背景logo
Android性能優化典範
http://hukai.me/android-performance-patterns/
http://hukai.me/android-performance-patterns-season-2/
http://hukai.me/android-performance-patterns-season-3/
http://hukai.me/android-performance-patterns-season-4/
http://hukai.me/android-performance-patterns-season-5/
http://hukai.me/android-performance-patterns-season-6/
Android內存優化之OOM
http://hukai.me/android-performance-oom/
Android性能優化之電量篇
http://hukai.me/android-performance-battery/
Android性能優化之內存篇
http://hukai.me/android-performance-memory/
Android性能優化之運算篇
http://hukai.me/android-performance-compute/
Android性能優化之渲染篇
http://hukai.me/android-performance-render/