天天看點

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使用參見下表
Android 記憶體洩露實踐分析

  • 善用Reference
    • Java引用介紹
    • Java四種引用由高到低依次為:強引用  >  軟引用  >  弱引用  >  虛引用
    • 表格說明
    Android 記憶體洩露實踐分析
  • 複用ConvertView
    • 複用詳解
  • 對象釋放
    • 遵循誰建立誰釋放的原則
    • 示例:顯示調用clear清單、對象賦空值

分析

​ 原理

  • Java記憶體配置設定機制
  • Java垃圾回收機制

​ 根本原因

  • 關注堆記憶體

​ 怎麼解決

  • 詳見方案

​ 實踐分析

  • 詳見實踐

方案

  • 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
    • GitHub位址
    • 使用方法
  • 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);
      }
           
Android 記憶體洩露實踐分析

靜态變量持有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();
        }
               
Android 記憶體洩露實踐分析

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);
  }
           
Android 記憶體洩露實踐分析

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;
    }
  }
           
Android 記憶體洩露實踐分析

後面

  • 歡迎補充實際中遇到的洩露類型
  • 文章如有錯誤,歡迎指正
  • 如有更好的記憶體洩露分享方法,歡迎一起讨論

原文發表于:Testerhome;

作者:ycwdaaaa ; 

原文連結:https://testerhome.com/topics/5822

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

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

繼續閱讀