前言
成都創(chuàng)新互聯(lián)公司服務(wù)項(xiàng)目包括臺(tái)安網(wǎng)站建設(shè)、臺(tái)安網(wǎng)站制作、臺(tái)安網(wǎng)頁制作以及臺(tái)安網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,臺(tái)安網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到臺(tái)安省份的部分城市,未來相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)過程中有效避免我們的應(yīng)用程序出現(xiàn)內(nèi)存泄露的問題。內(nèi)存泄露相信大家都不陌生,我們可以這樣理解:「沒有用的對(duì)象無法回收的現(xiàn)象就是內(nèi)存泄露」。
如果程序發(fā)生了內(nèi)存泄露,則會(huì)帶來以下這些問題
下面我們從基礎(chǔ)說起
基礎(chǔ)知識(shí)
Java 的內(nèi)存分配簡述
Java四種不同的引用類型
與 Android 中的差異:在 2.3 以后版本中,即使內(nèi)存夠用,Android 系統(tǒng)會(huì)優(yōu)先將 SoftReference 的對(duì)象提前回收掉, 其他和 Java 中是一樣的。
因此谷歌官方建議用LruCache(least recentlly use 最少最近使用算法)。會(huì)將內(nèi)存控制在一定的大小內(nèi), 超出最大值時(shí)會(huì)自動(dòng)回收, 這個(gè)最大值開發(fā)者自己定。
什么是內(nèi)存泄漏?
內(nèi)存泄漏根本原因
長生命周期的對(duì)象持有短生命周期對(duì)象**強(qiáng)/軟引用**,導(dǎo)致本應(yīng)該被回收的短生命周期的對(duì)象卻無法被正?;厥?。
例如在單例模式中,我們常常在獲取單例對(duì)象時(shí)需要傳一個(gè) Context 。單例對(duì)象是一個(gè)長生命周期的對(duì)象(應(yīng)用程序結(jié)束時(shí)才終結(jié)),而如果我們傳遞的是某一個(gè) Activity 作為 context,那么這個(gè) Activity 就會(huì)因?yàn)橐帽怀钟卸鵁o法銷毀,從而導(dǎo)致內(nèi)存泄漏。
內(nèi)存泄漏的危害
內(nèi)存泄漏的典型案例
永遠(yuǎn)的單例(Singleton)
由于單例模式的靜態(tài)特性,使得它的生命周期和我們的應(yīng)用一樣長,一不小心讓單例無限制的持有 Activity 的強(qiáng)引用就會(huì)導(dǎo)致內(nèi)存泄漏。
解決方案
把傳入的 Context 改為同應(yīng)用生命周期一樣長的 Application 中的 Context。
通過重寫 Application,提供 getContext 方法,那樣就不需要在獲取單例時(shí)傳入 context。
public class BaseApplication extends Application{ private static ApplicationContext sContext; @Override public void onCreate(){ super.onCreate(); sContext = getApplicationContext(); } public static Context getApplicationContext(){ return sContext; } }
Handler引發(fā)的內(nèi)存泄漏
由于 Handler 屬于 TLS(Thread Local Storage)變量,導(dǎo)致它的生命周期和 Activity 不一致。因此通過 Handler 來更新 UI 一般很難保證跟 View 或者 Activity 的生命周期一致,故很容易導(dǎo)致無法正確釋放。
例如:
public class HandlerBadActivity extends AppCompatActivity { private final Handler handler = new Handler(){//非靜態(tài)內(nèi)部類,持有外部類的強(qiáng)引用 @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_handler_bad); // 延遲 5min 發(fā)送一個(gè)消息 handler.postDelayed(new Runnable() { //內(nèi)部會(huì)將該 Runable 封裝為一個(gè) Message 對(duì)象,同時(shí)將 Message.target 賦值為 handler @Override public void run() { //do something } }, 1000 * 60 * 5); this.finish(); } }
上面的代碼中發(fā)送了了一個(gè)延時(shí) 5 分鐘執(zhí)行的 Message,當(dāng)該 Activity 退出的時(shí)候,延時(shí)任務(wù)(Message)還在主線程的 MessageQueue 中等待,此時(shí)的 Message 持有 Handler 的強(qiáng)引用(創(chuàng)建時(shí)通過 Message.target 進(jìn)行指定),并且由于 Handler 是 HandlerBadActivity 的非靜態(tài)內(nèi)部類,所以 Handler 會(huì)持有一個(gè)指向 HandlerBadActivity 的強(qiáng)引用,所以雖然此時(shí) HandlerBadActivity 調(diào)用了 finish 也無法進(jìn)行內(nèi)存回收,造成內(nèi)存泄漏。
解決方法
將 Handler 聲明為靜態(tài)內(nèi)部類,但是要注意**如果用到 Context 等外部類的 非static 對(duì)象,還是應(yīng)該使用 ApplicationContext 或者通過弱引用來持有這些外部對(duì)象**。
public class HandlerGoodActivity extends AppCompatActivity { private static final class MyHandler extends Handler{//聲明為靜態(tài)內(nèi)部類(避免持有外部類的強(qiáng)引用) private final WeakReferencemActivity; public MyHandler(HandlerGoodActivity activity){ this.mActivity = new WeakReference (activity);//使用弱引用 } @Override public void handleMessage(Message msg) { HandlerGoodActivity activity = mActivity.get(); if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判斷 activity 是否為空,以及是否正在被銷毀、或者已經(jīng)被銷毀 removeCallbacksAndMessages(null); return; } // do something } } private final MyHandler myHandler = new MyHandler(this); }
慎用 static 成員變量
static 修飾的變量位于內(nèi)存的方法區(qū),其生命周期與 App 的生命周期一致。 這必然會(huì)導(dǎo)致一系列問題,如果你的 app 進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的,那即使 app 切到后臺(tái),這部分內(nèi)存也不會(huì)被釋放。
解決方法
不要在類初始化時(shí)初始化靜態(tài)成員,也就是可以考慮懶加載。架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計(jì),那么此對(duì)象的生命周期你有責(zé)任管理起來。
當(dāng)然,Application 的 context 不是萬能的,所以也不能隨便亂用,對(duì)于有些地方則必須使用 Activity 的 Context,對(duì)于Application,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:
功能 | Application | Service | Activity |
---|---|---|---|
Start an Activity | NO1 | NO1 | YES |
Show a Dialog | NO | NO | YES |
Layout Inflation | YES | YES | YES |
Start an Service | YES | YES | YES |
Bind an Service | YES | YES | YES |
Send a Broadcast | YES | YES | YES |
Register BroadcastReceiver | YES | YES | YES |
Load Resource Values | YES | YES | YES |
使用系統(tǒng)服務(wù)引發(fā)的內(nèi)存泄漏
為了方便我們使用一些常見的系統(tǒng)服務(wù),Activity 做了一些封裝。比如說,可以通過 getPackageManager在 Activtiy 中獲取 PackageManagerService,但是,里面實(shí)際上調(diào)用了 Activity 對(duì)應(yīng)的 ContextImpl 中的 getPackageManager 方法
ContextWrapper#getPackageManager
@Override public PackageManager getPackageManager() { return mBase.getPackageManager(); }
ContextImpl#getPackageManager
@Override public PackageManager getPackageManager() { if (mPackageManager != null) { return mPackageManager; } IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm));//創(chuàng)建 ApplicationPackageManager } return null; }
ApplicationPackageManager#ApplicationPackageManager
ApplicationPackageManager(ContextImpl context, IPackageManager pm) { mContext = context;//保存 ContextImpl 的強(qiáng)引用 mPM = pm; } private UserManagerService(Context context, PackageManagerService pm, Object packagesLock, File dataDir) { mContext = context;//持有外部 Context 引用 mPm = pm; //代碼省略 }
PackageManagerService#PackageManagerService
public class PackageManagerService extends IPackageManager.Stub { static UserManagerService sUserManager;//持有 UMS 靜態(tài)引用 public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS } }
遇到的內(nèi)存泄漏問題是因?yàn)樵?Activity 中調(diào)用了 getPackageManger 方法獲取 PMS ,該方法調(diào)用的是 ContextImpl,此時(shí)如果ContextImpl 中 PackageManager 為 null,就會(huì)創(chuàng)建一個(gè) PackageManger(ContextImpl 會(huì)將自己傳遞進(jìn)去,而 ContextImpl 的 mOuterContext 為 Activity),創(chuàng)建 PackageManager 實(shí)際上會(huì)創(chuàng)建 PackageManagerService(簡稱 PMS),而 PMS 的構(gòu)造方法中會(huì)創(chuàng)建一個(gè) UserManger(UserManger 初始化之后會(huì)持有 ContextImpl 的強(qiáng)引用)。
只要 PMS 的 class 未被銷毀,那么就會(huì)一直引用著 UserManger ,進(jìn)而導(dǎo)致其關(guān)聯(lián)到的資源無法正常釋放。
解決辦法
將getPackageManager()改為 getApplication()#getPackageManager() 。這樣引用的就是 Application Context,而非 Activity 了。
遠(yuǎn)離非靜態(tài)內(nèi)部類和匿名類
因?yàn)槭褂梅庆o態(tài)內(nèi)部類和匿名類都會(huì)默認(rèn)持有外部類的引用,如果生命周期不一致,就會(huì)導(dǎo)致內(nèi)存泄漏。
public class NestedClassLeakActivity extends AppCompatActivity { class InnerClass {//非靜態(tài)內(nèi)部類 } private static InnerClass sInner;//指向非靜態(tài)內(nèi)部類的靜態(tài)引用 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_nested_class); if (sInner == null) { sInner = new InnerClass();//創(chuàng)建非靜態(tài)內(nèi)部類的實(shí)例 } } }
非靜態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而外部類中又有一個(gè)該非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例,該靜態(tài)實(shí)例的生命周期和應(yīng)用的一樣長,而靜態(tài)實(shí)例又持有 Activity 的引用,因此導(dǎo)致 Activity 的內(nèi)存資源不能正常回收。
解決方法
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類 也可以將該內(nèi)部類抽取出來封裝成一個(gè)單例
集合引發(fā)的內(nèi)存泄漏
我們通常會(huì)把一些對(duì)象的引用加入到集合容器(比如ArrayList)中,當(dāng)我們不再需要該對(duì)象時(shí)(通常會(huì)調(diào)用 remove 方法),并沒有把它的引用從集合中清理掉(其中的一種情況就是 remove 方法沒有將不再需要的引用賦值為 null),下面以 ArrayList 的 remove 方法為例
public E remove( int index) { // 數(shù)組越界檢查 RangeCheck(index); modCount++; // 取出要?jiǎng)h除位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 計(jì)算數(shù)組要復(fù)制的數(shù)量 int numMoved = size - index - 1; // 數(shù)組復(fù)制,就是將index之后的元素往前移動(dòng)一個(gè)位置 if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); // 將數(shù)組最后一個(gè)元素置空(因?yàn)閯h除了一個(gè)元素,然后index后面的元素都向前移動(dòng)了,所以最后一個(gè)就沒用了),好讓gc盡快回收 elementData[--size ] = null; // Let gc do its work return oldValue; }
WebView 引發(fā)的內(nèi)存泄漏
WebView 解析網(wǎng)頁時(shí)會(huì)申請(qǐng)Native堆內(nèi)存用于保存頁面元素,當(dāng)頁面較復(fù)雜時(shí)會(huì)有很大的內(nèi)存占用。如果頁面包含圖片,內(nèi)存占用會(huì)更嚴(yán)重。并且打開新頁面時(shí),為了能快速回退,之前頁面占用的內(nèi)存也不會(huì)釋放。有時(shí)瀏覽十幾個(gè)網(wǎng)頁,都會(huì)占用幾百兆的內(nèi)存。這樣加載網(wǎng)頁較多時(shí),會(huì)導(dǎo)致系統(tǒng)不堪重負(fù),最終強(qiáng)制關(guān)閉應(yīng)用,也就是出現(xiàn)應(yīng)用閃退或重啟。
由于占用的都是Native 堆內(nèi)存,所以實(shí)際占用的內(nèi)存大小不會(huì)顯示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虛擬機(jī)分配的內(nèi)存,即使Native堆內(nèi)存已經(jīng)占用了幾百兆,這里顯示的還只是幾兆或十幾兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆內(nèi)存信息。
據(jù)說由于 WebView 的一個(gè) BUG,即使它所在的 Activity(或者Service) 結(jié)束也就是 onDestroy() 之后,或者直接調(diào)用 WebView.destroy()之后,它所占用這些內(nèi)存也不會(huì)被釋放。
解決方法
把使用了 WebView 的 Activity (或者 Service) 放在單獨(dú)的進(jìn)程里。
使用 WebView 的頁面(Activity),在生命周期結(jié)束頁面退出(onDestory)的時(shí)候,主動(dòng)調(diào)用WebView.onPause()==以及==WebView.destory()以便讓系統(tǒng)釋放 WebView 相關(guān)資源。
其他常見的引起內(nèi)存泄漏原因
Android 3.0 以下,Bitmap 在不使用的時(shí)候沒有使用 recycle() 釋放內(nèi)存。
非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例容易造成內(nèi)存泄漏:即一個(gè)類中如果你不能夠控制它其中內(nèi)部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態(tài)類和弱引用來處理(譬如ViewRoot的實(shí)現(xiàn))。
警惕線程未終止造成的內(nèi)存泄露;譬如在 Activity 中關(guān)聯(lián)了一個(gè)生命周期超過 Activity 的 Thread,在退出 Activity 時(shí)切記結(jié)束線程。
一個(gè)典型的例子就是 HandlerThread 的 run 方法。該方法在這里是一個(gè)死循環(huán),它不會(huì)自己結(jié)束,線程的生命周期超過了 Activity 生命周期,我們必須手動(dòng)在 Activity 的銷毀方法中中調(diào)用 thread.getLooper().quit() 才不會(huì)泄露。
對(duì)象的注冊(cè)與反注冊(cè)沒有成對(duì)出現(xiàn)造成的內(nèi)存泄露;譬如注冊(cè)廣播接收器、注冊(cè)觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等。
創(chuàng)建與關(guān)閉沒有成對(duì)出現(xiàn)造成的泄露;譬如Cursor資源必須手動(dòng)關(guān)閉,WebView必須手動(dòng)銷毀,流等對(duì)象必須手動(dòng)關(guān)閉等。
避免代碼設(shè)計(jì)模式的錯(cuò)誤造成內(nèi)存泄露;譬如循環(huán)引用,A 持有 B,B 持有 C,C 持有 A,這樣的設(shè)計(jì)誰都得不到釋放。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)創(chuàng)新互聯(lián)的支持。