專項:Android 內存泄露實踐分析
定義
內存泄漏也稱作“存儲滲漏”,用動態存儲分配函數動態開辟的空間,在使用完畢後未釋放,結果導致一直占據該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之後未回收)即所謂內存泄漏。
內存泄漏形象的比喻是“操作係統可提供給所有進程的存儲空間正在被某個進程榨幹”,最終結果是程序運行時間越長,占用存儲空間越來越多,最終用盡全部存儲空間,整個係統崩潰。所以“內存泄漏”是從操作係統的角度來看的。這裏的存儲空間並不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於磁盤交換區設定的大小。由程序申請的一塊內存,如果沒有任何一個指針指向它,那麼這塊內存就泄漏了。
——來自《百度百科》
影響
- 導致OOM
- 糟糕的用戶體驗
- 雞肋的App存活率
成效
- 內存泄露是一個持續的過程,隨著版本的迭代,效果越明顯
- 由於某些原因無法改善的泄露(如框架限製),則盡量降低泄露的內存大小
- 內存泄露實施後的版本,一定要驗證,不必馬上推行到正式版,可作為beta版持續觀察是否影響/引發其他功能/問題
內存泄露實施後,項目的收獲:
- OOM減少30%以上
- 平均使用內存從80M穩定到40M左右
- 用戶體驗上升,流暢度提升
- 存活率上升,推送到達率提升
類型
- IO
- FileStream Cursor
- Bitmap
- Context
- 單例
- Callback
- Service
- BraodcastReceiver
- ContentObserver
- Handler
- Thread
技巧
- 慎用Context
- Context概念(https://blog.csdn.net/lmj623565791/article/details/40481055)
- 四大組件Context和Application的context使用參見下表
- 善用Reference
類型 | 垃圾回收時間 | 生存時間 |
---|---|---|
強引用 | 永遠不會 | JVM停止運行時終止 |
軟引用 | 內存不足時 | 內存不足時終止 |
弱引用 | 垃圾回收時 | 垃圾回收時終止 |
虛引用 | 垃圾回收時 | 垃圾回收時終止 |
Java引用介紹(https://blog.csdn.net/mazhimazh/article/details/19752475)
Java四種引用由高到低依次為:強引用 > 軟引用 > 弱引用 > 虛引用
表格說明
-
複用ConvertView
-
對象釋放
- 遵循誰創建誰釋放的原則
- 示例:顯示調用clear列表、對象賦空值
分析
原理
- Java內存分配機製(https://blog.csdn.net/shimiso/article/details/8595564)
- Java垃圾回收機製(https://www.cnblogs.com/sunniest/p/4575144.html)
根本原因
- 關注堆內存
怎麼解決
- 詳見方案
實踐分析
- 詳見實踐
方案
- StrictMode
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy
- .Builder()
- .detectAll()
- .penaltyLog()
- .build());StrictMode.setVmPolicy(new StrictMode.VmPolicy
- .Builder()
- .detectAll()
-
.penaltyLog()
.build());- 主要檢查項:內存泄露、耗時操作等 使用方法:AppContext的onCreate()方法加上
Leakcanary
GitHub地址(https://github.com/square/leakcanary)
使用方法(https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)Leakcanary + StrictMode + monkey **(推薦)**
使用階段:功能測試完成後,穩定性測試開始時
使用方法:安裝集成了Leakcanary的包,跑monkey
收獲階段:一段時間後,會發現出現N個泄露
實戰分析:逐條分析每個泄露並改善/修複
StrictMode:查看日誌搜索StrictMode關鍵字Adb命令
手動觸發GC
通過adb shell dumpsys meminfo packagename -d查看
查看Activity以及View的數量
越接近0越好
對比進入Activity以及View前的數量和退出Activity以及View後的數量判斷Android Monitor
使用介紹(https://wetest.qq.com/lab/view/?id=99)MAT
使用介紹(https://blog.csdn.net/xiaanming/article/details/42396507)
實踐(示例)
Bitmap泄露
Bitmap泄露一般會泄露較多內存,視圖片大小、位圖而定
- 經典場景:App啟動圖
- 解決內存泄露前後內存相差10M+,可謂驚人
- 解決方案: App啟動圖Activity的onDestroy()中及時回收內存
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
recycleImageView(imgv_load_ad);
}
public static void recycleImageView(View view){
if(view==null) return;
if(view instanceof ImageView){
Drawable drawable=((ImageView) view).getDrawable();
if(drawable instanceof BitmapDrawable){
Bitmap bmp = ((BitmapDrawable)drawable).getBitmap();
if (bmp != null && !bmp.isRecycled()){
((ImageView) view).setImageBitmap(null);
bmp.recycle();
bmp=null;
}
}
}
}
IO流未關閉
- 分析:通過日誌可知FileOutputStream()未關閉
- 問題代碼:
public static void copyFile(File source, File dest) { FileChannel inChannel = null; FileChannel outChannel = null; Log.i(TAG, "source path: " + source.getAbsolutePath()); Log.i(TAG, "dest path: " + dest.getAbsolutePath()); try { inChannel = new FileInputStream(source).getChannel(); outChannel = new FileOutputStream(dest).getChannel(); inChannel.transferTo(0, inChannel.size(), outChannel); } catch (IOException e) { e.printStackTrace(); } }
- 解決方案:
- 及時關閉IO流,避免泄露
public static void copyFile(File source, File dest) {
FileChannel inChannel = null;
FileChannel outChannel = null;
Log.i(TAG, "source path: " + source.getAbsolutePath());
Log.i(TAG, "dest path: " + dest.getAbsolutePath());
try {
inChannel = new FileInputStream(source).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
E/StrictMode: A resource was acquired at attached stack trace but never released.
See java.io.Closeable for information on avoiding resource leaks.
java.lang.Throwable: Explicit termination method 'close' not called
at dalvik.system.CloseGuard.open(CloseGuard.java:180)
at java.io.FileOutputStream.<init>(FileOutputStream.java:89)
at java.io.FileOutputStream.<init>(FileOutputStream.java:72)
at com.heyniu.lock.utils.FileUtil.copyFile(FileUtil.java:44)
at com.heyniu.lock.db.BackupData.backupData(BackupData.java:89)
at com.heyniu.lock.ui.HomeActivity$11.onClick(HomeActivity.java:675)
at android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
單例模式泄露
- 分析:通過截圖我們發現SplashActivity被ActivityUtil的實例activityStack持有
- 引用代碼: ActivityUtil.getAppManager().add(this);
- 持有代碼:
public void add(Activity activity) { if (activityStack == null) { synchronized (ActivityUtil.class){ if (activityStack == null) { activityStack = new Stack<>(); } } } activityStack.add(activity); }
- 解決方案:
- 在SplashActivity的onDestroy()生命周期移除引用
@Override protected void onDestroy() { super.onDestroy(); ActivityUtil.getAppManager().remove(this); }
靜態變量持有Context實例泄露
- 分析:長生命周期持有短什麼周期引用導致泄露,詳見上文四大組件Context和Application的context使用
- 示例引用代碼:
private static HttpRequest req; public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) { // TODO Auto-generated constructor stub req = new HttpRequest(context, url, TaskId, requestBody, Headers, listener); req.post(); }
- 解決方案:
- public static void cancel(int TaskId) {
- if(req != null && req.get() != null){
- req.get().AsyncCancel(TaskId);
- }
}
private static WeakReference<HttpRequest> req;public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
// TODO Auto-generated constructor stub
req = new WeakReference<HttpRequest>(new HttpRequest(context, url, TaskId, requestBody, Headers, listener));
req.get().post();
}
private static HttpRequest req;public static void HttpUtilPost(Context context, int TaskId, String url, String requestBody,ArrayList<HttpHeader> Headers, RequestListener listener) {
// TODO Auto-generated constructor stub
req = new HttpRequest(context.getApplicationContext(), url, TaskId, requestBody, Headers, listener);
req.post();
}
- 改為長生命周期
- 改為弱引用
- pass:弱引用隨時可能為空,使用前先判空
- 示例代碼:
服務未解綁注冊泄露
- 分析:一般發生在注冊了某服務,不用時未解綁服務導致泄露
- 引用代碼:
private void initSensor() { // 獲取傳感器管理器 sm = (SensorManager) container.activity.getSystemService(Context.SENSOR_SERVICE); // 獲取距離傳感器 acceleromererSensor = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); // 設置傳感器監聽器 acceleromererListener = new SensorEventListener() { ...... }; sm.registerListener(acceleromererListener, acceleromererSensor, SensorManager.SENSOR_DELAY_NORMAL); }
- 解決方案:
在Activity的onDestroy()方法解綁服務
@Override protected void onDestroy() { super.onDestroy(); sm.unregisterListener(acceleromererListener,acceleromererSensor); }
Handler泄露
- 分析:由於Activity已經關閉,Handler任務還未執行完成,其引用了Activity的實例導致內存泄露
- 引用代碼:
handler.sendEmptyMessage(0);
- 解決方案:
在Activity的onDestroy()方法回收Handler
@Override protected void onDestroy() { super.onDestroy(); handler.removeCallbacksAndMessages(null); }
- 圖片後續遇到再補上
異步線程泄露
- 分析:一般發生在線程執行耗時操作時,如下載,此時Activity關閉後,由於其被異步線程引用,導致無法被正常回收,從而內存泄露
- 引用代碼:
new Thread() { public void run() { imageArray = loadImageFromUrl(imageUrl); }.start();
- 解決方案:
把線程作為對象提取出來
在Activity的onDestroy()方法阻塞線程
thread = new Thread() { public void run() { imageArray = loadImageFromUrl(imageUrl); }; thread.start(); @Override protected void onDestroy() { super.onDestroy(); if(thread != null){ thread.interrupt(); thread = null; } }
後麵
- 歡迎補充實際中遇到的泄露類型
- 文章如有錯誤,歡迎指正
- 如有更好的內存泄露分享方法,歡迎一起討論
未完待續。。。
阿裏雲測移動質量中心(以下簡稱MQC)是為廣大企業客戶和移動開發者提供真機測試服務的雲平台,擁有大量熱門機型,提供7x24全天候服務。 我們致力於提供專業、穩定、全麵、高價值的自動化測試能力,以及簡單易用的使用流程、貼心的技術服務,並且幫助客戶以最低的成本、最高的效率發現APP中的各類隱患(APP崩潰、各類兼容性問題、功能性問題、性能問題等),減少用戶流失,提高APP質量和市場競爭力。
聯係我們:
網站地址:https://mqc.aliyun.com
開發者交流旺旺群:335334143
開發者交流QQ群:492028798
客服郵箱:mqc_group@service.alibaba.com
更多精彩技術分享 歡迎關注 MQC公眾號
最後更新:2017-08-13 22:47:39