Android性能優化之內存優化——內存泄漏篇

一、 簡介

內存泄露(Memory Leak)簡單來說,就是該釋放或回收的資源沒有主動去釋放或回收,導致GC也無法回收,最後永遠無法正常回收,造成系統資源浪費。內存泄露會積累放大影響,嚴重時會導致內存溢出,引起程序卡死崩潰等。

和內存泄露有關的常見場景如下:

  • 非靜態內部類
  • 匿名內部類
  • BroadcastReceiver
  • ContentObserver
  • Cursor
  • Handler的延時任務
  • View的延時任務
  • Timer的延時任務
  • HandlerThread
  • Thread
  • Bitmap
  • 單例模式

下面將結合示例代碼、MAT分析、源碼,對以上常見情況逐一講解。


二、 非靜態內部類

定義一個最簡單的非靜態內部類,執行javac編譯其class文件:

儘管寫在了一個java文件中,編譯後仍然拆分成兩個class文件,並且觀察內部類的class文件,發現編譯器自動生成了一個有參構造器,參數爲外部類,因此,當在外部類中實例化內部類對象時,類加載器會調用該構造器傳入外部類的this引用,即非靜態內部類持有外部引用。

當內部類的生命週期大於外部類時,外部類就無法進行銷燬,導致內存泄露,下面舉例說明:

在Activity中,通過非靜態內部類的方式定義線程類,實例化其對象並運行,Activity主動調用finish方法進行銷燬,抓取hprof文件:

DemoActivity實例的GC最短路徑:

InnerClass實例的GC最短路徑:

由於mInnerClass對象還在執行線程活動,它又持有外部類DemoActivity實例的引用,因此this$0即外部DemoActivity的實例無法被回收。

綜上,當我們使用非靜態內部類時,一定要在外部類銷燬時主動釋放內部類的資源,避免因內部類生命週期大於外部類導致內存泄露,而對於像線程這種無法主動停止、釋放資源的例子,可採用靜態內部類+弱引用的方式實現。


三、 匿名內部類

定義一個匿名內部類,執行javac編譯其class文件:

觀察匿名內部類的class文件,發現編譯器自動生成了一個有參構造器,參數爲外部類,結合上一節內容,可得知,匿名內部類同樣持有外部類的實例,同樣存在內存泄露的風險,原理相同,此處不再贅述。


四、 BroadcastReceiver

在Activity中,以靜態內部類的方式定義廣播接收器,實例化其對象並註冊廣播,Activity主動調用finish方法進行銷燬,抓取hprof文件無任何內存泄露現象,分析registerReceiver源碼:

  • ActivityregisterReceiver方法具體實現位於ContextImpl類中
  • 註冊時如果沒有傳入Handler對象,默認使用主線程的Handler
  • receiver會被封裝在ReceiverDispatcher對象中
  • mPackageInfo會在ContextImpl對象實例化時賦值,因此通常是通過mPackageInfo去獲得ReceiverDispatcher實例,但是如果首次調用沒有獲取到實例,則仍然會創建該實例

ReceiverDispatcher對象內部包含了當前調用方的主線程(mActivityThread)和用於傳給AMS進行註冊的binder(mIIntentReceiver)。

由於入參registered爲true,因此InnerReceiver在實例化時通過弱引用方式持有receiver對象,這也就解釋了爲什麼我們的DemoActivity實例在銷燬時沒有出現mReceiver對象的內存泄露。

