一:內存泄漏的概念
內存泄漏:內存不再GC的掌控中。也就是一個對象不再需要使用,本該被回收時,但是有另外一個對象持有該對象,導致對象不能回收。這種導致本該回收的對象不能回收且停留在堆內存中,這就是內存泄漏。
內存分配策略:
- 靜態內存:存放靜態數據,這塊內存是在編譯時就已經分配好的,在整個運行期間都存在。他主要存放靜態數據、全局的static數據和一些常量。
- 棧內存:程序執行是,局部變量的創建存儲區,執行結束後會自動釋放。
- 堆內存:也叫動態內存分配。有時可以用malloc或者new來申請分配一個內存。在C/C++可能需要自己負責釋放(java裏面直接依賴GC機制)。在C/C++這裏是可以自己掌控內存的,需要有很高的素養來解決內存的問題。java在這一塊貌似程序員沒有很好的方法自己去解決垃圾內存,需要的是編程的時候就要注意自己良好的編程習慣。
區別:1:堆是不連續的內存區域,堆空間比較靈活也特別大。
2: 棧式一塊連續的內存區域,大小是有操作系統覺決定的。
3:堆管理很麻煩,頻繁地new/remove會造成大量的內存碎片,這樣就會慢慢導致效率低下。對於棧的話,他先進後出,進出完全不會產生碎片,運行效率高且穩定。
public class Student{
int a = 1;
Dimen d1= new Dimen ();
public void XXX(){
int b = 1;//棧裏面
Dimen d2 = new Dimen ();
}
}
1.成員變量全部存儲在堆中(包括基本數據類型,引用及引用的對象實體)---因爲他們屬於類,類對象最終還是要被new出來的。
2.局部變量的基本數據類型和引用存儲於棧當中,引用的對象實體存儲在堆中。-----因爲他們屬於方法當中的變量,生命週期會隨着方法一起結束。
內存泄露,主要討論堆內存,他存放的就是引用指向的對象實體。
內存泄露(Memory Leak):
進程中某些對象已經沒有使用價值了,但是他們卻還可以直接或者間接地被引用到GC Root導致無法回收。
當內存泄露過多的時候,再加上應用本身佔用的內存,日積月累最終就會導致內存溢出OOM.
內存溢出(OOM):
當應用佔用的heap資源超過了Dalvik虛擬機分配的內存就會內存溢出。比如:加載大圖片。
二:內存泄漏的情況
OutOfMenoryError主要有以下幾種情況:
1:在ListView或者GridView 等加載大量數據或圖片,構造adapter沒有使用緩存ContentView
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder = null;
//如果convertView對象爲空則創建新對象,不爲空則複用
if (convertView == null) {
convertView = inflater.inflate(..., null);
// 創建 ViewHodler 對象
vHolder = new ViewHolder();
vHolder.img= (ImageView) convertView.findViewById(...);
vHolder.tv= (TextView) convertView
.findViewById(...);
// 將ViewHodler保存到Tag中
convertView.setTag(vHolder);
} else {
//當convertView不爲空時,通過getTag()得到View
vHolder = (ViewHolder) convertView.getTag();
}
// 給對象賦值,修改顯示的值
vHolder.img.setImageBitmap(...);
vHolder.tv.setText(...);
return convertView;
}
static class ViewHolder {
TextView tv;
ImageView img;
}
圖片非常佔用內存,一定要管理好內存,不然很容易內存溢出。
滑出去的圖片就回收,節省內存。看ListView的源碼----回收對象,還會重用ConvertView。
如果用戶反覆滑動或者下面還有同樣的圖片,就會造成多次重複IO(很耗時),
那麼需要緩存---平衡好內存大小和IO,算法和一些特殊的java類。
算法:lrucache(最近最少使用先回收)
特殊的java類:利於回收,StrongReference,SoftReference,WeakReference,PhatomReference
StrongReference強引用:
回收時機:從不回收 使用:對象的一般保存 生命週期:JVM停止的時候纔會終止
SoftReference,軟引用
回收時機:當內存不足的時候;使用:SoftReference<String>結合ReferenceQueue構造有效期短;生命週期:內存不足時終止
WeakReference,弱引用
回收時機:在垃圾回收的時候;使用:同軟引用; 生命週期:GC後終止
PhatomReference 虛引用
回收時機:在垃圾回收的時候;使用:合ReferenceQueue來跟蹤對象唄垃圾回收期回收的活動; 生命週期:GC後終止
開發時,爲了防止內存溢出,處理一些比較佔用內存大並且生命週期長的對象的時候,可以儘量使用軟引用和弱引用。
軟引用比LRU算法更加任性,回收量是比較大的,你無法控制回收哪些對象。
比如使用場景:默認頭像、默認圖標。
ListView或者GridView等要使用內存緩存+外部緩存(SD卡)
2:資源爲關閉引起的內存泄漏
BroadCastReceiver:registerReceiver()
和unregisterReceiver()
要成對出現,通常需要在Activity
的onDestory()
方法去取消註冊廣播接收者。
Cursor:操作完close。
Bitmap:使用後未調用recycle()。
IO流:用完後及時關閉。
自定義屬性attribute:attr.recycle()回收。
當不需要使用的時候,要記得及時釋放資源。否則就會內存泄露。
3:不需要用的監聽未移除會發生內存泄露
例子1:
// tv.setOnClickListener();//監聽執行完回收對象
//add監聽,放到集合裏面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean b) {
//監聽view的加載,view加載出來的時候,計算他的寬高等。
//計算完後,一定要移除這個監聽
tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
});
例子2:
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
//不需要用的時候記得移除監聽
sensorManager.unregisterListener(listener);
4:集合類泄漏
集合類中如果只有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局性的變量(比如類中的靜態屬性),那麼沒有 響應的刪除機制,很可能導致集合所佔用的內存只增不減。
5:無限循環動畫
沒有在onDestroy中停止動畫,否則Activity就會變成泄露對象。
比如:輪播圖效果。
6:Context
泄漏 這是一個很隱晦的OutOfMemoryError
的情況。先看一個Android官網提供的例子:
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
這段代碼效率很快,但同時又是極其錯誤的:
我們看一下setBackgroundDrawable(Drawable background)
的源碼:
public void setBackgroundDrawable(Drawable background) {
...
background.setCallback(this);
}
有background.setCallback(this);
方法,也就是說Drawable
擁有TextView
的引用,而TextView
又擁有Activity
*(Context類型)*的引用, 因爲sBackground
爲static
的,即使Activity
被銷燬,但是sBackground
的生命週期還沒走完,所以內存仍然不會被釋放。這樣就會有內存泄露了。 對,這樣想是對的,但是我們看一下setCallback
的源碼:
public final void setCallback(Callback cb) {
mCallback = new WeakReference<Callback>(cb);
}
我們會發現裏面使用了WeakReference
,所以不會存在內存泄露了,但是官網當時提供的例子明明說有泄露,這是因爲在3.0之後, 修復了這個內存泄露的問題,在3.0之前setCallback
,方法是沒有使用WeakReference
的,所以這種泄露的情況在3.0之前會發生,3.0之後已經被修復。
7: 線程也是造成內存泄露的一個重要的源頭。線程產生內存泄露的主要原因在於線程生命週期的不可控。我們來考慮下面一段代碼。
public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new MyThread().start();
}
private class MyThread extends Thread{
@Override
public void run() {
super.run();
//耗時的操作
}
}
}
假設MyThread
的run
函數是一個很費時的操作,當調用finish
的時候Activity
會銷燬掉嗎?
事實上由於我們的線程是Activity
的內部類,所以MyThread
中保存了Activity
的一個引用,當MyThread
的run
函數沒有結束時,MyThread
是不會被銷燬的, 因此它所引用的Activity
也不會被銷燬,因此就出現了內存泄露的問題。
8:單例造成的內存泄漏
由於單例的靜態特性使得其生命週期跟應用的生命週期一樣長,所以如果使用不恰當的話,很容易造成內存泄漏,比如:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
這裏如果傳入的是Activity
的Context
,當該Context
的Activity
退出後,由於其被單例對象引用,所以會導致Activity
無法被回收,就造成內存泄漏。
:9:儘量使用ApplicationContext
Context
引用的生命週期超過它本身的生命週期,也會導致Context
泄漏。 所以如果打算保存一個長時間的對象時儘量使用Application
這種Context
類型。 例如:
mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
按道理來說這種系統服務是不會有問題,但是有些廠商在修改的時候,可能會導致Context
無法被及時釋放。
10:Handler的使用,在Activity退出的時候注意移除(尤其是循環的時候)
public class ThreadDemo extends Activity {
private static final String TAG = "ThreadDemo";
private int count = 0;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
public void run() {
//爲了方便 查看,我們用Log打印出來
Log.e(TAG, Thread.currentThread().getName() + " " +count);
//每2秒執行一次
mHandler.postDelayed(mRunnable, 2000);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//通過Handler啓動線程
mHandler.post(mRunnable);
}
}
這樣在也會引發內存泄露。我們應該在onDestory
方法中移除Handler
,代碼如下:
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mRunnable);
}
11:由上面的Handler
可以引伸出來的匿名內部類、非靜態內部類和異步線程導致的內存泄漏。
下面看一個非靜態內部類創建靜態實例導致的內存泄漏
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
因爲非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態實例,該實例的聲明週期與應用的一樣長, 這就導致了靜態實例一直會持有Activity
的引用而造成內存泄漏。
下面再看一個匿名內部類和異步現成的現象:
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
上面的例子中ref1
對象是沒問題的,但是ref2
這個匿名類的實現對象中有外部類的引用,如果此時線程的生命週期與Activity
的不一致時就會造成了泄漏。
總結一下避免Contex
t泄漏應該注意的問題:
- 使用
getApplicationContext()
類型。 - 注意對
Context
的引用不要超過它本身的生命週期。 - 慎重的使用
static
關鍵字。 Activity
裏如果有線程或Handler
時,一定要在onDestroy()
裏及時停掉。
三:內存泄漏的判斷的工具
性能優化的幫助工具:
MAT,
Memory Monitor(屬於AndroidMonitor中一個模塊),
HeapTool(查看堆信息),
Allaction Tracking,
LeakCanary
Lint工具
1:Allaction Tracking
追蹤內存分配信息。可以很直觀地看到某個操作的內存是如何進行一步一步地分配的。
2:LeakCanary
Square公司
可以直接在手機端查看內存泄露的工具
實現原理:本質上還是用命令控制生成hprof文件分析檢查內存泄露。
然後發送通知。
Application
install()
LeakCanary
androidWatcher()
RefWatcher
new AndroidWatcherExecutor() --->dumpHeap()/analyze()(--->runAnalysis())--->Hprof文件分析
new AndroidHeapDumper()
new ServiceHeapDumpListener
3:Lint分析工具
Android Studio很方便 很好用。
檢測資源文件是否有沒有用到的資源。
檢測常見內存泄露
安全問題SDK版本安全問題
是否有費的代碼沒有用到
代碼的規範---甚至駝峯命名法也會檢測
自動生成的羅列出來
沒用的導包
可能的bug