內(nèi)存泄露和內(nèi)存優(yōu)化
對(duì)于Android來說,每一個(gè)APP的內(nèi)存是有限的。你過你的內(nèi)存出現(xiàn)問題:泄露,長期占用過高,就會(huì)導(dǎo)致app易于被殺掉。頻繁的gc導(dǎo)致app卡頓等現(xiàn)象。
常見情況
-
Activity的Context的使用
- 界面的Context靜態(tài)化
- 單例式將界面的Context作為初始化入?yún)?shù),并且在單例模式保存
- 特殊的,在Android 6.0中,不能使用Activity的Context通過接口getSystemService()來獲取各種Manager,如下所示:
AActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);
如上所示,在Android 6.0 中就會(huì)造成內(nèi)存泄露
- 非靜態(tài)內(nèi)部類持有外部類的引用
在Java中,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)都會(huì)持有外部類(一般是指Activity等頁面)的引用,當(dāng)兩者的生命周期出現(xiàn)不一致的時(shí)候,很容易導(dǎo)致內(nèi)存泄露。
如下所示,非常常見的幾種情況:
Hanlder
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg)
{
super.handleMessage(msg);
}
};
這里的Handler會(huì)引用Activity的引用,當(dāng)handler調(diào)用postDelay的時(shí)候,若Activity已經(jīng)finish掉了,因?yàn)檫@個(gè) handler 會(huì)在一段時(shí)間內(nèi)繼續(xù)被 main Looper 持有,導(dǎo)致引用仍然存在,在這段時(shí)間內(nèi),如果內(nèi)存吃緊至超出,是很危險(xiǎn)的。
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(){
}
}
假設(shè)MyThread的run函數(shù)是一個(gè)很費(fèi)時(shí)的操作,當(dāng)我們開啟該線程后,將設(shè)備的橫屏變?yōu)榱素Q屏,一般情況下當(dāng)屏幕轉(zhuǎn)換時(shí)會(huì)重新創(chuàng)建Activity,按照我們的想法,老的Activity應(yīng)該會(huì)被銷毀才對(duì),然而事實(shí)上并非如此。由于我們的線程是Activity的內(nèi)部類,所以MyThread中保存了Activity的一個(gè)引用,當(dāng)MyThread的run函數(shù)沒有結(jié)束時(shí),MyThread是不會(huì)被銷毀的,因此它所引用的老的Activity也不會(huì)被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。
Runnable
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是,ref2使用了匿名內(nèi)部類,也就是說當(dāng)前的Activity會(huì)被ref2所應(yīng)用,如果將這個(gè)引用傳入到了一個(gè)異步線程,該線程的生命周期與Activity的生命周期不一致的時(shí)候,就會(huì)導(dǎo)致內(nèi)存泄露。
- Static變量造成內(nèi)存泄露
1. 界面類的靜態(tài)化: 靜態(tài)Activity
2. 界面中View的靜態(tài)化: 靜態(tài)View
界面中View的靜態(tài)化一定會(huì)導(dǎo)致頁面內(nèi)存泄露。界面中的View都是持有界面引用的,靜態(tài)變量的生命周期與整個(gè)app的生命周期一致。
3. 非靜態(tài)內(nèi)部類的靜態(tài)化
具體的 如下所示:
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 {
...
}
...
}
其中有一個(gè)background.setCallback(this);,所以這就導(dǎo)致這個(gè)靜態(tài)變量指向的對(duì)象又持有了TextView這個(gè)對(duì)象的引用,TextView持有的確實(shí)整個(gè)Activity的引用。這樣就導(dǎo)致了內(nèi)存泄露。
我們?cè)賮砜匆粋€(gè)例子:
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 {}
}
靜態(tài)的非靜態(tài)內(nèi)部類對(duì)象sInnerClass持有了外部Acitivity的引用,當(dāng)屏幕發(fā)生變化時(shí),不會(huì)被釋放。
-
<font size = 5>資源沒有關(guān)閉</font>
1. Cursor游標(biāo)沒有關(guān)閉
數(shù)據(jù)庫中才操作經(jīng)常碰到cursor。
2. InputStream、OutputStream等沒有關(guān)閉
文件讀寫、Socket讀寫等經(jīng)常碰到
3. 注冊(cè)的廣播等沒有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負(fù)責(zé)執(zhí)行某些后臺(tái)任務(wù),或?yàn)橛布峁┙涌冢绻鹀ontext對(duì)象想要在服務(wù)內(nèi)部的事件發(fā)生時(shí)被通知,需要注冊(cè)監(jiān)聽器。然而這讓服務(wù)持有了activity的引用,如果activity銷毀時(shí)沒有取消注冊(cè),那么你的activity就泄露了。
View添加到?jīng)]有刪除機(jī)制的容器中
屬性動(dòng)畫導(dǎo)致的內(nèi)存泄露
如果你設(shè)置你的動(dòng)畫為無限循環(huán),而且沒有在onDestroy中停止該動(dòng)畫,那么動(dòng)畫會(huì)一直播放下去,Activity的View會(huì)被動(dòng)畫吃持有,而View持有了Activiy。從而導(dǎo)致內(nèi)存泄露。<font size = 5>過期引用</font>
當(dāng)一個(gè)數(shù)組擴(kuò)容后又被縮減,比如size從0->200->100(一個(gè)棧先增長,后收縮),那么元素的index>=100的那些元素(被Pop掉的)都算是過期的元素,那些引用就是過期的引用(永遠(yuǎn)不會(huì)再被接觸的應(yīng)用)-來自Effective Java
public Object pop(){
if(size==0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; //消除過期引用
return result;
}
由于過期引用的存在,GC并不會(huì)去回收他們,我們需要手動(dòng)的釋放他們。
內(nèi)存溢出和內(nèi)存的查看方法
- 使用第三方開源庫
LeakCanary
在這里就不做具體的介紹了。網(wǎng)上的使用demo:
leakCanary Demo
- adb shell命令
通過以下命令可以查看你APP的內(nèi)存使用情況已經(jīng)Activity和View等的個(gè)數(shù)情況,具體的
adb shell dumpsys meminfo packagename
其中,packagename就是你程序的報(bào)名,具體的示例,如下圖所示:

如上圖所示:
上面部分顯示的是你的app所占用的內(nèi)存總數(shù)(主要是看TOTAL,內(nèi)存所實(shí)際占用的值)
下面的部分可以看到你的一些對(duì)象的個(gè)數(shù):如Views、Activities等。
當(dāng)你進(jìn)入一個(gè)acitivity的時(shí)候,activity的個(gè)數(shù)會(huì)增加,退出后會(huì)減少,如果只增加、不減少,就說明出現(xiàn)了內(nèi)存泄露的問題。 (經(jīng)過實(shí)際的測(cè)試,這個(gè)對(duì)有些手機(jī),好不管用,就算我寫個(gè)demo:只有一個(gè)Activity,什么也沒有做,進(jìn)來、退出、進(jìn)來、activities個(gè)數(shù)會(huì)變大,不會(huì)立即變小,需要等一段時(shí)間才會(huì)變小)
- DDMS
DDMS是Android開發(fā)環(huán)境中的Dalvik虛擬機(jī)(andoid4.4之前,4.4及其之后引入了ART虛擬機(jī))調(diào)試監(jiān)控服務(wù)。
1. update heap

對(duì)一個(gè)activity進(jìn)入退出反復(fù)多次看data object是否穩(wěn)定在一個(gè)范圍
2. MAT(Memory Analyzer Tool)

dump hprof file : 點(diǎn)擊后等待一會(huì),會(huì)生成一個(gè)hprof文件。插件版本的MAT可以直接打開該文件,否則需要進(jìn)行一步轉(zhuǎn)換操作。 提供了這個(gè)工具 hprof-conv (位于 sdk/tools下), 轉(zhuǎn)換命令如下所示:
./hprof-conv xxx-a.hprof xxx-b.hprof
最后通過DDMS-File-open,打開的hprof文件即可進(jìn)行分析內(nèi)存泄露相關(guān)。
內(nèi)存優(yōu)化建議
- 了解你機(jī)器的內(nèi)存情況
通過以下代碼可以查看每個(gè)進(jìn)程可用的最大內(nèi)存,即heapgrowthlimit值
ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M為單位
通過以下代碼可以獲取 應(yīng)用程序的最大可用內(nèi)存
long maxMemory = Runtime.getRuntime().maxMemeory(); //以字節(jié)為單位
兩者的區(qū)別:
單位不一致 前者以M為單位,后者以字節(jié)為單位。
具體的以lenovo的一款手機(jī)(S850T, Android版本為4.4.2)為例: 經(jīng)過測(cè)試兩者得到的值一致均是128M。
使用場景
當(dāng)你進(jìn)行圖片加載的時(shí)候,都會(huì)使用到LRUCache,初始化的時(shí)候設(shè)置緩存的大小。一般來說都設(shè)置為當(dāng)前最大內(nèi)存的1/8,如果你就是一個(gè)圖片應(yīng)用你直接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();
};
};
- 當(dāng)界面不可見、內(nèi)存緊張的時(shí)候釋放內(nèi)存
android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前為onLowMemory) ,系統(tǒng)會(huì)根據(jù)不同的內(nèi)存狀態(tài)來毀掉,參數(shù) level 代表了你app的不同狀態(tài),Application、Activity、Fragment、Service、ContentProvider均可以響應(yīng)。具體如下:
TRIM_MEMORY_UI_HIDDEN: 應(yīng)用程序被隱藏了,如按了Home或者Back導(dǎo)致UI不可見,這個(gè)時(shí)候,我們應(yīng)該釋放一些內(nèi)存。
以下三個(gè)是我們的應(yīng)用程序真正運(yùn)行時(shí)的回調(diào):
TRIM_MEMORY_RUNNING_MODERATE: 程序正常運(yùn)行,并不會(huì)被殺掉,但是手機(jī)的內(nèi)存有點(diǎn)低了,系統(tǒng)可能開始根據(jù)LRU規(guī)則來殺死進(jìn)程了。
TRIM_MEMORY_RUNNING_LOW: 程序正常運(yùn)行,并不會(huì)被殺掉,但是手機(jī)內(nèi)存非常的低了,應(yīng)該釋放一些資源了,否則影響性能。
TRIM_MEMORY_RUNNING_CRITICAL: 程序正在運(yùn)行,但是系統(tǒng)已經(jīng)根據(jù)LRU殺死了大部分緩存的進(jìn)程了,此時(shí)我們需要釋放內(nèi)存,否則系統(tǒng)可能會(huì)干掉你。
以下三個(gè)是當(dāng)應(yīng)用程序是緩存時(shí)候的回調(diào):
TRIM_MEMORY_BACKGROUND: 內(nèi)存不足,并且該進(jìn)程是后臺(tái)進(jìn)程。
TRIM_MEMORY_MODERATE: 內(nèi)存不足,并且該進(jìn)程在后臺(tái)進(jìn)程列表的中部。
TRIM_MEMORY_COMPLETE:內(nèi)存不足,并且該進(jìn)程在后臺(tái)進(jìn)程列表的最后一個(gè),馬上就要被清理了,這個(gè)時(shí)候應(yīng)該把一切盡可能釋放的都釋放掉。
通常在我們開始進(jìn)行架構(gòu)設(shè)計(jì)的時(shí)候,就要考慮到哪些東西是要常駐的,哪些東西是緩存后要被清理, 一般情況下,以下資源都要被清理:
緩存:包括文件緩存、圖片的緩存、比如第三方圖片緩存庫。
一些動(dòng)態(tài)生成的View: 比如一般應(yīng)用的圖片輪播View,在你的應(yīng)用隱藏后,根本不需要輪播。
案例分析:
1. LRUCache緩存的清理方式:trimToSize()接口可以重新設(shè)置緩存的大小。evictAll()接口可以清楚所有的LRUCache緩存內(nèi)容。
2. 暴力清理界面中的View
-
圖片資源的壓縮
1. res中資源到壓縮: 使用有損壓縮工具,比如:tinyPng,壓縮后的圖片肉眼根本看不出來,壓縮率可以達(dá)到50%以上。
2. BitmapFactory的壓縮。
通過BitmapFactory的Options設(shè)置,降低采樣率,壓縮圖片到適合的大小,同時(shí)注意使用若引用和緩存機(jī)制。
Bitmap.Config設(shè)置圖片的格式為RGB565,這個(gè)設(shè)置肉眼是看不出色彩的丟失,而且比RGB8888占存小的多。
使用BitmapFactory.Options.inBitmap字段。如果這個(gè)選項(xiàng)被設(shè)置,那么使用該Options 的decode方法將會(huì)嘗試復(fù)用一個(gè)已經(jīng)存在的bitmap來加載新的bitmap。這意味著bitmap的內(nèi)存將被復(fù)用,避免分配和釋放內(nèi)存來提升性能。然后,使用inBitmap有一些限制。特別是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用該特性。具體的見使用示例
3. 將圖片資源放在合適的drawable目錄下。
-
使用Android優(yōu)化過的類和集合
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是通過時(shí)間來換取效率,在數(shù)千之內(nèi)建議使用ArrayMap。
- 避免創(chuàng)建不必要的對(duì)象
在短時(shí)間內(nèi)創(chuàng)建了大量的對(duì)象,然后有釋放,這樣就引起了內(nèi)存抖動(dòng)。頻繁的引起GC操作,會(huì)導(dǎo)致內(nèi)存的卡頓。
1. 字符串的拼接:StringBuffer(非線程安全)和StringBuilder(線程安全)的使用
2. 自定義View中不要在onDraw中定義畫筆等對(duì)象
3. 在循環(huán)函數(shù)內(nèi)避免創(chuàng)建重復(fù)的對(duì)象,將多個(gè)函數(shù)都經(jīng)常用到的不可變對(duì)象拿出來統(tǒng)一進(jìn)行初始化,在一開始寫的時(shí)候就要特別的注意,否則后邊修改起來很是麻煩(主要是再找到他很麻煩)
4. 在循環(huán)的內(nèi)部不要使用try catch操作,將其拿到外面來。
5. 不要在循環(huán)中進(jìn)行文件的操作:比如判斷文件是否存在,這相對(duì)是一個(gè)很耗時(shí)的操作
案例說明
SimpleDateFromat是用來時(shí)間轉(zhuǎn)換的,一般的,開發(fā)者都會(huì)定義個(gè)專門用于時(shí)間轉(zhuǎn)化的static的函數(shù):
public static String paserTimeToYM(long time)
{
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
return format.format(new Date(time));
}
假如你在for循環(huán)中調(diào)用此函數(shù)。就不停的重復(fù)創(chuàng)建SimpleDateFromat對(duì)象。你應(yīng)該將對(duì)象創(chuàng)建拿出來,放在類中,或者是重新定義一個(gè)時(shí)間轉(zhuǎn)換函數(shù),入慘為已經(jīng)創(chuàng)建好的SimpleDateFormat對(duì)象。
還需要注意的是:假如你的循環(huán)量很大,不建議在for循環(huán)中進(jìn)行時(shí)間轉(zhuǎn)換,而是在你用到的時(shí)候才進(jìn)行轉(zhuǎn)換,比如顯示出來。
- 不要擴(kuò)大變量的作用域
classs A
{
private B mB;
public A(B b) {
this.mB = b;
//就在構(gòu)造函數(shù)中進(jìn)行了對(duì)mB進(jìn)行了一些操作
}
//后續(xù)再也沒有用到過mB
}
class B
{
public B() {
}
public static void main(String[] args)
{
}
}
如上所示的簡單代碼:類A的構(gòu)造函數(shù)中,傳入了類B的對(duì)象,并且類A中定義了成員變量mB,但是mB就在構(gòu)造函數(shù)中用了一下,后續(xù)再也沒有用,在類A中mB的生命周期和A一致。本來mB的作用域就在構(gòu)造函數(shù),結(jié)果擴(kuò)大為整個(gè)類。
- 不要讓生命周期比Activity長的對(duì)象持有Activity的引用
這樣的錯(cuò)誤很多,比如:將Activity的Context傳給單例模式,毫不知情的將Activity的Context傳給非靜態(tài)內(nèi)部類或者是匿名內(nèi)部類。
- 盡量的使用Application的Context
Application的生命周期是整個(gè)app,他會(huì)一直在。
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的。
-
移除回調(diào)
1. handler的removeCallbacksAndMessages(null)
2. setXXXCallback(null)、 setXXXListener(null),需要注意的是,要進(jìn)行callback調(diào)用的地方就需要進(jìn)行判斷了
- 常量的使用
關(guān)于enum和static。Android強(qiáng)烈建議不要使用enum,他會(huì)使得內(nèi)存消耗變大為原來的2倍以上。
-
使用代碼混淆剔除不需要的代碼
jar包的混淆:使用proguardgui.bat
jar包的合并:使用插件fatjar
請(qǐng)使用靜態(tài)內(nèi)部類+WeakReference的方式
非靜態(tài)內(nèi)部類和匿名內(nèi)部類會(huì)持有頁面的應(yīng)用,請(qǐng)使用靜態(tài)內(nèi)部類,并將頁面的引用通過WeakReference的方式傳遞過去。
- 合理的使用多進(jìn)程
android對(duì)單個(gè)進(jìn)程都有一個(gè)內(nèi)存允許的最大內(nèi)存限制。加入你在你的app中又啟動(dòng)一個(gè)進(jìn)程,這樣你的內(nèi)存限制就變?yōu)榱嗽瓉淼?倍。
啟動(dòng)多進(jìn)程的方法很簡單,只需要在AndroidManifest.xml聲明的四大組件的標(biāo)簽中增加"android:process"屬性即可。
進(jìn)程分為兩種:私有進(jìn)程和全局進(jìn)程。私有進(jìn)程在名稱簽名添加冒號(hào)即可。
但是多進(jìn)程有一些需要注意的地方:
1. Application的onCreate會(huì)被調(diào)用多次。一般程序會(huì)將程序的一些初始化的操作放在這里,這點(diǎn)需要注意。
2. 多進(jìn)程之間的通訊必須使用AIDL接口,需要注意的一點(diǎn)是:AIDL之間傳遞大量數(shù)據(jù)是有一個(gè)限制的。 傳遞內(nèi)容過大會(huì)出現(xiàn):TransactionToolLargeException。官方文檔說明:最大的限制為1M。
3. 多進(jìn)程導(dǎo)致 靜態(tài)成員、單例模式和SharedPreference 都變的不可靠。
4. 多進(jìn)程之間傳遞數(shù)據(jù)的效率:有些手機(jī)在傳遞大量數(shù)據(jù)的時(shí)候,效率很差。
5. 多進(jìn)程傳遞對(duì)象需要實(shí)現(xiàn)序列化操作。
6. AIDL支持的數(shù)據(jù)類型:基本數(shù)據(jù)類型;String和CharSequence;List僅僅支持ArrayList,里面的每一個(gè)對(duì)象都必須支持序列化,Map只支持HashMap,里面的key和value都必須支持序列化(必須被AIDL支持)。
7. AIDL服務(wù)端可以使用CopyOnWriteArrayList和ConcurrentHashMap來進(jìn)行自動(dòng)線程同步,客戶端拿到的依然是ArrayList和HashMap。
8.AIDL服務(wù)端和客戶端之間做監(jiān)聽器,服務(wù)端需要使用RemoteCallbackList,否則客戶端的監(jiān)聽器無法收到通知(因?yàn)榉?wù)端實(shí)質(zhì)還是一份新的序列化后的監(jiān)聽器實(shí)例,并不是客戶端那份)。
9.客戶端調(diào)用遠(yuǎn)程服務(wù)方法時(shí),因?yàn)檫h(yuǎn)程方法運(yùn)行在服務(wù)端的binder線程池中,同時(shí)客戶端線程會(huì)被掛起,所以如果該方法過于耗時(shí),而客戶端又是UI線程,會(huì)導(dǎo)致ANR,所以當(dāng)確認(rèn)該遠(yuǎn)程方法是耗時(shí)操作時(shí),應(yīng)避免客戶端在UI線程中調(diào)用該方法。同理,當(dāng)服務(wù)器調(diào)用客戶端的listener方法時(shí),該方法也運(yùn)行在客戶端的binder線程池中,所以如果該方法也是耗時(shí)操作,請(qǐng)確認(rèn)運(yùn)行在服務(wù)端的非UI線程中。另外,因?yàn)榭蛻舳说幕卣{(diào)listener運(yùn)行在binder線程池中,所以更新UI需要用到handler。
我們將在進(jìn)程常駐中進(jìn)行簡單的示例分析,實(shí)現(xiàn)多進(jìn)程的相互喚醒操作。
- 請(qǐng)不要使用注解框架
程序注解框架極大的方便了程序開發(fā)者,不需要開發(fā)者大量的寫findViewById(), setOnclickListener()等方法,但是程序注解框架是將類中的所有相關(guān)方法都緩存在內(nèi)容中不會(huì)釋放,這些內(nèi)存就會(huì)越來越大,從而得不到釋放。而且一般程序注解方法都是用到了Java的反射機(jī)制。這個(gè)是不建議使用的(雖然有時(shí)候反射不得不使用)。