閱讀454 返回首頁    go 阿裏雲 go 技術社區[雲棲]


專項:Android 內存泄露實踐分析

定義

內存泄漏也稱作“存儲滲漏”,用動態存儲分配函數動態開辟的空間,在使用完畢後未釋放,結果導致一直占據該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之後未回收)即所謂內存泄漏。
 
內存泄漏形象的比喻是“操作係統可提供給所有進程的存儲空間正在被某個進程榨幹”,最終結果是程序運行時間越長,占用存儲空間越來越多,最終用盡全部存儲空間,整個係統崩潰。所以“內存泄漏”是從操作係統的角度來看的。這裏的存儲空間並不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於磁盤交換區設定的大小。由程序申請的一塊內存,如果沒有任何一個指針指向它,那麼這塊內存就泄漏了。
 
——來自《百度百科》

影響

  • 導致OOM
  • 糟糕的用戶體驗
  • 雞肋的App存活率

成效

  • 內存泄露是一個持續的過程,隨著版本的迭代,效果越明顯
  • 由於某些原因無法改善的泄露(如框架限製),則盡量降低泄露的內存大小
  • 內存泄露實施後的版本,一定要驗證,不必馬上推行到正式版,可作為beta版持續觀察是否影響/引發其他功能/問題
內存泄露實施後,項目的收獲:
  • OOM減少30%以上
  • 平均使用內存從80M穩定到40M左右
  • 用戶體驗上升,流暢度提升
  • 存活率上升,推送到達率提升

類型

  • IO  
    • FileStream Cursor
  • Bitmap
  • Context
    • 單例
    • Callback
  • Service
    • BraodcastReceiver
    • ContentObserver
  • Handler
  • Thread

技巧

類型 垃圾回收時間 生存時間
強引用 永遠不會 JVM停止運行時終止
軟引用 內存不足時 內存不足時終止
弱引用 垃圾回收時 垃圾回收時終止
虛引用 垃圾回收時 垃圾回收時終止
Java引用介紹(https://blog.csdn.net/mazhimazh/article/details/19752475)
Java四種引用由高到低依次為:強引用  >  軟引用  >  弱引用  >  虛引用
表格說明

分析

 

原理

根本原因

  • 關注堆內存

怎麼解決

  • 詳見方案

 實踐分析

  • 詳見實踐

方案

  • 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); }   
     image

靜態變量持有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:弱引用隨時可能為空,使用前先判空
  • 示例代碼:

image

服務未解綁注冊泄露

  • 分析:一般發生在注冊了某服務,不用時未解綁服務導致泄露
  • 引用代碼:
     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);  } image

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公眾號  
17

最後更新:2017-08-13 22:47:39

  上一篇:go  雲棲大會變遷史(2009-2017)
  下一篇:go  說說Android的MVP模式