android 內存泄漏(如何防止內存泄漏)

由于項目中大量出現內存泄漏導致內存使用量增多而不能立馬釋放,不得不研究內存泄漏,接下來我們切入主題。
以下都是本人收集和總結的內容:

1. 什么是內存泄漏

一般情況下內存泄漏是由忘記釋放分配的內存導致的,而邏輯上的內存泄漏則是由于忘記在對象不再被使用的時候釋放對其的引用導致的。如果一個對象仍然存在強引用,垃圾回收器就無法對其進行垃圾回收。

2. android中的存儲泄漏

在安卓平臺,泄漏 Context 對象問題尤其嚴重。這是因為像 Activity 這樣的 Context 對象會引用大量很占用內存的對象,例如 View 層級,以及其他的資源。如果 Context 對象發生了內存泄漏,那它引用的所有對象都被泄漏了。安卓設備大多內存有限,如果發生了大量這樣的內存泄漏,那內存將很快耗盡。
如果一個對象的合理生命周期沒有清晰的定義,那判斷邏輯上的內存泄漏將是一個見仁見智的問題。幸運的是,activity 有清晰的生命周期定義,使得我們可以很明確地判斷 activity 對象是否被內存泄漏。onDestroy() 函數將在 activity 被銷毀時調用,無論是程序員主動銷毀 activity,還是系統為了回收內存而將其銷毀。如果 onDestroy 執行完畢之后,activity 對象仍被 heap root 強引用,那垃圾回收器就無法將其回收。所以我們可以把生命周期結束之后仍被引用的 activity 定義為被泄漏的 activity。
Activity 是非常重量級的對象,所以我們應該極力避免妨礙系統對其進行回收。然而有多種方式會讓我們無意間就泄露了 activity 對象。我們把可能導致 activity 泄漏的情況分為兩類,一類是使用了進程全局(process-global)的靜態變量,無論 APP 處于什么狀態,都會一直存在,它們持有了對 activity 的強引用進而導致內存泄漏,另一類是生命周期長于 activity 的線程,它們忘記釋放對 activity 的強引用進而導致內存泄漏。

3. 常見導致App內存泄漏的情況

3.1 靜態 Activity

泄漏 activity 最簡單的方法就是在 activity 類中定義一個 static 變量,并且將其指向一個運行中的 activity 實例。如果在 activity 的生命周期結束之前,沒有清除這個引用,那它就會泄漏了。這是因為 activity(例如 MainActivity) 的類對象是靜態的,一旦加載,就會在 APP 運行時一直常駐內存,因此如果類對象不卸載,其靜態成員就不會被垃圾回收。

public class MainActivity extends AppCompatActivity {

private static MainActivity activity;

@Override
protected void onCreate(Bundle savedInstanceState) {
  View saButton = findViewById(R.id.sa_button);
  saButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
      setStaticActivity();
      nextActivity();
    }
  });
}


void setStaticActivity() {
  activity = this;
}
  
  void nextActivity() {
        Intent intent = new Intent(this, DestinationActivity.class);
        startActivity(intent);
        SystemClock.sleep(600);
        finish();
    }
}

3.2 靜態 View

另一種類似的情況是對經常啟動的 activity 實現一個單例模式,讓其常駐內存可以使它能夠快速恢復狀態。然而,就像前文所述,不遵循系統定義的 activity 生命周期是非常危險的,也是沒必要的,所以我們應該極力避免。
但是如果我們有一個創建起來非常耗時的 View,在同一個 activity 不同的生命周期中都保持不變呢?所以讓我們為它實現一個單例模式。現在一旦 activity 被銷毀,內存就會泄漏!因為一旦 view 被加入到界面中,它就會持有 context 的強引用,也就是我們的 activity。由于我們通過一個靜態成員引用了這個 view,所以我們也就引用了 activity,因此 activity 就發生了泄漏。所以一定不要把加載的 view 賦值給靜態變量,如果你真的需要,那一定要確保在 activity 銷毀之前將其從 view 層級中移除

public class MainActivity extends AppCompatActivity {
 private static View view;
@Override
protected void onCreate(Bundle savedInstanceState) {
  View svButton = findViewById(R.id.sv_button);
  svButton.setOnClickListener(new View.OnClickListener() {
    @Override 
    public void onClick(View v) {
      setStaticView();
      nextActivity();
    }
  });
}

void setStaticView() {
  view = findViewById(R.id.sv_button);
}


}

3.3 內部類

現在讓我們在 activity 內部定義一個類,也就是內部類。這樣做的原因有很多,比如增加封裝性和可讀性。如果我們創建了一個內部類的對象,并且通過靜態變量持有了 activity 的引用,那也會發生 activity 泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
     View icButton = findViewById(R.id.ic_button);
     icButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          createInnerClass();
          nextActivity();
      }});
    }

    
  void createInnerClass() {
      class InnerClass {
      }
      inner = new InnerClass();
  }
}

