Why does the setOnClickListener reference Activity of view not leak memory

Posted by Dragoonus on Thu, 03 Mar 2022 17:44:06 +0100

Learning is done

preface

Q: an Activity implements onClickListener. At this time, a Button uses setOnClickListener(this)
Why is there no memory leak?

A: because the View reference object will be released when the Activity is destroyed, there will be no memory leakage.

Q: can you make it more detailed?

Answer: Yes, Well, the window will detach all view s, and then release the references one by one.

Q: be more detailed.

A:...

What is a memory leak

First, what is a memory leak?
Memory leakage refers to the continuous occupation of memory by useless objects (objects that are no longer used) or the failure to release the memory of useless objects in time, resulting in the waste of memory space, which is called memory leakage. Sometimes the memory leak is not serious and difficult to detect, so that developers do not know that there is a memory leak, but sometimes it can be very serious, and you will be prompted Out of memory.
What is the root cause of Java memory leak?
If an object with a long life cycle holds a reference to an object with a short life cycle, memory leakage is likely to occur. Although the object with a short life cycle is no longer needed, it cannot be recycled because the object with a long life cycle holds its reference. This is the scene of memory leakage in Java.

This is also the meaning of memory leakage in Android. For example, when exiting an Activity, if the Activity cannot release its memory because it is held and referenced by other objects with a longer life cycle, it will lead to memory leakage.

answer

  • When an Activity is destroyed, it will certainly release the corresponding resources. For example, take the Activity out of the stack, remove all views on the Window, release various events in the View, and finally empty all resources in the Window. Call GC for a wave of recycling.
  • With regard to the circular reference of View and Activity, GC will solve the problem of circular reference through reachability analysis.
    GC will traverse down from the GC roots object node and mark all unreachable objects. After marking, confirm whether it is necessary to execute its finalize() method. If not, or it has been executed, it will be recycled. If so, enter a special queue and wait for execution.
    GC will mark the objects in the queue for the second time. If the objects are not re associated with the reference chain in the finalize() method, they will be recycled.

interest

Since the understanding of when the Activity releases the View is not detailed enough, the following section describes the process of tracing the release of the View from the source code. Those who are interested can have a look together. If my description is not quite right, you are welcome to make corrections in the comment area.
I would appreciate your guidance.

Reference location of OnClickListener in View

First, we need to know how the this reference of setOnClickListener(this) is referenced in the View

##View#setOnClickListener

You can see that the reference is assigned to getlistenerinfo() mOnClickListener

    // android31  android.view.View#setOnClickListener
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

Let's see who getListenerInfo() is

View#getListenerInfo

ListenerInfo is a member information class used by View to store a series of listener s

Then we can basically determine that the OnClickListener we passed in is finally referenced in mListenerInfo.

    // android31  android.view.View#getListenerInfo
    ListenerInfo mListenerInfo;
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

    static class ListenerInfo {
            ...
            public OnClickListener mOnClickListener;
            ...
    }

Explore when the reference is released

Find the use of mListenerInfo in the View and when it is set to empty.

Retrieve mListenerInfo = null or getListenerInfo = null or other similar operations in the file.

Obviously, I didn't find a similar method and operation. Hahaha, it's so embarrassing.

Since it doesn't exist, we will then find out when the reference of View is released. If the reference of View is released, the object it refers to is also an invalid object and can still be released.

Activity#onDestroy

