为什么view的setOnClickListener引用Activity不会内存泄漏
Posted CrazyApes
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么view的setOnClickListener引用Activity不会内存泄漏相关的知识,希望对你有一定的参考价值。
学就完事
本文:https://www.jianshu.com/p/4b3aad1b8dba
文章目录
前言
问:一个Activity
实现了onClickListener
,这时1个Button
使用了setOnClickListener(this)
为什么不会出现内存泄露?
答:因为Activity
销毁的时候会释放View
引用对象,所以不会内存泄露啊 。
问:能不能在细点?
答:?_? ,厄,window
会detach
所有的view
,然后view
逐个释放引用。
问:再细点。
答:。。。
什么是内存泄露
首先,什么是内存泄露?
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
Java内存泄漏的根本原因是什么呢?
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。
android 中的内存泄露也是这个意思,比如当退出Activity
的时候,如果由于Activity
被其它生命周期更长的对象所持有引用,而无法释放其所持有的内存,就会导致内存泄露。
答题
Activity
在销毁时肯定是会释放对应的资源的,比如将Activity
出栈,移除Window
上所有的View
,释放View
中的各种事件,最后Window
啥的资源全置空。呼叫GC来一波回收。- 关于
View
和Activity
循环引用的问题,GC会通过可达性分析来解决循环引用的问题。
GC会从GC roots
对象节点向下遍历,将所有不可达的对象进行标记,标记后确认是否有必要执行其finalize()
方法,如果没有,或者已经被执行,则回收。如果有,则进入一个专门的队列,等待执行。
GC会对队列中的对象进行第二次小规模标记,如果对象没有在finalize()
方法中重新与引用链关联,就会被回收。
兴趣
由于对于Activity
在何时去释放View
的了解不够细致,所以,后面篇幅写了大概从源码去追溯View
释放的一个过程。有兴趣的可以一起看一下,如果我描述的不太对,欢迎大家评论区指正。
我会非常感激您的指导。
OnClickListener在View中的引用位置
首先,咱们得先知道最终setOnClickListener(this)
的this
引用在View
中如何被引用
##View#setOnClickListener
可以看到引用被赋值于getListenerInfo().mOnClickListener
// android31 android.view.View#setOnClickListener
public void setOnClickListener(@Nullable OnClickListener l)
if (!isClickable())
setClickable(true);
getListenerInfo().mOnClickListener = l;
我们再去看看getListenerInfo()
是谁
View#getListenerInfo
ListenerInfo
是View
用来存储一系列listener
的成员信息类
那我们基本就可以确定最终将我们传入的OnClickListener
引用在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;
...
探索该引用何时被释放
在View
中查找mListenerInfo
的使用,以及何时被置空释放。
在文件中检索mListenerInfo = null
或者 getListenerInfo = null
或者其它类似操作。
显然,我没有找到类似的方法和操作,哈哈哈,好尴尬。
既然它没有,那我们就接着找找View是什么时候被释放引用的,如果View的引用被释放,那他引用的这个对象也属于无效对象了,仍可以被释放。
Activity#onDestroy
既然是销毁的过程,咱就是Activity.onDestroy
方法开始。
- 发现通过
mManagedDialogs
关闭所有弹框 - 发现通过
mManagedCursors
关闭所有cursor
- 发现通过
mSearchManager
停止搜索 - 发现通过
mActionBar
销毁ActionBar
- 发现通过
dispatchActivityDestroyed
分发ActivityLifecycleCallbacks
- 发现通过
notifyContentCaptureManagerIfNeeded
哎呀,不知道做什么的。回头补补 - 发现通过
mUiTranslationController
分发UiTranslationStateCallback
// 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();
唉,没看到window
和view
相关操作呢。
那咱往上找一下。
Activity#performDestroy
哎~ 我们看到一句合适的代码mWindow.destroy
其它的暂时先不看,我们直接去看mWindow.destroy
干了什么
// 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);
确认下mWindow
是谁,找到初始化的地方
// android31 android.app.Activity#attach
@UnsupportedAppUsage
private Window mWindow;
final void attach(Context context, ...)
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
PhoneWindow
并没有重写destroy
所以最终还是走了继承自Window
的destroy
方法
// android31 android.view.Window#destroy
/** @hide */
public final void destroy()
mDestroyed = true;
好吧,依然没看到什么有用的信息。
咱么接着向上扒!
看看谁调用的performDestroy
Instrumentation#callActivityOnDestroy
比较眼熟的Instrumentation
类哈
没有合适的信息,继续向上
// android-31 android.app.Instrumentation#callActivityOnDestroy
public void callActivityOnDestroy(Activity activity)
...
activity.performDestroy();
ActivityThread#performDestroyActivity
我们暂时剃掉了部分影响不大的代码,直接从mInstrumentation.callActivityOnDestroy
之后的流程继续看
- 有
r.window.closeAllPanels()
,移除系统菜单(ActionBar菜单)
的View
- 有
mActivities.remove(r.token);
,移除Activity
引用 - 其它释放操作
// 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);
感觉还不够啊,继续向上扒
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;
到此处,window
开始移除所有的子view
我们不太确定是哪个方法最终生效了。
但是我们可以借助Debug Log,通过监控View
的detached
事件来确定流程。
如下图
哎呀呀呀,你看,后面的流程很明白了。
wm.removeViewImmediate(v)
调用WindowManagerImpl#removeViewImmediate
方法
// android-30 android.view.WindowManagerImpl#removeViewImmediate
@Override
public void removeViewImmediate(View view)
mGlobal.removeView(view, true);
- 然后进入
mGlobal.removeView(view, true);
方法就是WindowManagerGlobal#removeView
- 调用
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);
- 在
removeViewLocked
方法中进入ViewRootImpl#die
方法 ViewRootImpl#die
方法中执行ViewRootImpl#doDie
开始执行die操作ViewRootImpl#doDie
中调用ViewRootImpl#dispatchDetachedFromWindow
ViewRootImpl#dispatchDetachedFromWindow
中终于mView.dispatchDetachedFromWindow();
- 我们可以去看
view
的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
这条线,稍微有些不同。
开始做detached
相关工作,并在onDetachedFromWindowInternal
方法中释放了部分事件传递的资源
@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
可以看到这个方法中,我们remove
了很多操作
比如removeUnsetPressCallback
,removeLongPressCallback
,removePerformClickCallback
,
clearAccessibilityThrottles
等
@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());
为什么要remove这些Callback,他们做了什么,咱们接着查
View.removePerformClickCallback
这里可以看到,判断了mPerformClick != null
就会去remove
/**
* Remove the pending click action
*/
@UnsupportedAppUsage
private void removePerformClickCallback()
if (mPerformClick != null)
removeCallbacks(mPerformClick);
而removeCallbacks
函数做了什么?
移除了一些特殊的message
操作,我们看到了attachInfo.mHandler
,也就是说,如果没有取消的话,确实可能存在内存泄露的风险,具体要看这个mHandler
是怎么实现得。
/**
* <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;
哈。跑深了哈,不过确认了子View
在detachedFromWindow
之后确实释放了很多东西,移除了所有可能延迟执行的Callback
,关闭动画等,取消UI操作啥的。
回归上一个方法ActivityThread#handleDestroyActivity
方法中做了如上的操作,那么我们继续向上看看,还做了什么操作
ActivityThread#handleRelaunchActivityInner
这个方法很多明显的置空操作了。
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);
到这里,基本window
,view
都被释放空了。
参考文献
暂无
以上是关于为什么view的setOnClickListener引用Activity不会内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
为什么view的setOnClickListener引用Activity不会内存泄漏
尝试调用虚拟方法 'void android.widget.Button.setOnClickListener(android.view.View$onClickListener)'
Android Studio:尝试在空对象引用上调用虚拟方法“void android.view.View.setOnClickListener”
尝试在空对象引用上调用虚拟方法“void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)”
尝试调用虚方法'void android.view.View.setOnClickListener [duplicate]