雖然應用層沒有泄露,但是框架層仍然有泄露問題,接下來看AMS中:

    // Maximum number of receivers an app can register.
    private static final int MAX_RECEIVERS_ALLOWED_PER_APP = 1000;

    /**
     * Keeps track of all IIntentReceivers that have been registered for broadcasts.
     * Hash keys are the receiver IBinder, hash value is a ReceiverList.
     */
    final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();

    public Intent registerReceiver(IApplicationThread caller, String callerPackage,
            IIntentReceiver receiver, IntentFilter filter, String permission, int userId,
            int flags) {
        // TODO
        ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
        if (rl == null) {
            rl = new ReceiverList(this, callerApp, callingPid, callingUid,
                    userId, receiver);
            if (rl.app != null) {
                // 如果該進程活躍的receiver數量達到1000,即AMS中該進程的接收器binder對象達到1000個,
                // 則會拋出該異常,這就印證了反註冊廣播接收器的必要性
                final int totalReceiversForApp = rl.app.receivers.size();
                if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
                    throw new IllegalStateException("Too many receivers, total of "
                            + totalReceiversForApp + ", registered for pid: "
                            + rl.pid + ", callerPackage: " + callerPackage);
                }
                rl.app.receivers.add(rl);
            } else {
                // 如果進程已經掛掉,則銷燬這些binder對象
                try {
                    receiver.asBinder().linkToDeath(rl, 0);
                } catch (RemoteException e) {
                    return sticky;
                }
                rl.linkedToDeath = true;
            }
            mRegisteredReceivers.put(receiver.asBinder(), rl);
        } else if (rl.uid != callingUid) {
            throw new IllegalArgumentException(
                    "Receiver requested to register for uid " + callingUid
                    + " was previously registered for uid " + rl.uid
                    + " callerPackage is " + callerPackage);
        } else if (rl.pid != callingPid) {
            throw new IllegalArgumentException(
                    "Receiver requested to register for pid " + callingPid
                    + " was previously registered for pid " + rl.pid
                    + " callerPackage is " + callerPackage);
        } else if (rl.userId != userId) {
            throw new IllegalArgumentException(
                    "Receiver requested to register for user " + userId
                    + " was previously registered for user " + rl.userId
                    + " callerPackage is " + callerPackage);
        }
        BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
                    permission, callingUid, userId, instantApp, visibleToInstantApps);
        if (rl.containsFilter(filter)) {
            Slog.w(TAG, "Receiver with filter " + filter
                    + " already registered for pid " + rl.pid
                    + ", callerPackage is " + callerPackage);
        } else {
            rl.add(bf);
            // TODO
            mReceiverResolver.addFilter(bf);
        }
    }

  • AMS內部維護了一個廣播接收器的註冊表mRegisteredReceivers,key爲傳過來的IIntentReceiver的binder對象,而value是由這個IIntentReceiver構造的註冊數組ReceiverList
  • ReceiverList中保存了進程信息和接收器的filter。
  • 當該廣播接收器首次被註冊時,會在AMS中創建一個對應的註冊數組,然後將註冊數組存入ASM的註冊表中,最後再把接收器中的filter存入這個註冊數組。
  • 同一個廣播接收器只能被一處註冊一次,重複註冊無效,他處註冊會拋出異常
  • 及時反註冊接收器,防止AMS中過多無效binder引發異常

終上所述,當我們在應用層忘記反註冊廣播接收器時,雖然應用層不會因此導致內存泄露,但是框架層仍然發生了泄露,且該泄露會累積影響,嚴重時導致應用異常崩潰。

除此之外,我們也明白了爲什麼自Android O開始,不允許應用註冊靜態廣播,原因之一便是靜態廣播對系統資源的浪費,我們無法主動去反註冊該接收器。

截取自developers-廣播限制

正確的做法是,在適當的時候反註冊廣播,及時釋放相關資源:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReceiver != null) {
            unregisterReceiver(mReceiver);
        }
    }

五、 ContentObserver

在Activity中,通過靜態內部類的方式定義內容觀察者,實例化其對象並註冊監聽,Activity主動調用finish方法進行銷燬,抓取hprof文件:

DemoObserver實例的GC最短路徑:

此處發生內存泄露的mContentObserver對象即mObserver自身,其被ContentObserver內部的Transport對象(本質上是一個binder)反向引用:

分析registerContentObserver源碼:

然後調用到了框架層ContentService中:

    private final ObserverNode mRootNode = new ObserverNode("");

    public void registerContentObserver(Uri uri, boolean notifyForDescendants,
            IContentObserver observer, int userHandle, int targetSdkVersion) {
        // TODO
        mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
                uid, pid, userHandle);
        // TODO
    }
    
    public static final class ObserverNode {
        private class ObserverEntry implements IBinder.DeathRecipient {...}
        private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
        private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
        
        public void addObserverLocked(Uri uri, IContentObserver observer,
                                      boolean notifyForDescendants, Object observersLock,
                                      int uid, int pid, int userHandle) {
            addObserverLocked(uri, 0, observer, notifyForDescendants, observersLock,
                    uid, pid, userHandle);
        }
        
        private void addObserverLocked(Uri uri, int index, IContentObserver observer,
                                       boolean notifyForDescendants, Object observersLock,
                                       int uid, int pid, int userHandle) {
            // If this is the leaf node add the observer
            if (index == countUriSegments(uri)) {
                mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
                        uid, pid, userHandle, uri));
                return;
            }

            // Look to see if the proper child already exists
            String segment = getUriSegment(uri, index);
            if (segment == null) {
                throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer");
            }
            int N = mChildren.size();
            for (int i = 0; i < N; i++) {
                ObserverNode node = mChildren.get(i);
                if (node.mName.equals(segment)) {
                    node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
                            observersLock, uid, pid, userHandle);
                    return;
                }
            }

            // No child found, create one
            ObserverNode node = new ObserverNode(segment);
            mChildren.add(node);
            node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
                    observersLock, uid, pid, userHandle);
        }
    }

