內(nèi)存泄露和內(nèi)存優(yōu)化
對于Android來說,每一個APP的內(nèi)存是有限的碉克。你過你的內(nèi)存出現(xiàn)問題:泄露凌唬,長期占用過高,就會導致app易于被殺掉漏麦。頻繁的gc導致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 中就會造成內(nèi)存泄露
- 非靜態(tài)內(nèi)部類持有外部類的引用
在Java中酥夭,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)都會持有外部類(一般是指Activity等頁面)的引用,當兩者的生命周期出現(xiàn)不一致的時候,很容易導致內(nèi)存泄露熬北。
如下所示疙描,非常常見的幾種情況:
Hanlder
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg)
{
super.handleMessage(msg);
}
};
這里的Handler會引用Activity的引用,當handler調(diào)用postDelay的時候讶隐,若Activity已經(jīng)finish掉了起胰,因為這個 handler 會在一段時間內(nèi)繼續(xù)被 main Looper 持有,導致引用仍然存在巫延,在這段時間內(nèi)效五,如果內(nèi)存吃緊至超出,是很危險的炉峰。
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ù)是一個很費時的操作畏妖,當我們開啟該線程后,將設(shè)備的橫屏變?yōu)榱素Q屏疼阔,一般情況下當屏幕轉(zhuǎn)換時會重新創(chuàng)建Activity戒劫,按照我們的想法,老的Activity應該會被銷毀才對婆廊,然而事實上并非如此迅细。由于我們的線程是Activity的內(nèi)部類,所以MyThread中保存了Activity的一個引用淘邻,當MyThread的run函數(shù)沒有結(jié)束時茵典,MyThread是不會被銷毀的,因此它所引用的老的Activity也不會被銷毀列荔,因此就出現(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)部類贴浙,也就是說當前的Activity會被ref2所應用,如果將這個引用傳入到了一個異步線程署恍,該線程的生命周期與Activity的生命周期不一致的時候崎溃,就會導致內(nèi)存泄露。
- Static變量造成內(nèi)存泄露
1. 界面類的靜態(tài)化: 靜態(tài)Activity
2. 界面中View的靜態(tài)化: 靜態(tài)View
界面中View的靜態(tài)化一定會導致頁面內(nèi)存泄露盯质。界面中的View都是持有界面引用的袁串,靜態(tài)變量的生命周期與整個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 {
...
}
...
}
其中有一個background.setCallback(this);呼巷,所以這就導致這個靜態(tài)變量指向的對象又持有了TextView這個對象的引用囱修,TextView持有的確實整個Activity的引用。這樣就導致了內(nèi)存泄露王悍。
我們再來看一個例子:
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)部類對象sInnerClass持有了外部Acitivity的引用破镰,當屏幕發(fā)生變化時,不會被釋放。
-
<font size = 5>資源沒有關(guān)閉</font>
1. Cursor游標沒有關(guān)閉
數(shù)據(jù)庫中才操作經(jīng)常碰到cursor鲜漩。
2. InputStream源譬、OutputStream等沒有關(guān)閉
文件讀寫、Socket讀寫等經(jīng)常碰到
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負責執(zhí)行某些后臺任務踩娘,或為硬件提供接口,如果context對象想要在服務內(nèi)部的事件發(fā)生時被通知喉祭,需要注冊監(jiān)聽器养渴。然而這讓服務持有了activity的引用,如果activity銷毀時沒有取消注冊泛烙,那么你的activity就泄露了厚脉。
View添加到?jīng)]有刪除機制的容器中
屬性動畫導致的內(nèi)存泄露
如果你設(shè)置你的動畫為無限循環(huán),而且沒有在onDestroy中停止該動畫胶惰,那么動畫會一直播放下去傻工,Activity的View會被動畫吃持有,而View持有了Activiy孵滞。從而導致內(nèi)存泄露中捆。<font size = 5>過期引用</font>
當一個數(shù)組擴容后又被縮減,比如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并不會去回收他們蟋滴,我們需要手動的釋放他們。
內(nèi)存溢出和內(nèi)存的查看方法
- 使用第三方開源庫
LeakCanary
在這里就不做具體的介紹了痘绎。網(wǎng)上的使用demo:
leakCanary Demo
- adb shell命令
通過以下命令可以查看你APP的內(nèi)存使用情況已經(jīng)Activity和View等的個數(shù)情況津函,具體的
adb shell dumpsys meminfo packagename
其中,packagename就是你程序的報名孤页,具體的示例尔苦,如下圖所示:
如上圖所示:
上面部分顯示的是你的app所占用的內(nèi)存總數(shù)(主要是看TOTAL,內(nèi)存所實際占用的值)
下面的部分可以看到你的一些對象的個數(shù):如Views行施、Activities等允坚。
當你進入一個acitivity的時候,activity的個數(shù)會增加蛾号,退出后會減少稠项,如果只增加、不減少鲜结,就說明出現(xiàn)了內(nèi)存泄露的問題展运。 (經(jīng)過實際的測試斩芭,這個對有些手機,好不管用乐疆,就算我寫個demo:只有一個Activity划乖,什么也沒有做,進來挤土、退出琴庵、進來、activities個數(shù)會變大仰美,不會立即變小迷殿,需要等一段時間才會變小)
- DDMS
DDMS是Android開發(fā)環(huán)境中的Dalvik虛擬機(andoid4.4之前,4.4及其之后引入了ART虛擬機)調(diào)試監(jiān)控服務咖杂。
1. update heap
對一個activity進入退出反復多次看data object是否穩(wěn)定在一個范圍
2. MAT(Memory Analyzer Tool)
dump hprof file : 點擊后等待一會庆寺,會生成一個hprof文件。插件版本的MAT可以直接打開該文件诉字,否則需要進行一步轉(zhuǎn)換操作懦尝。 提供了這個工具 hprof-conv (位于 sdk/tools下), 轉(zhuǎn)換命令如下所示:
./hprof-conv xxx-a.hprof xxx-b.hprof
最后通過DDMS-File-open,打開的hprof文件即可進行分析內(nèi)存泄露相關(guān)陵霉。
內(nèi)存優(yōu)化建議
- 了解你機器的內(nèi)存情況
通過以下代碼可以查看每個進程可用的最大內(nèi)存踊挠,即heapgrowthlimit值
ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M為單位
通過以下代碼可以獲取 應用程序的最大可用內(nèi)存
long maxMemory = Runtime.getRuntime().maxMemeory(); //以字節(jié)為單位
兩者的區(qū)別:
單位不一致 前者以M為單位效床,后者以字節(jié)為單位剩檀。
具體的以lenovo的一款手機(S850T谨朝, Android版本為4.4.2)為例: 經(jīng)過測試兩者得到的值一致均是128M。
使用場景
當你進行圖片加載的時候,都會使用到LRUCache图谷,初始化的時候設(shè)置緩存的大小便贵。一般來說都設(shè)置為當前最大內(nèi)存的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();
};
};
- 當界面不可見利耍、內(nèi)存緊張的時候釋放內(nèi)存
android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前為onLowMemory) 盔粹,系統(tǒng)會根據(jù)不同的內(nèi)存狀態(tài)來毀掉舷嗡,參數(shù) level 代表了你app的不同狀態(tài),Application捻脖、Activity郎仆、Fragment、Service曙旭、ContentProvider均可以響應桂躏。具體如下:
TRIM_MEMORY_UI_HIDDEN: 應用程序被隱藏了,如按了Home或者Back導致UI不可見较沪,這個時候尸曼,我們應該釋放一些內(nèi)存控轿。
以下三個是我們的應用程序真正運行時的回調(diào):
TRIM_MEMORY_RUNNING_MODERATE: 程序正常運行,并不會被殺掉冒签,但是手機的內(nèi)存有點低了钟病,系統(tǒng)可能開始根據(jù)LRU規(guī)則來殺死進程了。
TRIM_MEMORY_RUNNING_LOW: 程序正常運行档悠,并不會被殺掉,但是手機內(nèi)存非常的低了辖所,應該釋放一些資源了惰说,否則影響性能缘回。
TRIM_MEMORY_RUNNING_CRITICAL: 程序正在運行吆视,但是系統(tǒng)已經(jīng)根據(jù)LRU殺死了大部分緩存的進程了,此時我們需要釋放內(nèi)存啦吧,否則系統(tǒng)可能會干掉你拙寡。
以下三個是當應用程序是緩存時候的回調(diào):
TRIM_MEMORY_BACKGROUND: 內(nèi)存不足肆糕,并且該進程是后臺進程诚啃。
TRIM_MEMORY_MODERATE: 內(nèi)存不足,并且該進程在后臺進程列表的中部。
TRIM_MEMORY_COMPLETE:內(nèi)存不足造垛,并且該進程在后臺進程列表的最后一個魔招,馬上就要被清理了,這個時候應該把一切盡可能釋放的都釋放掉筋搏。
通常在我們開始進行架構(gòu)設(shè)計的時候仆百,就要考慮到哪些東西是要常駐的,哪些東西是緩存后要被清理奔脐, 一般情況下俄周,以下資源都要被清理:
緩存:包括文件緩存、圖片的緩存髓迎、比如第三方圖片緩存庫峦朗。
一些動態(tài)生成的View: 比如一般應用的圖片輪播View,在你的應用隱藏后排龄,根本不需要輪播波势。
案例分析:
1. LRUCache緩存的清理方式:trimToSize()接口可以重新設(shè)置緩存的大小。evictAll()接口可以清楚所有的LRUCache緩存內(nèi)容橄维。
2. 暴力清理界面中的View
-
圖片資源的壓縮
1. res中資源到壓縮: 使用有損壓縮工具尺铣,比如:tinyPng,壓縮后的圖片肉眼根本看不出來争舞,壓縮率可以達到50%以上凛忿。
2. BitmapFactory的壓縮。
通過BitmapFactory的Options設(shè)置竞川,降低采樣率店溢,壓縮圖片到適合的大小,同時注意使用若引用和緩存機制委乌。
Bitmap.Config設(shè)置圖片的格式為RGB565床牧,這個設(shè)置肉眼是看不出色彩的丟失,而且比RGB8888占存小的多遭贸。
使用BitmapFactory.Options.inBitmap字段戈咳。如果這個選項被設(shè)置,那么使用該Options 的decode方法將會嘗試復用一個已經(jīng)存在的bitmap來加載新的bitmap壕吹。這意味著bitmap的內(nèi)存將被復用除秀,避免分配和釋放內(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ù)千之內(nèi)建議使用ArrayMap。
- 避免創(chuàng)建不必要的對象
在短時間內(nèi)創(chuàng)建了大量的對象挤渔,然后有釋放肮街,這樣就引起了內(nèi)存抖動。頻繁的引起GC操作判导,會導致內(nèi)存的卡頓嫉父。
1. 字符串的拼接:StringBuffer(非線程安全)和StringBuilder(線程安全)的使用
2. 自定義View中不要在onDraw中定義畫筆等對象
3. 在循環(huán)函數(shù)內(nèi)避免創(chuàng)建重復的對象沛硅,將多個函數(shù)都經(jīng)常用到的不可變對象拿出來統(tǒng)一進行初始化,在一開始寫的時候就要特別的注意绕辖,否則后邊修改起來很是麻煩(主要是再找到他很麻煩)
4. 在循環(huán)的內(nèi)部不要使用try catch操作摇肌,將其拿到外面來。
5. 不要在循環(huán)中進行文件的操作:比如判斷文件是否存在仪际,這相對是一個很耗時的操作
案例說明
SimpleDateFromat是用來時間轉(zhuǎn)換的围小,一般的,開發(fā)者都會定義個專門用于時間轉(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ù)树碱。就不停的重復創(chuàng)建SimpleDateFromat對象肯适。你應該將對象創(chuàng)建拿出來,放在類中成榜,或者是重新定義一個時間轉(zhuǎn)換函數(shù)框舔,入慘為已經(jīng)創(chuàng)建好的SimpleDateFormat對象。
還需要注意的是:假如你的循環(huán)量很大伦连,不建議在for循環(huán)中進行時間轉(zhuǎn)換雨饺,而是在你用到的時候才進行轉(zhuǎn)換,比如顯示出來惑淳。
- 不要擴大變量的作用域
classs A
{
private B mB;
public A(B b) {
this.mB = b;
//就在構(gòu)造函數(shù)中進行了對mB進行了一些操作
}
//后續(xù)再也沒有用到過mB
}
class B
{
public B() {
}
public static void main(String[] args)
{
}
}
如上所示的簡單代碼:類A的構(gòu)造函數(shù)中额港,傳入了類B的對象,并且類A中定義了成員變量mB歧焦,但是mB就在構(gòu)造函數(shù)中用了一下移斩,后續(xù)再也沒有用,在類A中mB的生命周期和A一致绢馍。本來mB的作用域就在構(gòu)造函數(shù)向瓷,結(jié)果擴大為整個類。
- 不要讓生命周期比Activity長的對象持有Activity的引用
這樣的錯誤很多舰涌,比如:將Activity的Context傳給單例模式猖任,毫不知情的將Activity的Context傳給非靜態(tài)內(nèi)部類或者是匿名內(nèi)部類。
- 盡量的使用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的。
-
移除回調(diào)
1. handler的removeCallbacksAndMessages(null)
2. setXXXCallback(null)鸡典、 setXXXListener(null)源请,需要注意的是,要進行callback調(diào)用的地方就需要進行判斷了
- 常量的使用
關(guān)于enum和static。Android強烈建議不要使用enum谁尸,他會使得內(nèi)存消耗變大為原來的2倍以上舅踪。
-
使用代碼混淆剔除不需要的代碼
jar包的混淆:使用proguardgui.bat
jar包的合并:使用插件fatjar
請使用靜態(tài)內(nèi)部類+WeakReference的方式
非靜態(tài)內(nèi)部類和匿名內(nèi)部類會持有頁面的應用,請使用靜態(tài)內(nèi)部類症汹,并將頁面的引用通過WeakReference的方式傳遞過去硫朦。
- 合理的使用多進程
android對單個進程都有一個內(nèi)存允許的最大內(nèi)存限制贷腕。加入你在你的app中又啟動一個進程背镇,這樣你的內(nèi)存限制就變?yōu)榱嗽瓉淼?倍。
啟動多進程的方法很簡單泽裳,只需要在AndroidManifest.xml聲明的四大組件的標簽中增加"android:process"屬性即可瞒斩。
進程分為兩種:私有進程和全局進程。私有進程在名稱簽名添加冒號即可涮总。
但是多進程有一些需要注意的地方:
1. Application的onCreate會被調(diào)用多次胸囱。一般程序會將程序的一些初始化的操作放在這里,這點需要注意瀑梗。
2. 多進程之間的通訊必須使用AIDL接口烹笔,需要注意的一點是:AIDL之間傳遞大量數(shù)據(jù)是有一個限制的。 傳遞內(nèi)容過大會出現(xiàn):TransactionToolLargeException抛丽。官方文檔說明:最大的限制為1M谤职。
3. 多進程導致 靜態(tài)成員、單例模式和SharedPreference 都變的不可靠亿鲜。
4. 多進程之間傳遞數(shù)據(jù)的效率:有些手機在傳遞大量數(shù)據(jù)的時候允蜈,效率很差。
5. 多進程傳遞對象需要實現(xiàn)序列化操作蒿柳。
6. AIDL支持的數(shù)據(jù)類型:基本數(shù)據(jù)類型饶套;String和CharSequence;List僅僅支持ArrayList垒探,里面的每一個對象都必須支持序列化妓蛮,Map只支持HashMap,里面的key和value都必須支持序列化(必須被AIDL支持)圾叼。
7. AIDL服務端可以使用CopyOnWriteArrayList和ConcurrentHashMap來進行自動線程同步蛤克,客戶端拿到的依然是ArrayList和HashMap。
8.AIDL服務端和客戶端之間做監(jiān)聽器褐奥,服務端需要使用RemoteCallbackList咖耘,否則客戶端的監(jiān)聽器無法收到通知(因為服務端實質(zhì)還是一份新的序列化后的監(jiān)聽器實例,并不是客戶端那份)撬码。
9.客戶端調(diào)用遠程服務方法時儿倒,因為遠程方法運行在服務端的binder線程池中,同時客戶端線程會被掛起,所以如果該方法過于耗時夫否,而客戶端又是UI線程彻犁,會導致ANR,所以當確認該遠程方法是耗時操作時凰慈,應避免客戶端在UI線程中調(diào)用該方法汞幢。同理,當服務器調(diào)用客戶端的listener方法時微谓,該方法也運行在客戶端的binder線程池中森篷,所以如果該方法也是耗時操作,請確認運行在服務端的非UI線程中豺型。另外仲智,因為客戶端的回調(diào)listener運行在binder線程池中,所以更新UI需要用到handler姻氨。
我們將在進程常駐中進行簡單的示例分析钓辆,實現(xiàn)多進程的相互喚醒操作。
- 請不要使用注解框架
程序注解框架極大的方便了程序開發(fā)者肴焊,不需要開發(fā)者大量的寫findViewById(), setOnclickListener()等方法前联,但是程序注解框架是將類中的所有相關(guān)方法都緩存在內(nèi)容中不會釋放,這些內(nèi)存就會越來越大娶眷,從而得不到釋放似嗤。而且一般程序注解方法都是用到了Java的反射機制。這個是不建議使用的(雖然有時候反射不得不使用)茂浮。