標籤:
在分享會上聽小夥伴對這部分內容做了講解,發覺在平時的編程中確實有很多問題沒有注意到,故記錄下來分享給各位,也歡迎各位不吝賜教糾本文中不足之處。
記憶體流失與記憶體溢出:
記憶體溢出簡單講就是程式運行要求的記憶體大於虛擬機器能提供的最大記憶體,會導致程式崩潰,也就是我們常見的Out Of Memory錯誤。
記憶體泄露指程式未能釋放已經不再使用的記憶體。記憶體流失並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於程式設計的失誤,導致在釋放該段記憶體之前就失去了對該段記憶體的控制,從而造成了記憶體的浪費。少量的記憶體流失並不會影響到程式的運行,但長時間的積累會消耗越來越多的記憶體,最終導致記憶體溢出。
記憶體的分配是由程式完成的,而記憶體的釋放有 GC(記憶體回收機制)完成。GC 為了能夠正確的釋放對象, 必須監控每一個對象的運行狀態,包括對象的申請,引用,被引用,賦值等。當檢測到一個對象完全無用時,便可以對這個對象進行回收。
常見記憶體流失:
理解了以上概念,我們將列舉幾種在我們平時容易導致記憶體流失的不好編程習慣,並利用 Android Studio 內建的記憶體分析工具進行檢測記憶體流失。
- 單例造成的記憶體流失
- 集合類造成的記憶體流失
- 非靜態內部類造成的記憶體流失
- 匿名內部類/非同步線程造成記憶體流失
- Handler 造成記憶體流失
①單例造成記憶體流失:
單例由於其靜態特性使得其生命週期跟應用一樣長,處理不當極易導致記憶體流失。
例如:我們經常在程式的開屏頁初始化一些,資源讀取,網路請求等單例方法。我們現在來類比一個這樣的情境:
建立一個用來讀取 drawable 資源的工具類,並採用單例的設計模式。
public class ResourceReader { //單例對象 private static ResourceReader mInstance = null; private Context context; //通過 Context 上下文來建立對象 private ResourceReader(Context context){ this.context = context; } public static ResourceReader getInstance(Context context){ if(mInstance == null){ mInstance = new ResourceReader(context); } return mInstance; } public Drawable getDrawable(int drawableId){ return ContextCompat.getDrawable(context, drawableId); }}
在 WelcomeActivity 中,通過工具類讀取一張圖片,在按鈕點擊後跳轉 MainActivity 並將 WelcomeActivity finish 掉。
public class WelcomeActivity extends AppCompatActivity { private ImageView imageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_welcome); this.imageView = (ImageView) findViewById(R.id.welcome_image); //通過工具類讀取一個 Drawable 資源 Drawable drawable = ResourceReader.getInstance(this).getDrawable(R.drawable.welcome); imageView.setImageDrawable(drawable); } public void onClick(View view){ Intent intent = new Intent(this, MainActivity.class); startActivity(intent); finish();//關閉該Activity }}
在 MainActivity 中依然用工具類載入一個 drawable 資源。
public class MainActivity extends AppCompatActivity { private ImageView imageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Drawable drawable = ResourceReader.getInstance(this).getDrawable(R.drawable.main); imageView = (ImageView) findViewById(R.id.main_image); imageView.setImageDrawable(drawable); }}
運行效果如下:
在調試啟動並執行時候實際上 Android Studio 已經在記錄調試裝置的一些即時資訊了,開啟 Android Monitor 的 Montiors,可以看到如下介面
我們可以在點擊 ② 按鈕一小段時間後點擊 ③ 按鈕(dump java heap),因為點 ② 後會讓我們的裝置發起一個 GC 回收操作,回收那些無用的對象,因為這些無用的對象不在我們的考慮範圍內。
點完 ② 按鈕後,Studio 就開始自己工作了,稍等一下dump 成功後會產生 hprof 檔案並自動開啟,檔案名稱為進程加時間戳記。
此圖由網友提供
接下來我們點擊右側 Analyzer Tasks 展開後點擊,Perform Analyzer 按鈕稍後便可以看到 Studio 為我們自動分析的結果。
由於我們的代碼比較簡單,在這裡便可以很直觀的看出我們記憶體流失的 Activity,在較為複雜的代碼情況下我們還可以結合 MAT 一起來使用,可以展現更加直觀的結果,這裡不再詳述。
現在我們分析一下 WelcomeActivity 記憶體流失的原因:
我們在WelcomeActivity 中調用 drawable 載入工具類時傳入了其自身上下文使得ResourceReader 的靜態對象擁有了對 WelcomeActivity 的引用,在我們使用完 WelcomeActivity 並調用 finish() 方法時,我們以為其已經銷毀並且記憶體可以被回收,其實不然,由於其被引用GC 系統無法回收這段記憶體而且我們也失去對這段記憶體的控制,這便導致了WelcomeActivity 的記憶體流失。
解決辦法:
①我們在 Activity 被銷毀時消除單例對它的引用。這種方法有所限制,並不是所有的情況都適合。例如類中存在非靜態屬性,則在不同時間調用可能導致其值錯亂。
在 ResourceReader 工具類中加入以下代碼:
public void reset(){ if(mInstance != null){ mInstance = null; context = null; } }
在 Activity 的銷毀方法中調用上邊新加入的方法:
@Overrideprotected void onDestroy() { ResourceReader.getInstance(this).reset(); super.onDestroy(); }
②在單例中我們儘可能的引用生命週期較長的對象,如 Application(推薦)改動也較少,只需要將 Context 改為 ApplicationContext。
public static ResourceReader getInstance(Context context){ if(mInstance == null){ //將 Context 改為 ApplicationContext mInstance = new ResourceReader(context.getApplicationContext()); } return mInstance; }
修改完畢之後我們再次運行程式看看結果:
再看我們的 Analyzer Tasks 的分析
②集合類造成記憶體流失:
如果某個集合是類的全域變數,如果該變數沒有相應的刪除機制則很有可能導致該集合佔用的記憶體只增不減。
類比一種情境,有一個 Bitmap 的列表在某個操作後會加入一張圖片,但是由於設計的缺陷該對象並沒有刪除元素的機制。
public class GatherActivity extends AppCompatActivity { private static final List<Bitmap> bitmapList = new ArrayList<>(); private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gather); textView = (TextView) findViewById(R.id.list_numer); } public void onClick(View view){ if(view.getId() == R.id.btn_add){ //加入Bitmap bitmapList.add(BitmapFactory.decodeResource(getResources(), R.drawable.welcome)); } textView.setText(String.format("共有 %d 個元素", bitmapList.size())); } @Override protected void onDestroy() { Log.v("ygl","Gather Activity On Destroy"); super.onDestroy(); }}
解決辦法:
①對於 final static 修飾符一定要慎用。
②對於集合一定要在特定的時機進行刪除元素,或清空,或轉儲本地,避免集合所佔記憶體無限制增長。
③非靜態內部類造成記憶體流失:
這是經常被我們忽略的一點,非靜態內部類預設會持有對外部類的引用,而該非靜態內部類有建立了一個靜態執行個體,如果沒有合理釋放則會造成記憶體泄露。
public class NearActivity extends AppCompatActivity { //非靜態內部類 User 建立的靜態執行個體 mUser private static User mUser = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_near); mUser = new User(); } @Override protected void onDestroy() { Log.v("ygl","Near Activity On Destroy"); super.onDestroy(); } class User{ private String name; private int age; }}
我們通過上邊的分析方法可以得出,在 NearActivity 銷毀後其記憶體並沒有被回收。
解決辦法:
①在合適的時機將內部類的靜態對象進行銷毀,如:
@Override protected void onDestroy() { Log.v("ygl","Near Activity On Destroy"); if(mUser != null){ mUser = null; } super.onDestroy(); }
②將內部類定義為靜態內部類,使其不與外部類建立關係。
④匿名內部類/非同步線程導致記憶體流失:
匿名內部類會持有一個外部類的引用,如果再將該引用傳入非同步線程,此線程與外部類的生命週期不再相同,就可能導致外部類對象記憶體流失。
舉一個我們經常用到的情境:
public class SyncActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sync); //建立匿名內部類 new Thread(new Runnable() { @Override public void run() { for(int i=0;i<1000;i++){ try{ Thread.sleep(1000); Log.v("ygl","i="+i); }catch (Exception e){} } } }).start(); //銷毀Activity finish(); } @Override protected void onDestroy() { Log.v("ygl","Sync Activity On Destroy"); super.onDestroy(); }}
大家可能已經猜到了,由於子線程在 Activity 銷毀後依然會繼續執行,而內部類 new Runnable() {};擁有對外部類 Activity的引用導致了Activity所佔記憶體無法被回收。
解決辦法:
①如果是在 Activity 結束後已沒有必要啟動並執行線程,在 onDestroy 中中斷子線程的運行。
②可以考慮用全域的線程池代替在類中聲明子線程。
⑤Handler 造成記憶體流失:
Handler 生命週期和 Activity 是不一致的,當 Activity 被 finish 時,順延強制任務的 Message 還會繼續執行存在於主線程中,它持有 Activity 的 Handler 引用,導致 Activity 無法被回收。
public class SampleActivity extends Activity { private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { ... } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler.postDelayed(new Runnable() { @Override public void run() { ... } }, 1000 * 60 * 10); finish(); }}
解決辦法:
①在 Activity 結束時可以清空不必要的 Message 訊息
②採用靜態內部類和弱引用結合的方式
總結:
- 對Activity 等組件的引用應該控制在Activity的生命週期內;如果不能則考慮使用 ApplicationContext,盡量避免Activity被外部長生命週期的對象引用。
- 盡量不要再靜態變數或靜態內部類中使用非靜態外部成員變數,即使必須使用,應在合適的時機將外部成員變數置空。
- Handler 資源釋放時可以清空Handler 裡面的訊息。
- 某些比較占記憶體的對象最好可以在使用完畢後主動的釋放,比如 Bitmap.Recycle(),清空數組等。
- 對於各種網路流,檔案流等要及時地關閉。
- 對於比較敏感的 單例 靜態對象 全域集合等要謹慎的考慮。
Android 記憶體流失