簡而言之,將binder封裝在ObserverEntry對象中,加入到mObservers數組,也就是說,框架層持有了應用層註冊時的observer對象的binder引用,也就間接地持有了observer對象的引用,導致該observer對象無法回收,引起內存泄露。因此,我們應該主動去反註冊observer,釋放其資源:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mObserver != null) {
            getContentResolver().unregisterContentObserver(mObserver);
        }
    }

六、 Cursor

在Activity中,通過ContentResolver對象獲取查詢操作的Cursor對象,Activity主動調用finish方法進行銷燬,抓取hprof文件無任何內存泄露現象,首先分析Activity中的getContentResolver方法:

getContentResolver具體實現位於ContextImpl類中,返回的對象爲靜態內部類ApplicationContentResolver的實例:

其實例化位於構造方法中。

接下來分析ContentResolver類中query源碼:

    public final @Nullable Cursor query(...) {
        try {
            // 默認構造方法實例化時mWrapped對象爲null
            if (mWrapped != null) {
                return mWrapped.query(uri, projection, queryArgs, cancellationSignal);
            }
        } catch (RemoteException e) {
            return null;
        }
        IContentProvider unstableProvider = acquireUnstableProvider(uri);
        if (unstableProvider == null) {
            return null;
        }
        IContentProvider stableProvider = null;
        Cursor qCursor = null;
        try {
            // TODO
            // 分兩種不同情況去獲取qCursor對象
            try {
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);
            } catch (DeadObjectException e) {
                unstableProviderDied(unstableProvider);
                stableProvider = acquireProvider(uri);
                if (stableProvider == null) {
                    return null;
                }
                qCursor = stableProvider.query(
                        mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
            }
            if (qCursor == null) {
                return null;
            }
            // TODO
            // Wrap the cursor object into CursorWrapperInner object.
            final IContentProvider provider = (stableProvider != null) ? stableProvider
                    : acquireProvider(uri);
            // 此處採用委派模式,CursorWrapperInner本身也是實現的Cursor接口
            // 將qCursor對象包裝成CursorWrapperInner對象
            final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
            stableProvider = null;
            qCursor = null;
            return wrapper;
            // TODO
        } catch (RemoteException e) {
            // Arbitrary and not worth documenting, as Activity
            // Manager will kill this process shortly anyway.
            return null;
        } finally {
            // 及時回收無用資源
            if (qCursor != null) {
                qCursor.close();
            }
            if (cancellationSignal != null) {
                cancellationSignal.setRemote(null);
            }
            if (unstableProvider != null) {
                releaseUnstableProvider(unstableProvider);
            }
            if (stableProvider != null) {
                releaseProvider(stableProvider);
            }
        }
    }

其中的acquireUnstableProvideracquireProvider方法均爲ApplicationContentResolver中的調用,並且都是調用的ActivityThreadacquireProvider方法:

  • 如果ActivityThread中已有這個provider,則返回
  • 如果沒有,則通過AMS獲取ContentProviderHolder對象
  • 獲取到ContentProviderHolder對象後保存provider並返回

ContentProviderHolder內部持有一個指向AMS中對應provider的IContentProvider實例和用於跟應用層通信的binder對象connection,其創建過程均位於框架層AMS中,此處不進行發散。

因此,當我們通過ContentResolver對ContentProvider進行操作時,框架層會返回binder對象,應用層會將其包裝爲CursorWrapperInner對象,那麼,按理說如果我們沒有主動釋放資源,框架層將會發生內存泄露,這個binder未被及時回收,但是實際卻沒有發生,於是我們繼續分析包裝類CursorWrapperInner

其中重寫了finalize方法,這意味着當GC輪詢到該對象時,會去執行close方法,也就是Cursorclose方法,對資源進行釋放,回收binder。但是這樣一來,問題就非常明顯,儘管Android本身有這種保障機制進行回收,但其時機相對於主動釋放,晚了許多,因此該方法中會拋出警告,提醒開發者及時調用close主動釋放Cursor資源。