3.4 Handlers

同樣的,定義一個匿名的 Runnable 對象并將其提交到 Handler 上也可能導致 activity 泄漏。Runnable 對象間接地引用了定義它的 activity 對象,而它會被提交到 Handler 的 MessageQueue 中,如果它在 activity 銷毀時還沒有被處理,那就會導致 activity 泄漏了。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
      View hButton = findViewById(R.id.h_button);
      hButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          createHandler();
          nextActivity();
      }
  });
 }

   void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
} 

3.5 Threads

同樣的,使用 Thread 和 TimerTask 也可能導致 activity 泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          spawnThread();
          nextActivity();
      }
    });
 }
 void spawnThread() {
     new Thread() {
         @Override public void run() {
             while(true);
         }
     }.start();
 }  
}


3.6 Timer Tasks

只要它們是通過匿名類創建的,盡管它們在單獨的線程被執行,它們也會持有對 activity 的強引用,進而導致內存泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          scheduleTimer();
          nextActivity();
      }
    });
 }

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
  }
   
}

3.7 Sensor Manager

系統服務可以通過 context.getSystemService 獲取,它們負責執行某些后臺任務,或者為硬件訪問提供接口。如果 context 對象想要在服務內部的事件發生時被通知,那就需要把自己注冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷毀時取消注冊,那就會導致 activity 泄漏了。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          registerListener() ;
          nextActivity();
      }
    });
 }

  void registerListener() {
         SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
         Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
         sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    } 
}

總結:在編寫代碼時要注意釋放不該有的內存,最壞的情況下,你的 APP 可能會由于大量的內存泄漏而內存耗盡,進而閃退,但它并不總是這樣。相反,內存泄漏會消耗大量的內存,但卻不至于內存耗盡,這時,APP 會由于內存不夠分配而頻繁進行垃圾回收。垃圾回收是非常耗時的操作,會導致嚴重的卡頓。在 activity 內部創建對象時,一定要格外小心,并且要經常測試是否存在內存泄漏。

4. 使用as工具檢測內存泄漏

在我們的日常追求構建更好的應用程序時,我們作為開發者需要考慮多方面的因素,其中之一是要確保我們的應用程序不會崩潰。崩潰的一個常見原因是內存泄漏。這方面的問題可以以各種形式表現出來。在大多數情況下,我們看到內存使用率穩步上升,直到應用程序不能分配更多的資源和必然崩潰。在Java中這往往導致一個OutOfMemoryException異常被拋出。在某些的情況下,泄漏的類甚至可以堅持足夠長的時間來接收注冊的回調,導致一些非常奇怪的錯誤,并往往拋出了臭名昭著的IllegalStateException異常
所以接下來用as工具檢測一些常見的內存泄露:

4.1 系統服務注冊

public class LeaksActivity extends Activity implements LocationListener {

    private LocationManager locationManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leaks);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, this);
    }

    // Listener implementation omitted
}

我們讓了Android 的LocationManager通知我們位置更新。所有我們需要設置這是系統服務本身和一個回調來接收更新。在這里,我們實現了服務本身位置的接口,這意味著LocationManager將開始使用。現在,如果該裝置被旋轉,新的活動將被創建取代已經注冊為位置更新舊的。由于系統服務肯定比其他生命周期長,使它不可能垃圾收集回收仍依賴于特定活動的資源,從而導致內存泄漏。然后該裝置的反復旋轉將引起非可回收活動填滿存儲器,最終導致一個OutOfMemoryException異常。但為了解決內存泄漏,我們首先必須要能夠找到它。
接下來用android studio 的內存監控,實時監控內存使用與分配,從使用內存異常中找到內存溢出的問題:

而Android監視器Android Studio中2.1.png

任何資源配置的交互將在這里體現出來,使之可以進行跟蹤應用程序的資源使用情況。接下來我們對上面的案例進行分析,首先開啟我們的應用程序,然后旋轉設備后,馬上執行Dump Java Heap ,就回生成一份 hprof 文件。

這么復雜的內存堆棧。淡定,我們首先找到自己剛剛執行的類,然后點擊查看Analyzer Tasks,可以看到出來一個界面把Detect Leaked Activities(檢查泄露內存)勾選上,接下來對Analysis Results里面的數據進行分析

分析內存結構.png

我們選中內存泄漏的activity,查看Reference Tree,可以清楚的看到一個服務的回調,就是之前地位系統服務的回調接口。知道了內存泄漏的地方,我們就可以把他解決掉

public class LeaksActivity extends Activity implements LocationListener {

    private LocationManager locationManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leaks);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, this);
    }

    // Listener implementation omitted

     @Override
      protected void onDestroy() {
            locationManager.removeUpdates(this);
            super.onDestroy();
      }
}

再進行旋轉設備就不會內存泄漏。

4.2 內部類

java中內部類使用很廣泛,然而有時候我們往往忽略內部類的生命周期,導致內存泄漏。接下來我們分析常見的Andr??oid activity:


