WMS之addView和removeView时都发生肾么事了

3,880 阅读4分钟

一、前言

众所周知,Android中是通过WindowManger来添加、删除和更新视图,也就是WindowManager提供的addViewremoveViewupdateViewLayout方法。但是在添加或移除View的代码逻辑是啥,在这期间做了哪些工作,调用removeView方法就真的立刻删除view了吗?本文正是针对这些问题,对相关系统源码做了一次梳理和分析。作为一次学习源码过程,希望对相关知识点做到既能用,也明白底层原理。

在详细梳理代码前,首先我们先明确一下View的这些操作方法在哪里声明的,实现是怎样的,调用链路是啥。

1、「增删改」的View操作方法都是在ViewManager接口中声明的

public interface ViewManager
{
    /**
     * Assign the passed LayoutParams to the passed View and add the view to the window.
     * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
     * errors, such as adding a second view to a window without removing the first view.
     * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
     * secondary {@link Display} and the specified display can't be found
     * (see {@link android.app.Presentation}).
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

2、WindowManagerImplWindowManager接口的实现类,相关代码如下

		// 添加view
		@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
		
		//更新布局
    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }

		//移除view
		@Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

		//立即移除view
 		@Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

发现对于WindowManagerImpl来说,并不是它自己来处理,而是委托给mGlobal对象处理。mGlobal是啥呢,就是WindowManagerGlobal的对象。

3、WindowManagerGlobal才是真正处理View「增删改」的类

综上,addViewremoveView的处理链路如下

image-20211228180515868.png

二、源码分析

第一节总整体视角说明了添加移除View的参与者,以及调用链路。本小节,从代码角度出发,分析一下在addViewremoveView时具体逻辑和所做工作。

2.1 addView

前文已知,addView的代码实现在WindowManagerGlobal中,在该类中,有些重要的缓存数据的结构,详见注释说明

		// 	存放要添加的view对象	
		private final ArrayList<View> mViews = new ArrayList<View>();
		// 存放窗口对应的ViewRootImpl对象
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
   	// 存放将要移除的View对象,这个列表中的view属于未被移除但将要移除的状态
    private final ArraySet<View> mDyingViews = new ArraySet<View>();

1、WindowManagerGlobal#addView

代码如下(部分无关代码已省略)

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ......
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }
			// ①
            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }
			// ②
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
			// ③
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
              	// ④
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

下面👇详细说明下标注序号的代码作用。

①、首先是找到要添加的view的位置索引,对于新添加的view,由于以前不存在所以返回。同理,也就会跳过对将要移除view的处理。如果view的位置索引大于0,而且在存放将要移除view的列表中存在,那么会去调用ViewRootImpldoDie方法移除view,该方法在removeView时会详细介绍。

②、代码执行到该处,会创建ViewRootImpl对象,在构造方法中,初始化变量如mWindowSessionmWindowmAttachInfo等,还有一些标志变量如mAddedview已添加)、mFirstview首次添加)等。

③、缓存要添加的view对象、创建的ViewRootImpl对象等

④、调用ViewRootImplsetView方法,setView方法是与WMS进行沟通的桥梁。

2、setView

addView方法做了一些前期准备工作,然后调用setView开始真正添加视图,setView的代码如下(有删减)。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;//初始化mView

                mAttachInfo.mDisplayState = mDisplay.getState();
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                mWindowAttributes.copyFrom(attrs);
                if (mWindowAttributes.packageName == null) {
                    mWindowAttributes.packageName = mBasePackageName;
                }
                attrs = mWindowAttributes;
                setTag();

                ......
                mAdded = true;//view已添加
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                // ①
                requestLayout();
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    mInputChannel = new InputChannel();
                }
                mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                        & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    // ②
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
								......
                if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        ......
                    }
                   ......
                }
               ......
            }
        }
    }

①、调用requestLayout方法

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

检查当前线程是否是UI线程,然后调用scheduleTraversals方法,最终调用到performTraversals方法,这就是View测量、布局和绘制的起点。

②、mWindowSession.addToDisplaymWindowSession最终是由WMS创建并返回,是一个单例,即一个进程只有这么一个mWindowSession对象。APP进程就是通过mWindowSession跟WMS通讯的。

关于mWindowSession涉及到APP进程和WMS的交互和binder进程间通信,详细原理和实现这里暂不详述,后续会整理和输出相关文档。

2.2 removeView

移除视图时,使用方式和添加View类似,调用WindowManagerImpl#removeViewremoveViewImmediate方法,二者区别下面会详细说道。

1、移除View

先看removeView方法,还是由WindowManagerGlobal具体实现该方法,代码如下。

// ①
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);
        }
    }

①、调用WindowManagerImpl#removeView方法时,参数immediatefalse

②、还是先计算要移除view的位置索引,然后通过该索引找到ViewRootImpl中缓存的view。2个view相同则正常结束,否则抛出异常。

③、调用removeViewLocked移除view

2、removeViewLocked

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

        if (view != null) {
            InputMethodManager imm = InputMethodManager.getInstance();
            if (imm != null) {
                imm.windowDismissed(mViews.get(index).getWindowToken());
            }
        }
  		// ②
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
              	// ③
                mDyingViews.add(view);
            }
        }
    }

①、获取要移除view对应的ViewRootImpl对象,拿到ViewRootImpl对象后,获取mView成员变量,它缓存着要移除的view对象。mView的赋值是在addView时完成。

②、调用ViewRootImpl#die方法移除view

③、对于immediatefalse的情况,调用die方法后会返回true,所以deferred = true,表示延迟移除view此时将待移除的view加入mDyingViews列表缓存

3、die

代码如下👇

/**
     * @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;
    }

由方法注释大致可知,如果参数immediatetrue,且view不处于测量、布局和绘制中。(mIsInTraversalperformTraversals方法中赋值为true),立刻移除该view。否则将其放入队列稍后执行。所以

①、对于removeView方法,immediatefalse,不会走到doDie方法

②、发送一个消息(MSG_DIE)异步处理view的移除,查看Handler的代码

 @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ......
                case MSG_DIE:
                    doDie();
                    break;
                ......
            }
        }
    }

4、doDie

根据die方法的代码分析,removeView时会发送一个消息异步处理移除ViewHandler处理时,调用了doDie方法。代码如下👇

void doDie() {
        checkThread();
	 			......
        synchronized (this) {
            if (mRemoved) {// 首次移除,mRemoved为false
                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);
                            }
                        } catch (RemoteException e) {
                        }
                    }

                    mSurface.release();
                }
            }

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

①、在调用addView方法时,mAdded变量赋为true,故会调用dispatchDetachedFromWindow方法。

void dispatchDetachedFromWindow() {
        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);

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

        mSurface.release();
				......
        try {
          	// ②
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }

        // Dispose the input channel after removing the window so the Window Manager
        // doesn't interpret the input channel being closed as an abnormal termination.
        if (mInputChannel != null) {
            mInputChannel.dispose();
            mInputChannel = null;
        }

        mDisplayManager.unregisterDisplayListener(mDisplayListener);

        unscheduleTraversals();
    }

该方法就是真正移除view之处,包含资源的释放和重置。dispatchDetachedFromWindow会调用onDetachedFromWindow,我们可以重写onDetachedFromWindow实现一些资源释放的工作。然后在mWindowSession.remove(mWindow);处就移除view了。

②、mFirst变量在addView方法中创建ViewRootImpl对象时初始化为true,在performTraversals完成View布局后(performLayout)和绘制前(performDraw),还原为false。对于成功绘制并展示的viewmFirstfalse,那么就会进入条件体内。

③、调用WindowManagerGlobal#doRemove方法,代码如下

void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
        if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) {
            doTrimForeground();
        }
    }

该方法就是移除mRootsmParamsmViewsmDyingViews等列表保存的数据。

2.3 小结

通过对removeView流程的梳理和说明,我们明白了view移除过程所做的操作。简单总结一下就是

  1. removeView时并非立即将view销毁,而是将其放入一个列表进行缓存,然后通过handler发送移除view的消息MSG_DIE到消息队列,等待处理。
  2. 如果需要立即移除view,可以调用removeViewImmdiate方法。
  3. addView时,如果view已经添加过了(位置索引大于等于0),那么会先判断一下是否在待移除view列表中(mDyingViews)。**如果在,则不再等待MSG_DIE消息被执行,而是主动调用doDie方法立刻移除。否则,抛出view已添加的异常(**new IllegalStateException("View has already been added to the window manager.") )。