所以,當我們使用Cursor時,應該在操作結束後及時關閉:

    Cursor cursor = null;
    try {
        cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder, null);
        // TODO
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
    }

七、 Handler的延時任務

在Activity中,以靜態內部類的方式定義一個Handler,實例化爲mHandler並執行一個延時任務,在任務開始執行之前主動調用finish方法進行銷燬,抓取hprof文件:

StaticHandler實例的GC最短路徑:

主線程消息隊列中有一個Message實例持有一個target強引用,這個targetmHandler,而Message實例也就是sendMessageDelayed傳入的那個對象。

下面將結合Handler源碼分析:

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        // TODO
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        // TODO
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        // 此處Message反向引用了Handler對象
        msg.target = this;
        // TODO
        return queue.enqueueMessage(msg, uptimeMillis);
    }

    /**
     * post方法本質上是發送了一個匿名Message到消息隊列中
     */
    public final boolean post(@NonNull Runnable r) {
       return sendMessageDelayed(getPostMessage(r), 0);
    }

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

Handler內部的調用鏈可知,Message實例會持有Handler實例的強引用,用於消息隊列輪詢到該消息時執行回調,只要該Message實例沒有消費結束被回收,Handler實例便無法回收,導致該對象內存泄露。

正確的做法是在適當的時候主動移除Handler中剩餘的消息,釋放資源,以便回收:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

八、 View的延時任務

自定義一個View,在Attach到窗口的回調中執行一個延時任務。在Activity佈局中使用該自定義View,在延時任務開始執行之前主動調用finish方法進行銷燬,抓取hprof文件:

DemoTextView實例的GC最短路徑:

DemoActivity實例的GC最短路徑:

DemoRunnable實例的GC最短路徑:

  • 主線程中有待執行的Message實例
  • Message實例中的target成員變量爲ViewRootImpl中的內部類ViewRootHandler實例
  • ViewRootHandler實例持有外部ViewRootImpl實例的引用,可知其爲非靜態內部類
  • ViewRootImpl實例間接引用了activity對象,即DemoActivity實例
  • DemoActivity又引用了DemoTextView的實例mDemo對象
  • Message實例中的callback成員變量指向了mRunnable對象。

因此只有當該Message實例被回收時,才能解引用,最終回收上述三個發生泄露的對象。

接下來結合View的源碼分析,Message實例是如何導致mDemo對象泄露:

    public boolean postDelayed(Runnable action, long delayMillis) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.postDelayed(action, delayMillis);
        }
        // TODO
    }

根據View的繪製流程和異步消息流程,此處的mAttachInfo是在ViewRootImpl實例繪製View實例的第一幀時調用ViewdispatchAttachedToWindow方法賦值的,因此分析ViewRootImpl源碼:

    final class ViewRootHandler extends Handler {...}

    final ViewRootHandler mHandler = new ViewRootHandler();

    public ViewRootImpl(Context context, Display display) {
        // TODO
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
        // TODO
    }
  • ViewRootImpl內部定義了一個非靜態內部類ViewRootHandler用於向主線程發送事件,其實例持有外部類ViewRootImpl實例的引用
  • mAttachInfo中的成員變量mHandlerViewRootHandler的實例,因此間接持有ViewRootImpl實例的引用
  • 因此View中發送的消息會持有ViewRootImpl實例的引用

ViewRootImpl中有一個方法,用於和Activity實例建立關聯:

    public void setActivityConfigCallback(ActivityConfigCallback callback) {
        mActivityConfigCallback = callback;
    }

這個方法由PhoneWindow對象調用:

    /** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
    void onViewRootImplSet(ViewRootImpl viewRoot) {
        viewRoot.setActivityConfigCallback(mActivityConfigCallback);
    }

而這個方法又是由DecorView對象調用:

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // TODO
        mWindow.onViewRootImplSet(getViewRootImpl());
    }

再分析mActivityConfigCallback對象的賦值過程,其位於PhoneWindow的構造方法中:

    /**
     * Constructor for main window of an activity.
     */
    public PhoneWindow(Context context, Window preservedWindow,
            ActivityConfigCallback activityConfigCallback) {
        this(context);
        // Only main activity windows use decor context, all the other windows depend on whatever
        // context that was given to them.
        mUseDecorContext = true;
        if (preservedWindow != null) {
            mDecor = (DecorView) preservedWindow.getDecorView();
            // TODO
        }
        // TODO
        mActivityConfigCallback = activityConfigCallback;
    }

