內存泄露和內存優化
對于Android來說,每一個APP的內存是有限的。你過你的內存出現問題:泄露,長期占用過高,就會導致app易于被殺掉。頻繁的gc導致app卡頓等現象。
常見情況
-
Activity的Context的使用
- 界面的Context靜態化
- 單例式將界面的Context作為初始化入參數,并且在單例模式保存
- 特殊的,在Android 6.0中,不能使用Activity的Context通過接口getSystemService()來獲取各種Manager,如下所示:
AActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
如上所示,在Android 6.0 中就會造成內存泄露
- 非靜態內部類持有外部類的引用
在Java中,非靜態內部類(包括匿名內部類)都會持有外部類(一般是指Activity等頁面)的引用,當兩者的生命周期出現不一致的時候,很容易導致內存泄露。
如下所示,非常常見的幾種情況:
Hanlder
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg)
{
super.handleMessage(msg);
}
};
這里的Handler會引用Activity的引用,當handler調用postDelay的時候,若Activity已經finish掉了,因為這個 handler 會在一段時間內繼續被 main Looper 持有,導致引用仍然存在,在這段時間內,如果內存吃緊至超出,是很危險的。
Thread
public class ThreadActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyThread().start();
}
private class MyThread extends Thread {
@Override
public void run() {
super.run();
dosomthing();
}
}
private void dosomthing(){
}
}
假設MyThread的run函數是一個很費時的操作,當我們開啟該線程后,將設備的橫屏變為了豎屏,一般情況下當屏幕轉換時會重新創建Activity,按照我們的想法,老的Activity應該會被銷毀才對,然而事實上并非如此。由于我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷毀的,因此它所引用的老的Activity也不會被銷毀,因此就出現了內存泄露的問題。
Runnable
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區別是,ref2使用了匿名內部類,也就是說當前的Activity會被ref2所應用,如果將這個引用傳入到了一個異步線程,該線程的生命周期與Activity的生命周期不一致的時候,就會導致內存泄露。
- Static變量造成內存泄露
1. 界面類的靜態化: 靜態Activity
2. 界面中View的靜態化: 靜態View
界面中View的靜態化一定會導致頁面內存泄露。界面中的View都是持有界面引用的,靜態變量的生命周期與整個app的生命周期一致。
3. 非靜態內部類的靜態化
具體的 如下所示:
public class MainActivity extends AppCompatActivity {
private static Drawable sDrawable;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView lableView = new TextView(this);
if(sDrawable == null) {
sDrawable = getDrawable(R.drawable.icon);
}
labelView.setBackgroundDrawable(sDrawable);
setContentView(lableView);
}
}
View的setBackgroundDrawable()的源碼如下所示:
public void setBackgroundDrawable(Drawable background) {
...
if (background != null) {
...
background.setCallback(this);
...
} else {
...
}
...
}
其中有一個background.setCallback(this);,所以這就導致這個靜態變量指向的對象又持有了TextView這個對象的引用,TextView持有的確實整個Activity的引用。這樣就導致了內存泄露。
我們再來看一個例子:
public class MainActivity extends AppCompatActivity {
private static InnerClass sInnerClass;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
sHello = new Hello();
}
public class InnerClass {}
}
靜態的非靜態內部類對象sInnerClass持有了外部Acitivity的引用,當屏幕發生變化時,不會被釋放。
-
<font size = 5>資源沒有關閉</font>
1. Cursor游標沒有關閉
數據庫中才操作經常碰到cursor。
2. InputStream、OutputStream等沒有關閉
文件讀寫、Socket讀寫等經常碰到
3. 注冊的廣播等沒有unRegister
4. 一些CallBack的Listener沒有被清除,舉例:
void registerListener() {
SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
Sensor snedor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListneer(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
getSystemService負責執行某些后臺任務,或為硬件提供接口,如果context對象想要在服務內部的事件發生時被通知,需要注冊監聽器。然而這讓服務持有了activity的引用,如果activity銷毀時沒有取消注冊,那么你的activity就泄露了。
View添加到沒有刪除機制的容器中
屬性動畫導致的內存泄露
如果你設置你的動畫為無限循環,而且沒有在onDestroy中停止該動畫,那么動畫會一直播放下去,Activity的View會被動畫吃持有,而View持有了Activiy。從而導致內存泄露。<font size = 5>過期引用</font>
當一個數組擴容后又被縮減,比如size從0->200->100(一個棧先增長,后收縮),那么元素的index>=100的那些元素(被Pop掉的)都算是過期的元素,那些引用就是過期的引用(永遠不會再被接觸的應用)-來自Effective Java
public Object pop(){
if(size==0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; //消除過期引用
return result;
}
由于過期引用的存在,GC并不會去回收他們,我們需要手動的釋放他們。
內存溢出和內存的查看方法
- 使用第三方開源庫
LeakCanary
在這里就不做具體的介紹了。網上的使用demo:
leakCanary Demo
- adb shell命令
通過以下命令可以查看你APP的內存使用情況已經Activity和View等的個數情況,具體的
adb shell dumpsys meminfo packagename
其中,packagename就是你程序的報名,具體的示例,如下圖所示:
如上圖所示:
上面部分顯示的是你的app所占用的內存總數(主要是看TOTAL,內存所實際占用的值)
下面的部分可以看到你的一些對象的個數:如Views、Activities等。
當你進入一個acitivity的時候,activity的個數會增加,退出后會減少,如果只增加、不減少,就說明出現了內存泄露的問題。 (經過實際的測試,這個對有些手機,好不管用,就算我寫個demo:只有一個Activity,什么也沒有做,進來、退出、進來、activities個數會變大,不會立即變小,需要等一段時間才會變小)
- DDMS
DDMS是Android開發環境中的Dalvik虛擬機(andoid4.4之前,4.4及其之后引入了ART虛擬機)調試監控服務。
1. update heap
對一個activity進入退出反復多次看data object是否穩定在一個范圍
2. MAT(Memory Analyzer Tool)
dump hprof file : 點擊后等待一會,會生成一個hprof文件。插件版本的MAT可以直接打開該文件,否則需要進行一步轉換操作。 提供了這個工具 hprof-conv (位于 sdk/tools下), 轉換命令如下所示:
./hprof-conv xxx-a.hprof xxx-b.hprof
最后通過DDMS-File-open,打開的hprof文件即可進行分析內存泄露相關。
內存優化建議
- 了解你機器的內存情況
通過以下代碼可以查看每個進程可用的最大內存,即heapgrowthlimit值
ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M為單位
通過以下代碼可以獲取 應用程序的最大可用內存
long maxMemory = Runtime.getRuntime().maxMemeory(); //以字節為單位
兩者的區別:
單位不一致 前者以M為單位,后者以字節為單位。
具體的以lenovo的一款手機(S850T, Android版本為4.4.2)為例: 經過測試兩者得到的值一致均是128M。
使用場景
當你進行圖片加載的時候,都會使用到LRUCache,初始化的時候設置緩存的大小。一般來說都設置為當前最大內存的1/8,如果你就是一個圖片應用你直接1/4也可以。
long cacheSize = Runtime.getRuntime().maxMemeory();
mLruCache = new LruCache<String, Bitmap>(cacheSize)
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
};
};
- 當界面不可見、內存緊張的時候釋放內存
android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前為onLowMemory) ,系統會根據不同的內存狀態來毀掉,參數 level 代表了你app的不同狀態,Application、Activity、Fragment、Service、ContentProvider均可以響應。具體如下:
TRIM_MEMORY_UI_HIDDEN: 應用程序被隱藏了,如按了Home或者Back導致UI不可見,這個時候,我們應該釋放一些內存。
以下三個是我們的應用程序真正運行時的回調:
TRIM_MEMORY_RUNNING_MODERATE: 程序正常運行,并不會被殺掉,但是手機的內存有點低了,系統可能開始根據LRU規則來殺死進程了。
TRIM_MEMORY_RUNNING_LOW: 程序正常運行,并不會被殺掉,但是手機內存非常的低了,應該釋放一些資源了,否則影響性能。
TRIM_MEMORY_RUNNING_CRITICAL: 程序正在運行,但是系統已經根據LRU殺死了大部分緩存的進程了,此時我們需要釋放內存,否則系統可能會干掉你。
以下三個是當應用程序是緩存時候的回調:
TRIM_MEMORY_BACKGROUND: 內存不足,并且該進程是后臺進程。
TRIM_MEMORY_MODERATE: 內存不足,并且該進程在后臺進程列表的中部。
TRIM_MEMORY_COMPLETE:內存不足,并且該進程在后臺進程列表的最后一個,馬上就要被清理了,這個時候應該把一切盡可能釋放的都釋放掉。
通常在我們開始進行架構設計的時候,就要考慮到哪些東西是要常駐的,哪些東西是緩存后要被清理, 一般情況下,以下資源都要被清理:
緩存:包括文件緩存、圖片的緩存、比如第三方圖片緩存庫。
一些動態生成的View: 比如一般應用的圖片輪播View,在你的應用隱藏后,根本不需要輪播。
案例分析:
1. LRUCache緩存的清理方式:trimToSize()接口可以重新設置緩存的大小。evictAll()接口可以清楚所有的LRUCache緩存內容。
2. 暴力清理界面中的View
-
圖片資源的壓縮
1. res中資源到壓縮: 使用有損壓縮工具,比如:tinyPng,壓縮后的圖片肉眼根本看不出來,壓縮率可以達到50%以上。
2. BitmapFactory的壓縮。
通過BitmapFactory的Options設置,降低采樣率,壓縮圖片到適合的大小,同時注意使用若引用和緩存機制。
Bitmap.Config設置圖片的格式為RGB565,這個設置肉眼是看不出色彩的丟失,而且比RGB8888占存小的多。
使用BitmapFactory.Options.inBitmap字段。如果這個選項被設置,那么使用該Options 的decode方法將會嘗試復用一個已經存在的bitmap來加載新的bitmap。這意味著bitmap的內存將被復用,避免分配和釋放內存來提升性能。然后,使用inBitmap有一些限制。特別是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用該特性。具體的見使用示例
3. 將圖片資源放在合適的drawable目錄下。
-
使用Android優化過的類和集合
1. SparseArrry<T>來替代HashMap<int, T>
2. LongSparseArray<T>, key為long,替代HashMap<long, T>
3. SimpleArrayMap<K, T>和ArrayMap<K,T>替代HashMap<K, T>, ArrayMap是通過時間來換取效率,在數千之內建議使用ArrayMap。
- 避免創建不必要的對象
在短時間內創建了大量的對象,然后有釋放,這樣就引起了內存抖動。頻繁的引起GC操作,會導致內存的卡頓。
1. 字符串的拼接:StringBuffer(非線程安全)和StringBuilder(線程安全)的使用
2. 自定義View中不要在onDraw中定義畫筆等對象
3. 在循環函數內避免創建重復的對象,將多個函數都經常用到的不可變對象拿出來統一進行初始化,在一開始寫的時候就要特別的注意,否則后邊修改起來很是麻煩(主要是再找到他很麻煩)
4. 在循環的內部不要使用try catch操作,將其拿到外面來。
5. 不要在循環中進行文件的操作:比如判斷文件是否存在,這相對是一個很耗時的操作
案例說明
SimpleDateFromat是用來時間轉換的,一般的,開發者都會定義個專門用于時間轉化的static的函數:
public static String paserTimeToYM(long time)
{
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
return format.format(new Date(time));
}
假如你在for循環中調用此函數。就不停的重復創建SimpleDateFromat對象。你應該將對象創建拿出來,放在類中,或者是重新定義一個時間轉換函數,入慘為已經創建好的SimpleDateFormat對象。
還需要注意的是:假如你的循環量很大,不建議在for循環中進行時間轉換,而是在你用到的時候才進行轉換,比如顯示出來。
- 不要擴大變量的作用域
classs A
{
private B mB;
public A(B b) {
this.mB = b;
//就在構造函數中進行了對mB進行了一些操作
}
//后續再也沒有用到過mB
}
class B
{
public B() {
}
public static void main(String[] args)
{
}
}
如上所示的簡單代碼:類A的構造函數中,傳入了類B的對象,并且類A中定義了成員變量mB,但是mB就在構造函數中用了一下,后續再也沒有用,在類A中mB的生命周期和A一致。本來mB的作用域就在構造函數,結果擴大為整個類。
- 不要讓生命周期比Activity長的對象持有Activity的引用
這樣的錯誤很多,比如:將Activity的Context傳給單例模式,毫不知情的將Activity的Context傳給非靜態內部類或者是匿名內部類。
- 盡量的使用Application的Context
Application的生命周期是整個app,他會一直在。
1. 在界面類中直接使用getApplicationContext。
2. 在其他地方使用MyApplication(extends Application)的getInstance操作。如下所示:
public class MyApplication extends Application
{
private static Context sContext;
@Override
public void onCreate()
{
Log.d(tag, "onCreate");
sContext = this;
}
public static Context getAppContext()
{
return sContext;
}
}
總之一句話:能使用Application的Context,就不要使用Activity的。
-
移除回調
1. handler的removeCallbacksAndMessages(null)
2. setXXXCallback(null)、 setXXXListener(null),需要注意的是,要進行callback調用的地方就需要進行判斷了
- 常量的使用
關于enum和static。Android強烈建議不要使用enum,他會使得內存消耗變大為原來的2倍以上。
-
使用代碼混淆剔除不需要的代碼
jar包的混淆:使用proguardgui.bat
jar包的合并:使用插件fatjar
請使用靜態內部類+WeakReference的方式
非靜態內部類和匿名內部類會持有頁面的應用,請使用靜態內部類,并將頁面的引用通過WeakReference的方式傳遞過去。
- 合理的使用多進程
android對單個進程都有一個內存允許的最大內存限制。加入你在你的app中又啟動一個進程,這樣你的內存限制就變為了原來的2倍。
啟動多進程的方法很簡單,只需要在AndroidManifest.xml聲明的四大組件的標簽中增加"android:process"屬性即可。
進程分為兩種:私有進程和全局進程。私有進程在名稱簽名添加冒號即可。
但是多進程有一些需要注意的地方:
1. Application的onCreate會被調用多次。一般程序會將程序的一些初始化的操作放在這里,這點需要注意。
2. 多進程之間的通訊必須使用AIDL接口,需要注意的一點是:AIDL之間傳遞大量數據是有一個限制的。 傳遞內容過大會出現:TransactionToolLargeException。官方文檔說明:最大的限制為1M。
3. 多進程導致 靜態成員、單例模式和SharedPreference 都變的不可靠。
4. 多進程之間傳遞數據的效率:有些手機在傳遞大量數據的時候,效率很差。
5. 多進程傳遞對象需要實現序列化操作。
6. AIDL支持的數據類型:基本數據類型;String和CharSequence;List僅僅支持ArrayList,里面的每一個對象都必須支持序列化,Map只支持HashMap,里面的key和value都必須支持序列化(必須被AIDL支持)。
7. AIDL服務端可以使用CopyOnWriteArrayList和ConcurrentHashMap來進行自動線程同步,客戶端拿到的依然是ArrayList和HashMap。
8.AIDL服務端和客戶端之間做監聽器,服務端需要使用RemoteCallbackList,否則客戶端的監聽器無法收到通知(因為服務端實質還是一份新的序列化后的監聽器實例,并不是客戶端那份)。
9.客戶端調用遠程服務方法時,因為遠程方法運行在服務端的binder線程池中,同時客戶端線程會被掛起,所以如果該方法過于耗時,而客戶端又是UI線程,會導致ANR,所以當確認該遠程方法是耗時操作時,應避免客戶端在UI線程中調用該方法。同理,當服務器調用客戶端的listener方法時,該方法也運行在客戶端的binder線程池中,所以如果該方法也是耗時操作,請確認運行在服務端的非UI線程中。另外,因為客戶端的回調listener運行在binder線程池中,所以更新UI需要用到handler。
我們將在進程常駐中進行簡單的示例分析,實現多進程的相互喚醒操作。
- 請不要使用注解框架
程序注解框架極大的方便了程序開發者,不需要開發者大量的寫findViewById(), setOnclickListener()等方法,但是程序注解框架是將類中的所有相關方法都緩存在內容中不會釋放,這些內存就會越來越大,從而得不到釋放。而且一般程序注解方法都是用到了Java的反射機制。這個是不建議使用的(雖然有時候反射不得不使用)。