Since it is the process of destruction, we are activity The ondestroy method starts.

  • It is found that all pop ups are closed through mManagedDialogs
  • It is found that all cursors are closed through mManagedCursors
  • Discovery stop search through msearch Manager
  • It is found that the ActionBar is destroyed through the mcactionbar
  • It is found that activitylifecycle callbacks are distributed through dispatchActivityDestroyed
  • It is found that notifyContentCaptureManagerIfNeeded. Alas, I don't know what to do. Go back and make up
  • It is found that the UiTranslationStateCallback is distributed through the mUiTranslationController
    // android31 android.app.Activity#onDestroy
    @CallSuper
    protected void onDestroy() {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
        mCalled = true;

        // dismiss any dialogs we are managing.
        if (mManagedDialogs != null) {
            final int numDialogs = mManagedDialogs.size();
            for (int i = 0; i < numDialogs; i++) {
                final ManagedDialog md = mManagedDialogs.valueAt(i);
                if (md.mDialog.isShowing()) {
                    md.mDialog.dismiss();
                }
            }
            mManagedDialogs = null;
        }

        // close any cursors we are managing.
        synchronized (mManagedCursors) {
            int numCursors = mManagedCursors.size();
            for (int i = 0; i < numCursors; i++) {
                ManagedCursor c = mManagedCursors.get(i);
                if (c != null) {
                    c.mCursor.close();
                }
            }
            mManagedCursors.clear();
        }

        // Close any open search dialog
        if (mSearchManager != null) {
            mSearchManager.stopSearch();
        }

        // SDK 24 Add , But it Add in 19 ,and remove in 20
        if (mActionBar != null) {
            mActionBar.onDestroy();
        }

        dispatchActivityDestroyed();

        // SDK 29 Add 
        notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_STOP);

        // SDK 31 Add
        if (mUiTranslationController != null) {
            mUiTranslationController.onActivityDestroyed();
        }
    }

Alas, I don't see window and view related operations.
Let's look up.

Activity#performDestroy

Hey ~ we see an appropriate code mwindow destroy
Don't look at the others for the time being. Let's go directly to mwindow What did destroy do

    // android31 android.app.Activity#performDestroy
    final void performDestroy() {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performDestroy:"
                    + mComponent.getClassName());
        }
        dispatchActivityPreDestroyed();
        mDestroyed = true;
        mWindow.destroy();
        mFragments.dispatchDestroy();
        onDestroy();
        EventLogTags.writeWmOnDestroyCalled(mIdent, getComponentName().getClassName(),
                "performDestroy");
        mFragments.doLoaderDestroy();
        if (mVoiceInteractor != null) {
            mVoiceInteractor.detachActivity();
        }
        dispatchActivityPostDestroyed();
        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
    }

Confirm who mWindow is and find the initialization place

    // android31 android.app.Activity#attach
    @UnsupportedAppUsage
    private Window mWindow;

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

PhoneWindow does not override destroy

So I finally took the destroy method inherited from Window

    // android31 android.view.Window#destroy
    /** @hide */
    public final void destroy() {
        mDestroyed = true;
    }

Well, I still don't see any useful information.
Let's pick it up!
Look who called performDestroy

Instrumentation#callActivityOnDestroy

Familiar Instrumentation class
No suitable information, keep going up

    // android-31 android.app.Instrumentation#callActivityOnDestroy
    public void callActivityOnDestroy(Activity activity) {
      ...
      activity.performDestroy();
    }

ActivityThread#performDestroyActivity

We temporarily shaved off some of the code that had little impact, directly from mminstrumentation The process after callactivityondestroy continues

  • With r.window Closeallpanels(), remove the View of the system menu (ActionBar menu)
  • There are m activities remove(r.token); , Remove Activity reference
  • Other release operations
    // android-31 android.app.ActivityThread#performDestroyActivity
    /** Core implementation of activity destroy call. */
    void performDestroyActivity(ActivityClientRecord r, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {

        ...
        try {
            r.activity.mCalled = false;
            mInstrumentation.callActivityOnDestroy(r.activity);
            if (!r.activity.mCalled) {
                throw new SuperNotCalledException("Activity " + safeToComponentShortString(r.intent)
                        + " did not call through to super.onDestroy()");
            }
            if (r.window != null) {
                r.window.closeAllPanels();
            }
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException("Unable to destroy activity "
                        + safeToComponentShortString(r.intent) + ": " + e.toString(), e);
            }
        }
        r.setState(ON_DESTROY);
        mLastReportedWindowingMode.remove(r.activity.getActivityToken());
        schedulePurgeIdler();
        synchronized (this) {
            if (mSplashScreenGlobal != null) {
                mSplashScreenGlobal.tokenDestroyed(r.token);
            }
        }
        // updatePendingActivityConfiguration() reads from mActivities to update
        // ActivityClientRecord which runs in a different thread. Protect modifications to
        // mActivities to avoid race.
        synchronized (mResourcesManager) {
            mActivities.remove(r.token);
        }
        StrictMode.decrementExpectedActivityCount(activityClass);
    }