public class AsyncActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        new BackgroundTask().execute();
    }

    private class BackgroundTask extends AsyncTask<Void, Void, String> {

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            textView.setText(result);
        }
    }
}

以上這種情況非多。問題來了,AsyncActivity中創建了一個匿名內部類BackgroundTask,同時開啟一個線程正在執行任務并且持有內存資源,如果在HTTP請求的情況下,這可能需要很長的時間,尤其在網速較慢的情況。更加容易內存泄漏。
接下來我們進行同樣的內存分析:

BackgroundTask內存分析.png

果然內存泄漏的罪魁禍首是BackgroundTask,是不是不刺激,我們再對代碼進行修改,執行后再次分析。

public class AsyncActivity extends Activity {

    TextView textView;
    AsyncTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        task = new BackgroundTask(textView).execute();
    }
    
    @Override
    protected void onDestroy() {
        task.cancel(true);
        super.onDestroy();
    }

    private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final TextView resultTextView;

        public BackgroundTask(TextView resultTextView) {
            this.resultTextView = resultTextView;
        }
        
        @Override
        protected void onCancelled() {
            // Cancel task. Code omitted.
        }

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            resultTextView.setText(result);
        }
    }
}

現在,內部類隱式引用已經解決,我們通過內部類傳入一個控件,再進入同樣的操作,看是否出現內存泄漏

BackgroundTask構造內存分析.png

呵呵,有出現一個新的內存泄漏,接下來我們冷靜再次分析一次,發現BackgroundTask中的resultTextView還有 Textview的強引用,那么該怎么解決這個問題了,最簡單就是給resultTextView加上WeakReference,以為當把最好一個強引用回收后,垃圾回收器就回考慮弱引用下的回收。寫個例子大家就明白了:

public class AsyncActivity extends Activity {

    TextView textView;
    AsyncTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        task = new BackgroundTask(textView).execute();
    }

    @Override
    protected void onDestroy() {
        task.cancel(true);
        super.onDestroy();
    }
  private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> textViewReference;

        public BackgroundTask(TextView resultTextView) {
            this.textViewReference = new WeakReference<>(resultTextView);
        }
        
        @Override
        protected void onCancelled() {
            // Cancel task. Code omitted.
        }

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            TextView view = textViewReference.get();
            if (view != null) {
                view.setText(result);
            }
        }
    }    
}

注意,在onPostExecute我們要檢查控件是否為空驗證,以防報空。
在運行分析任務activity就不再被泄漏!

4.3 匿名類

說白了就是匿名內部類,我們接下來對最近很火的Retrofit的網絡調用進行分析,不廢話,直接搞:

public class ListenerActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listener);
        textView = (TextView) findViewById(R.id.textView);

        GitHubService service = ((LeaksApplication) getApplication()).getService();
        service.listRepos("google")
                .enqueue(new Callback<List<Repo>>() {
                    @Override
                    public void onResponse(Call<List<Repo>> call,
                                           Response<List<Repo>> response) {
                        int numberOfRepos = response.body().size();
                        textView.setText(String.valueOf(numberOfRepos));
                    }

                    @Override
                    public void onFailure(Call<List<Repo>> call, Throwable t) {
                        // Code omitted.
                    }
                });
    }
}

其實和內部類分析的結果很相似,注意的是該activity的內存一直會在網絡請求完畢才消失。(以后寫代碼一點要注意這一點)


Retrofit網絡調用.png

處理結果和內部類一樣,大家看看代碼就知道了:

public class ListenerActivity extends Activity {

    TextView textView;
    Call call;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listener);
        textView = (TextView) findViewById(R.id.textView);

        GitHubService service = ((LeaksApplication)getApplication()).getService();

        call = service.listRepos("google");

        call.enqueue(new RepoCallback(textView));
    }
}
   @Override
    protected void onDestroy() {
        call.cancel();
        super.onDestroy();
    }

    private static class RepoCallback implements Callback<List<Repo>> {

        private final WeakReference<TextView> resultTextView;

        public RepoCallback(TextView resultTextView) {
            this.resultTextView = new WeakReference<>(resultTextView);
        }

        @Override
        public void onResponse(Call<List<Repo>> call,
                Response<List<Repo>> response) {
            TextView view = resultTextView.get();
            if (view != null) {
                int numberOfRepos = response.body().size();
                view.setText(String.valueOf(numberOfRepos));
            }
        }

        @Override
        public void onFailure(Call<List<Repo>> call, Throwable t) {
            // Code omitted.
        }
    }

總結:
我們要學會使用工具來對自己的代碼負責,對app的性能進行提升。一些常見的問題和處理方式已經在上面的例子中說明,謝謝你能讀這篇博客,說明你是一個很有責任心的程序員。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,654評論 0 8
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,237評論 2 7
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 802評論 0 5
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,415評論 0 12
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評論 25 708