Android 內存泄露實踐分析

今天看到一篇關於Android 內存泄露實踐分析的文章,感覺不錯,講的還算詳細,mark到這裏。

原文發表於:Testerhome;

作者:ycwdaaaa ; 

原文鏈接:https://testerhome.com/topics/5822



定義

​內存泄漏也稱作“存儲滲漏”,用動態存儲分配函數動態開闢的空間,在使用完畢後未釋放,結果導致一直佔據該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之後未回收)即所謂內存泄漏。

 

內存泄漏形象的比喻是“操作系統可提供給所有進程的存儲空間正在被某個進程榨乾”,最終結果是程序運行時間越長,佔用存儲空間越來越多,最終用盡全部存儲空間,整個系統崩潰。所以“內存泄漏”是從操作系統的角度來看的。這裏的存儲空間並不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於磁盤交換區設定的大小。由程序申請的一塊內存,如果沒有任何一個指針指向它,那麼這塊內存就泄漏了。

 

​ ——來自《百度百科》

影響

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

成效

  • 內存泄露是一個持續的過程,隨着版本的迭代,效果越明顯
  • 由於某些原因無法改善的泄露(如框架限制),則儘量降低泄露的內存大小
  • 內存泄露實施後的版本,一定要驗證,不必馬上推行到正式版,可作爲beta版持續觀察是否影響/引發其他功能/問題

內存泄露實施後,項目的收穫:

  • OOM減少30%以上
  • 平均使用內存從80M穩定到40M左右
  • 用戶體驗上升,流暢度提升
  • 存活率上升,推送到達率提升

類型

  • IO
    • FileStream
    • Cursor
  • Bitmap
  • Context

    • 單例
    • Callback
  • Service

    • BraodcastReceiver
    • ContentObserver
  • Handler

  • Thread

技巧

  • 慎用Context

    • Context概念
    • 四大組件Context和Application的context使用參見下表

  • 善用Reference

    • Java引用介紹
    • Java四種引用由高到低依次爲:強引用  >  軟引用  >  弱引用  >  虛引用
    • 表格說明

  • 複用ConvertView

  • 對象釋放

    • 遵循誰創建誰釋放的原則
    • 示例:顯示調用clear列表、對象賦空值

分析

​ 原理

​ 根本原因

  • 關注堆內存

​ 怎麼解決

  • 詳見方案

​ 實踐分析

  • 詳見實踐

方案

  • StrictMode

    • 使用方法:AppContext的onCreate()方法加上
    <span class="n" style="color: rgb(68, 68, 68); font-family: "Microsoft YaHei"; white-space: pre-wrap; background-color: rgb(245, 245, 245); box-sizing: border-box;"></span>StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy
                        .Builder()
                        .detectAll()
                        .penaltyLog()
                        .build());
    StrictMode.setVmPolicy(new StrictMode.VmPolicy
                        .Builder()
                        .detectAll()
                        .penaltyLog()
                        .build());<span class="o" style="font-family: "Microsoft YaHei"; white-space: pre-wrap; background-color: rgb(245, 245, 245); color: rgb(102, 102, 102); box-sizing: border-box;"></span>
    • 主要檢查項:內存泄露、耗時操作等
  • Leakcanary

  • 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

  • MAT

實踐(示例)

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流,避免泄露
<span class="kd" style="font-family: "Microsoft YaHei"; white-space: pre-wrap; background-color: rgb(245, 245, 245); color: rgb(170, 34, 255); box-sizing: border-box; font-weight: bold;"></span>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持有

  • 引用代碼:

<span style="color: rgb(51, 51, 51); font-family: "Microsoft YaHei"; font-size: 14px;">  ActivityUtil.getAppManager().add(this);</span>
  • 持有代碼:
    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();
    }
  • 解決方案:

    • 改爲弱引用
    • pass:弱引用隨時可能爲空,使用前先判空
    • 示例代碼:
     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();
        }
    
    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();
        }
    

Context泄露

Callback泄露

服務未解綁註冊泄露

  • 分析:一般發生在註冊了某服務,不用時未解綁服務導致泄露

  • 引用代碼:

 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;
    }
  }

後面

  • 歡迎補充實際中遇到的泄露類型
  • 文章如有錯誤,歡迎指正
  • 如有更好的內存泄露分享方法,歡迎一起討論

原文發表於:Testerhome;

作者:ycwdaaaa ; 

原文鏈接:https://testerhome.com/topics/5822


關於騰訊WeTest (wetest.qq.com)

騰訊WeTest是騰訊遊戲官方推出的一站式遊戲測試平臺,用十年騰訊遊戲測試經驗幫助廣大開發者對遊戲開發全生命週期進行質量保障。騰訊WeTest提供:適配兼容測試;雲端真機調試;安全測試;耗電量測試;服務器性能測試;輿情監控等服務。

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