I don't think it's enough. Keep picking up

ActivityThread#handleDestroyActivity

    @Override
    public void handleDestroyActivity(ActivityClientRecord r, boolean finishing, int configChanges,
            boolean getNonConfigInstance, String reason) {
        performDestroyActivity(r, finishing, configChanges, getNonConfigInstance, reason);
        cleanUpPendingRemoveWindows(r, finishing);
        WindowManager wm = r.activity.getWindowManager();
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    // Hold off on removing this until the new activity's window is being added.
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    // We can only keep the part of the view hierarchy that we control,
                    // everything else must be removed, because it might not be able to
                    // behave properly when activity is relaunching.
                    r.window.clearContentView();
                } else {
                    wm.removeViewImmediate(v);
                }
            }
            if (wtoken != null && r.mPendingRemoveWindow == null) {
                WindowManagerGlobal.getInstance().closeAll(wtoken,
                        r.activity.getClass().getName(), "Activity");
            } else if (r.mPendingRemoveWindow != null) {
                // We're preserving only one window, others should be closed so app views
                // will be detached before the final tear down. It should be done now because
                // some components (e.g. WebView) rely on detach callbacks to perform receiver
                // unregister and other cleanup.
                WindowManagerGlobal.getInstance().closeAllExceptView(r.token, v,
                        r.activity.getClass().getName(), "Activity");
            }
            r.activity.mDecor = null;
        }
        if (r.mPendingRemoveWindow == null) {
            // If we are delaying the removal of the activity window, then
            // we can't clean up all windows here.  Note that we can't do
            // so later either, which means any windows that aren't closed
            // by the app will leak.  Well we try to warning them a lot
            // about leaking windows, because that is a bug, so if they are
            // using this recreate facility then they get to live with leaks.
            WindowManagerGlobal.getInstance().closeAll(r.token,
                    r.activity.getClass().getName(), "Activity");
        }

        // Mocked out contexts won't be participating in the normal
        // process lifecycle, but if we're running with a proper
        // ApplicationContext we need to have it tear down things
        // cleanly.
        Context c = r.activity.getBaseContext();
        if (c instanceof ContextImpl) {
            ((ContextImpl) c).scheduleFinalCleanup(r.activity.getClass().getName(), "Activity");
        }
        if (finishing) {
            ActivityClient.getInstance().activityDestroyed(r.token);
        }
        mSomeActivitiesChanged = true;
    }


At this point, window starts removing all child view s
We're not sure which method finally worked.

However, with the help of Debug Log, we can determine the process by monitoring the detached event of View.

As shown below