PhoneWindow對象的實例化過程位於Activity類中:

    final void attach(...) {
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
    }

由於以上部分涉及到Activity的加載機制,這裏不再過多發散,以流程圖的形式直接展示:

因此只有當通過View發送到主線程消息隊列中的Message對象全部回收時,才能讓Message對象中的targetcallback等成員變量解引用,從而讓ViewRootImpl實例解引用,最終讓Activity實例和自定義View實例解引用,以便進行銷燬。

同上一節,在適當的時候主動移除Handler中剩餘的消息:

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(mRunnable);
    }

九、 Timer的延時任務

在Activity中,以靜態內部類的方式定義一個TimeTask,實例化爲mTask並通過Timer定時執行,主動調用finish方法進行銷燬,抓取hprof文件:

DemoTask實例的GC最短路徑:

mTask對象發生內存泄露,其被TimerThread的實例所持有,分析Timer的源碼:

    private final TaskQueue queue = new TaskQueue();
    private final TimerThread thread = new TimerThread(queue);

    public Timer() {
        this("Timer-" + serialNumber());
    }
    
    public Timer(boolean isDaemon) {
        this("Timer-" + serialNumber(), isDaemon);
    }
    
    public Timer(String name) {
        thread.setName(name);
        thread.start();
    }
    
    public Timer(String name, boolean isDaemon) {
        thread.setName(name);
        thread.setDaemon(isDaemon);
        thread.start();
    }

  • Timer內部有一個task隊列和timer線程
  • 不管是調用的哪種schedule,最終都是執行內部的同一個方法sched

接下來分析TimerThread源碼:

  • TimerThread本質上還是一個thread
  • 內部執行死循環,用於輪詢queue中的task
  • 剛創建時會進入等待,當有task入隊時,喚醒
  • 當task出隊時,如果隊列爲空,又進入等待
  • 當標誌位改變,且喚醒後隊列爲空,則中斷循環,線程執行結束

因此,當我們需要銷燬資源時,應該主動去調用方法,將調度的task移除,避免task泄露,但是銷燬分兩種方式,一種是Timercancel,另一種是TimerTaskcancel,下面我們分別進行介紹二者的區別。

先看Timercancel

    public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
    }
  • 改變線程標誌位,清空隊列,喚醒線程,讓其自然結束
  • 調用該方法之後無法再添加任何task,也就是說該timer已經被銷燬

然後是TimerTaskcancel

    public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }
  • 改變標誌位,當該task下一次被輪詢到時,會被從隊列中移除
  • 不影響timer本身的運行

所以,當我們需要完全銷燬所有資源時,可以直接銷燬timer:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mTimer != null) {
            mTimer.cancel();
        }
    }

十、 HandlerThread

在Activity中,以靜態內部類的方式定義一個HandlerThread和Handler,實例化爲mHandler並執行一個延時任務,主動調用finish方法進行銷燬,抓取hprof文件:

DemoHandlerThread實例的GC最短路徑:

DemoHandler實例的GC最短路徑:

Handler內存泄露原理一樣,由於mThread中存在未執行的Message實例,其內部持有mHandler對象的引用,故在該Message實例執行結束之前,二者均無法回收。

下面分析HandlerThread的源碼:

  • HandlerThread本質上也是一個Thread
  • 其內部有消息輪詢對象mLooper
  • 當線程開始時,便可以把輪詢對象返回用於構造Handler實例來實現子線程調度
  • 常用於構造和主線程並行的子線程消息隊列,實現異步和併發能力

因此,當我們需要銷燬資源時,應該移除mHandler中的消息,並退出線程:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        mThread.quit();
    }

十一、 Thread

在Activity中,以靜態內部類的方式定義一個Thread,實例化爲mThread並執行任務,Activity主動調用finish方法進行銷燬,抓取hprof文件:

Thread實例的GC最短路徑:

mThread對象發生內存泄露,分析Thread源碼:

調用start後,會通過JNI調用native層的創建方法:

這裏對native層的Thread對象進行實例化,持有java層Thread對象的引用,並將內存地址指針賦值給java層的變量nativePeer

當線程沒有被創建/執行,或者已經被銷燬時,這個native指針等於0。

接下來分析Thread的銷燬過程:

java層Thread類的stop方法不再被支持用於強制結束線程,因爲那將會導致一些不可預料的問題出現,例如:

數據庫IO屬於阻塞的、耗時的原子操作,如果在線程中強行中斷,會引起嚴重問題

