您好,登錄后才能下訂單哦!
前言
內存管理的目的就是讓我們在開發過程中有效避免我們的應用程序出現內存泄露的問題。內存泄露相信大家都不陌生,我們可以這樣理解:「沒有用的對象無法回收的現象就是內存泄露」。
如果程序發生了內存泄露,則會帶來以下這些問題
下面我們從基礎說起
基礎知識
Java 的內存分配簡述
Java四種不同的引用類型
與 Android 中的差異:在 2.3 以后版本中,即使內存夠用,Android 系統會優先將 SoftReference 的對象提前回收掉, 其他和 Java 中是一樣的。
因此谷歌官方建議用LruCache(least recentlly use 最少最近使用算法)。會將內存控制在一定的大小內, 超出最大值時會自動回收, 這個最大值開發者自己定。
什么是內存泄漏?
內存泄漏根本原因
長生命周期的對象持有短生命周期對象**強/軟引用**,導致本應該被回收的短生命周期的對象卻無法被正常回收。
例如在單例模式中,我們常常在獲取單例對象時需要傳一個 Context 。單例對象是一個長生命周期的對象(應用程序結束時才終結),而如果我們傳遞的是某一個 Activity 作為 context,那么這個 Activity 就會因為引用被持有而無法銷毀,從而導致內存泄漏。
內存泄漏的危害
內存泄漏的典型案例
永遠的單例(Singleton)
由于單例模式的靜態特性,使得它的生命周期和我們的應用一樣長,一不小心讓單例無限制的持有 Activity 的強引用就會導致內存泄漏。
解決方案
把傳入的 Context 改為同應用生命周期一樣長的 Application 中的 Context。
通過重寫 Application,提供 getContext 方法,那樣就不需要在獲取單例時傳入 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引發的內存泄漏
由于 Handler 屬于 TLS(Thread Local Storage)變量,導致它的生命周期和 Activity 不一致。因此通過 Handler 來更新 UI 一般很難保證跟 View 或者 Activity 的生命周期一致,故很容易導致無法正確釋放。
例如:
public class HandlerBadActivity extends AppCompatActivity { private final Handler handler = new Handler(){//非靜態內部類,持有外部類的強引用 @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 發送一個消息 handler.postDelayed(new Runnable() { //內部會將該 Runable 封裝為一個 Message 對象,同時將 Message.target 賦值為 handler @Override public void run() { //do something } }, 1000 * 60 * 5); this.finish(); } }
上面的代碼中發送了了一個延時 5 分鐘執行的 Message,當該 Activity 退出的時候,延時任務(Message)還在主線程的 MessageQueue 中等待,此時的 Message 持有 Handler 的強引用(創建時通過 Message.target 進行指定),并且由于 Handler 是 HandlerBadActivity 的非靜態內部類,所以 Handler 會持有一個指向 HandlerBadActivity 的強引用,所以雖然此時 HandlerBadActivity 調用了 finish 也無法進行內存回收,造成內存泄漏。
解決方法
將 Handler 聲明為靜態內部類,但是要注意**如果用到 Context 等外部類的 非static 對象,還是應該使用 ApplicationContext 或者通過弱引用來持有這些外部對象**。
public class HandlerGoodActivity extends AppCompatActivity { private static final class MyHandler extends Handler{//聲明為靜態內部類(避免持有外部類的強引用) private final WeakReference<HandlerGoodActivity> mActivity; public MyHandler(HandlerGoodActivity activity){ this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用 } @Override public void handleMessage(Message msg) { HandlerGoodActivity activity = mActivity.get(); if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判斷 activity 是否為空,以及是否正在被銷毀、或者已經被銷毀 removeCallbacksAndMessages(null); return; } // do something } } private final MyHandler myHandler = new MyHandler(this); }
慎用 static 成員變量
static 修飾的變量位于內存的方法區,其生命周期與 App 的生命周期一致。 這必然會導致一系列問題,如果你的 app 進程設計上是長駐內存的,那即使 app 切到后臺,這部分內存也不會被釋放。
解決方法
不要在類初始化時初始化靜態成員,也就是可以考慮懶加載。架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這么設計,那么此對象的生命周期你有責任管理起來。
當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對于有些地方則必須使用 Activity 的 Context,對于Application,Service,Activity三者的Context的應用場景如下:
功能 | 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 |
使用系統服務引發的內存泄漏
為了方便我們使用一些常見的系統服務,Activity 做了一些封裝。比如說,可以通過 getPackageManager在 Activtiy 中獲取 PackageManagerService,但是,里面實際上調用了 Activity 對應的 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));//創建 ApplicationPackageManager } return null; }
ApplicationPackageManager#ApplicationPackageManager
ApplicationPackageManager(ContextImpl context, IPackageManager pm) { mContext = context;//保存 ContextImpl 的強引用 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 靜態引用 public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS } }
遇到的內存泄漏問題是因為在 Activity 中調用了 getPackageManger 方法獲取 PMS ,該方法調用的是 ContextImpl,此時如果ContextImpl 中 PackageManager 為 null,就會創建一個 PackageManger(ContextImpl 會將自己傳遞進去,而 ContextImpl 的 mOuterContext 為 Activity),創建 PackageManager 實際上會創建 PackageManagerService(簡稱 PMS),而 PMS 的構造方法中會創建一個 UserManger(UserManger 初始化之后會持有 ContextImpl 的強引用)。
只要 PMS 的 class 未被銷毀,那么就會一直引用著 UserManger ,進而導致其關聯到的資源無法正常釋放。
解決辦法
將getPackageManager()改為 getApplication()#getPackageManager() 。這樣引用的就是 Application Context,而非 Activity 了。
遠離非靜態內部類和匿名類
因為使用非靜態內部類和匿名類都會默認持有外部類的引用,如果生命周期不一致,就會導致內存泄漏。
public class NestedClassLeakActivity extends AppCompatActivity { class InnerClass {//非靜態內部類 } private static InnerClass sInner;//指向非靜態內部類的靜態引用 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_nested_class); if (sInner == null) { sInner = new InnerClass();//創建非靜態內部類的實例 } } }
非靜態內部類默認會持有外部類的引用,而外部類中又有一個該非靜態內部類的靜態實例,該靜態實例的生命周期和應用的一樣長,而靜態實例又持有 Activity 的引用,因此導致 Activity 的內存資源不能正常回收。
解決方法
將該內部類設為靜態內部類 也可以將該內部類抽取出來封裝成一個單例
集合引發的內存泄漏
我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當我們不再需要該對象時(通常會調用 remove 方法),并沒有把它的引用從集合中清理掉(其中的一種情況就是 remove 方法沒有將不再需要的引用賦值為 null),下面以 ArrayList 的 remove 方法為例
public E remove( int index) { // 數組越界檢查 RangeCheck(index); modCount++; // 取出要刪除位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 計算數組要復制的數量 int numMoved = size - index - 1; // 數組復制,就是將index之后的元素往前移動一個位置 if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); // 將數組最后一個元素置空(因為刪除了一個元素,然后index后面的元素都向前移動了,所以最后一個就沒用了),好讓gc盡快回收 elementData[--size ] = null; // Let gc do its work return oldValue; }
WebView 引發的內存泄漏
WebView 解析網頁時會申請Native堆內存用于保存頁面元素,當頁面較復雜時會有很大的內存占用。如果頁面包含圖片,內存占用會更嚴重。并且打開新頁面時,為了能快速回退,之前頁面占用的內存也不會釋放。有時瀏覽十幾個網頁,都會占用幾百兆的內存。這樣加載網頁較多時,會導致系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啟。
由于占用的都是Native 堆內存,所以實際占用的內存大小不會顯示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虛擬機分配的內存,即使Native堆內存已經占用了幾百兆,這里顯示的還只是幾兆或十幾兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆內存信息。
據說由于 WebView 的一個 BUG,即使它所在的 Activity(或者Service) 結束也就是 onDestroy() 之后,或者直接調用 WebView.destroy()之后,它所占用這些內存也不會被釋放。
解決方法
把使用了 WebView 的 Activity (或者 Service) 放在單獨的進程里。
使用 WebView 的頁面(Activity),在生命周期結束頁面退出(onDestory)的時候,主動調用WebView.onPause()==以及==WebView.destory()以便讓系統釋放 WebView 相關資源。
其他常見的引起內存泄漏原因
Android 3.0 以下,Bitmap 在不使用的時候沒有使用 recycle() 釋放內存。
非靜態內部類的靜態實例容易造成內存泄漏:即一個類中如果你不能夠控制它其中內部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。
警惕線程未終止造成的內存泄露;譬如在 Activity 中關聯了一個生命周期超過 Activity 的 Thread,在退出 Activity 時切記結束線程。
一個典型的例子就是 HandlerThread 的 run 方法。該方法在這里是一個死循環,它不會自己結束,線程的生命周期超過了 Activity 生命周期,我們必須手動在 Activity 的銷毀方法中中調用 thread.getLooper().quit() 才不會泄露。
對象的注冊與反注冊沒有成對出現造成的內存泄露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數據庫的監聽)等。
創建與關閉沒有成對出現造成的泄露;譬如Cursor資源必須手動關閉,WebView必須手動銷毀,流等對象必須手動關閉等。
避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A 持有 B,B 持有 C,C 持有 A,這樣的設計誰都得不到釋放。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。