Oh, you see, the following process is very clear.

  • wm.removeViewImmediate(v) calls the WindowManagerImpl#removeViewImmediate method
    // android-30 android.view.WindowManagerImpl#removeViewImmediate
    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }
  • Then go to mglobal removeView(view, true); The method is WindowManagerGlobal#removeView
  • Call removeViewLocked(index, immediate);
    // android-30 android.view.WindowManagerGlobal#removeView
    @UnsupportedAppUsage
    public void removeView(View view, boolean immediate) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }

            throw new IllegalStateException("Calling with view " + view
                    + " but the ViewAncestor is attached to " + curView);
        }
    }

    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();

        if (root != null) {
            root.getImeFocusController().onWindowDismissed();
        }
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

  • Enter the ViewRootImpl#die method in the removeViewLocked method
  • Execute ViewRootImpl#doDie in the ViewRootImpl#die method to start the die operation
  • Calling ViewRootImpl#dispatchDetachedFromWindow in ViewRootImpl#doDie
  • ViewRootImpl#dispatchDetachedFromWindow dispatchDetachedFromWindow();
  • We can go to view's dispatchDetachedFromWindow
    /**
     * @param immediate True, do now if not in traversal. False, put on queue and do later.
     * @return True, request has been queued. False, request has been completed.
     */
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }

    void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }

            if (mAdded && !mFirst) {
                destroyHardwareRenderer();

                if (mView != null) {
                    int viewVisibility = mView.getVisibility();
                    boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                    if (mWindowAttributesChanged || viewVisibilityChanged) {
                        // If layout params have been changed, first give them
                        // to the window manager to make sure it has the correct
                        // animation info.
                        try {
                            if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                    & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                                mWindowSession.finishDrawing(
                                        mWindow, null /* postDrawTransaction */);
                            }
                        } catch (RemoteException e) {
                        }
                    }

                    destroySurface();
                }
            }

            mAdded = false;
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

    void dispatchDetachedFromWindow() {
        // Make sure we free-up insets resources if view never received onWindowFocusLost()
        // because of a die-signal
        mInsetsController.onWindowFocusLost();
        mFirstInputStage.onDetachedFromWindow();
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            mView.dispatchDetachedFromWindow();
        }

        mAccessibilityInteractionConnectionManager.ensureNoConnection();
        mAccessibilityManager.removeAccessibilityStateChangeListener(
                mAccessibilityInteractionConnectionManager);
        mAccessibilityManager.removeHighTextContrastStateChangeListener(
                mHighContrastTextManager);
        removeSendWindowContentChangedCallback();

        destroyHardwareRenderer();

        setAccessibilityFocus(null, null);

        mInsetsController.cancelExistingAnimations();

        mView.assignParent(null);
        mView = null;
        mAttachInfo.mRootView = null;

        destroySurface();

        if (mInputQueueCallback != null && mInputQueue != null) {
            mInputQueueCallback.onInputQueueDestroyed(mInputQueue);
            mInputQueue.dispose();
            mInputQueueCallback = null;
            mInputQueue = null;
        }
        try {
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }
        // Dispose receiver would dispose client InputChannel, too. That could send out a socket
        // broken event, so we need to unregister the server InputChannel when removing window to
        // prevent server side receive the event and prompt error.
        if (mInputEventReceiver != null) {
            mInputEventReceiver.dispose();
            mInputEventReceiver = null;
        }

        mDisplayManager.unregisterDisplayListener(mDisplayListener);

        unscheduleTraversals();
    }

View.dispatchDetachedFromWindow

This line is slightly different.
Start the detached related work, and release some event transmission resources in the onDetachedFromWindowInternal method

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchDetachedFromWindow() {
        AttachInfo info = mAttachInfo;
        if (info != null) {
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(GONE);
                if (isShown()) {
                    // Invoking onVisibilityAggregated directly here since the subtree
                    // will also receive detached from window
                    onVisibilityAggregated(false);
                }
            }
        }

        onDetachedFromWindow();
        onDetachedFromWindowInternal();

        if (info != null) {
            info.mViewRootImpl.getImeFocusController().onViewDetachedFromWindow(this);
        }

        ListenerInfo li = mListenerInfo;
        final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                li != null ? li.mOnAttachStateChangeListeners : null;
        if (listeners != null && listeners.size() > 0) {
            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
            // perform the dispatching. The iterator is a safe guard against listeners that
            // could mutate the list by calling the various add/remove methods. This prevents
            // the array from being modified while we iterate it.
            for (OnAttachStateChangeListener listener : listeners) {
                listener.onViewDetachedFromWindow(this);
            }
        }

        if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
            mAttachInfo.mScrollContainers.remove(this);
            mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
        }

        mAttachInfo = null;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchDetachedFromWindow();
        }

        notifyEnterOrExitForAutoFillIfNeeded(false);
        notifyAppearedOrDisappearedForContentCaptureIfNeeded(false);
    }

View.onDetachedFromWindowInternal