常見的做法是在主線程中調用子線程的join方法,將並行執行的任務變爲順序串行執行,並且需要確保這樣調用後子線程的生命週期不會影響主線程的資源回收。

當線程自然執行結束後,由native層主動進行銷燬:

將java層的變量nativePeer置爲0,native層對java層解引用,之後,java層Thread對象便可以被GC回收。

java層Thread主要是做一些計時方面的操作、暴露一些api供上層調用,通過JNI調用native層方法,改變線程狀態標誌位,來影響CPU調度的時間片執行情況。而native層纔是線程核心邏輯具體實現,控制線程整個生命週期和調度。

按照線程的設計,對於線程本身未執行結束造成的內存泄露,即子線程的執行無法被強制結束,我們只需要確保其不會對主線程或其他子線程的回收造成影響,當該子線程自然執行結束後,便可以被回收。另一方面,儘量避免這種使用方式,而是選擇可控的方式進行程序設計:

  • run方法中通過標誌位進行控制,當需要結束線程時,改變標誌位,及時執行結束

  • 使用HandlerThread

十二、 Bitmap

在Activity中,通過BitmapFactory解析實例化Bitmap對象,Activity主動調用finish方法進行銷燬,抓取hprof文件無任何內存泄露現象,分析BitmapFactory中的decodeResource方法:


其最終會走到decodeStream方法中:

  • 如果是資源文件的位圖,則走nativeDecodeAsset流程
  • 如果是本地文件的位圖,則走nativeDecodeStream流程

在native層,這倆方法最終都會走到doDecode中,這個方法代碼較長,且我自己也理解地不透徹,因此僅簡單介紹該方法中的要點:

  • native層有可以複用的位圖對象時,進行復用,即對其像素數據進行回收和重新設置
  • native層沒有可複用的位圖對象時,分配新內存來創建對象

接下來,我們分析native層的Bitmap類。

對於創建位圖:

直接調用了java層的位圖構造方法創建對象:

  • java層持有native層內存地址指針強引用,但從demo實際表現來看並沒有出現內存泄露
  • 創建了NativeAllocationRegistry對象,其用於將java層對象和native內存建立聯繫,當java層GC可達時,將執行傳入的native層的Finalizer用於回收native內存,正是這種方式,即便沒有在應用層主動調用recycle,也不會出現內存泄露問題

我們查看native層這個Finalizer:

直接銷燬了對象。

接着,我們分析回收位圖:

對位圖中的像素數據進行釋放,並重置對象的一些屬性。

以上是通過分析BitmapFactory粗略瞭解到的關於位圖的原理,除此之外,常用的實例化位圖方式還有Bitmap中的createBitmap方法,其最終調用的仍然是native層BitmapcreateBitmap方法。

綜上所述,當我們使用位圖時,如果沒有主動去回收對象,並不會造成內存泄露,原因是位圖實例化時會通過NativeAllocationRegistry對象進行管控從而優化,但是如果我們主動回收,及時釋放native層無用的像素數據,可以更好地節省內存,並且降低native層複用時纔去回收像素數據的開銷。


十三、 單例模式

單例模式本身不會有內存泄露問題,但是由於其生命週期過長的特點,尤其是對於餓漢式單例模式,生命週期更是與整個進程一樣長,如果未及時銷燬釋放不再使用的資源,會造成不必要的內存浪費,另一方面,對於進程保活的情況下,熱啓動時並不會重置單例對象中的變量,導致出現數據錯誤。

因此,對於單例模式:

  • 設計時,增加用於銷燬資源的方法,在適當的時候調用,並將實例解引用置空
  • 儘量避免餓漢式設計,靜態代碼塊和靜態聲明實例化都是冷啓動的負擔

十四、 總結

  • 有內存泄露
類型 典型場景 原因
內部類持有外部引用 1. 非靜態內部類
2. 匿名內部類
和外部類爲依賴關係,實例無法單獨存活,內部類實例存活導致外部類實例無法回收
框架層持有binder引用 1. BroadcastReceiver
2. ContentObserver
未主動對框架層解引用,導致binder對象泄露,框架層一定泄露,應用層可能泄露
其他 1. Handler-Message
2. TimerTask
3. 單例模式
生命週期過長未及時回收資源
  • 無內存泄露
類型 規避策略
Cursor 重寫finalize,在GC輪詢到時主動釋放資源
Bitmap 使用NativeAllocationRegistry,在GC可達時主動釋放native資源
發佈了87 篇原創文章 · 獲贊 34 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章