You can see that in this method, we remove a lot of operations
For example, removeUnsetPressCallback, removeLongPressCallback, removePerformClickCallback,
Clearaccessibility throttles, etc

    @CallSuper
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    protected void onDetachedFromWindowInternal() {
        mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
        mPrivateFlags3 &= ~PFLAG3_TEMPORARY_DETACH;

        removeUnsetPressCallback();
        removeLongPressCallback();
        removePerformClickCallback();
        clearAccessibilityThrottles();
        stopNestedScroll();

        // Anything that started animating right before detach should already
        // be in its final state when re-attached.
        jumpDrawablesToCurrentState();

        destroyDrawingCache();

        cleanupDraw();
        mCurrentAnimation = null;

        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            hideTooltip();
        }

        AccessibilityNodeIdManager.getInstance().unregisterViewWithId(getAccessibilityViewId());
    }

Why remove these callbacks? What did they do? Let's continue to check

View.removePerformClickCallback

Here you can see that you have judged mperformclick= Null will go to remove

   /**
     * Remove the pending click action
     */
    @UnsupportedAppUsage
    private void removePerformClickCallback() {
        if (mPerformClick != null) {
            removeCallbacks(mPerformClick);
        }
    }

What does the removeCallbacks function do?
After removing some special message operations, we see attachinfo mHandler, that is, if it is not cancelled, there may be a risk of memory leakage. It depends on how the mHandler is implemented.

    /**
     * <p>Removes the specified Runnable from the message queue.</p>
     *
     * @param action The Runnable to remove from the message handling queue
     *
     * @return true if this view could ask the Handler to remove the Runnable,
     *         false otherwise. When the returned value is true, the Runnable
     *         may or may not have been actually removed from the message queue
     *         (for instance, if the Runnable was not in the queue already.)
     *
     * @see #post
     * @see #postDelayed
     * @see #postOnAnimation
     * @see #postOnAnimationDelayed
     */
    public boolean removeCallbacks(Runnable action) {
        if (action != null) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                attachInfo.mHandler.removeCallbacks(action);
                attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                        Choreographer.CALLBACK_ANIMATION, action, null);
            }
            getRunQueue().removeCallbacks(action);
        }
        return true;
    }

Ha. I ran deeper, but I confirmed that the sub View did release a lot of things after detachedFromWindow, removing all callbacks that may delay execution, closing animation, canceling UI operations and so on.

Go back to the previous method. The ActivityThread#handleDestroyActivity method has done the above operations, so let's continue to look up and see what else has been done

ActivityThread#handleRelaunchActivityInner

This method has many obvious null operations.

  • r.activity = null;
  • r.window = null;
  • r.nextIdle = null;
    // android-30 android.app.ActivityThread#handleRelaunchActivityInner
    private void handleRelaunchActivityInner(ActivityClientRecord r, int configChanges,
            List<ResultInfo> pendingResults, List<ReferrerIntent> pendingIntents,
            PendingTransactionActions pendingActions, boolean startsNotResumed,
            Configuration overrideConfig, String reason) {
        // Preserve last used intent, it may be set from Activity#setIntent().
        final Intent customIntent = r.activity.mIntent;
        // Need to ensure state is saved.
        if (!r.paused) {
            performPauseActivity(r, false, reason, null /* pendingActions */);
        }
        if (!r.stopped) {
            callActivityOnStop(r, true /* saveState */, reason);
        }

        handleDestroyActivity(r.token, false, configChanges, true, reason);

        r.activity = null;
        r.window = null;
        r.hideForNow = false;
        r.nextIdle = null;
        // Merge any pending results and pending intents; don't just replace them
        if (pendingResults != null) {
            if (r.pendingResults == null) {
                r.pendingResults = pendingResults;
            } else {
                r.pendingResults.addAll(pendingResults);
            }
        }
        if (pendingIntents != null) {
            if (r.pendingIntents == null) {
                r.pendingIntents = pendingIntents;
            } else {
                r.pendingIntents.addAll(pendingIntents);
            }
        }
        r.startsNotResumed = startsNotResumed;
        r.overrideConfig = overrideConfig;

        handleLaunchActivity(r, pendingActions, customIntent);
    }

Here, the basic window and view are released empty.

reference

Not yet

Topics: Java Android Android Studio