Android Window机制

Window的简单使用

public void addView(View view){
        mFloatingButton = new Button(this);
        mFloatingButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_MOVE:
                        mWindowParams.x = (int) event.getRawX();
                        mWindowParams.y = (int) event.getRawY();
                        //window更新操作
                        mWindowManager.updateViewLayout(mFloatingButton,mWindowParams);
                        break;
                        default:
                            break;
                }
                return true;
            }
        });
        mFloatingButton.setText("bottom");
        mWindowParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
        ,WindowManager.LayoutParams.WRAP_CONTENT,0,0, PixelFormat.TRANSPARENT);
        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
        | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
        mWindowParams.gravity = Gravity.LEFT | Gravity.TOP;
        mWindowParams.x = 100;
        mWindowParams.y = 300;
        //8.0以上使用TYPE_APPLICATION_OVERLAY  8.0以下使用Error
        mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY ;
        //新增window操作
        mWindowManager.addView(mFloatingButton,mWindowParams);
    }
  //移出window操作
   public void removeView(View view){
        if (null != mWindowManager){
            mWindowManager.removeView(mFloatingButton);
        }
    }

应用Window层级分为1-99 ,子window层级范围是1000-1999,系统window层级范围是2000-2999.
通过一下代码设置:

  mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY ;

需要配置系统弹窗权限:

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Window内部机制

addView方法

通过下面代码获取到WindowManager对象。

WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

WindowManager是接口类,继承ViewManager接口。提供addView updateViewLayout removeView方法。实现类为WindowManagerImpl

mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);

所以我们看一下WindowManagerImpl的相关操作方法:

@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);
}

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

可以发现WindowManagerImpl并没有直接实现Window的相关操作方法,而是通过WindowManagerGlobal类来处理的。WindowManagerrGlobaladdView主要有以下几个操作:

1.检测参数是否合法,如果是子Window则需要调整布局参数,不是则针对是否硬件加速做处理
if (view == null) {
    throw new IllegalArgumentException("view must not be null");
   }
if (display == null) {
   throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
   throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
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;
     }
}
2.观测系统属性变化
// 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);
}
3.判断添加的view是否是之前删除的view,如果是立即调用dodie方法删除
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.");
           }
     // The previous removeView() had not completed executing. Now it has.
    }
4.创建ViewRootImpl并将View添加到列表中

在WindowManagerGlobal中有几个重要的列表:

private final ArrayList<View> mViews = new ArrayList<View>();

private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();

private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

其中mViews存储的是所以Window对应的view,mRoots存储的是Window对应的ViewRootImpl,mParams存储的是Window对应的布局参数,mDyingViews存储的是正在被删除的View对象,也就是那些调用了removeView方法但是删除操作还没完成的Window对象。在addView方法中将Window一系列对象存储到列表中:

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
5.通过ViewRootImpl更新界面并完成Window的添加过程:
root.setView(view, wparams, panelParentView);

通过ViewRootImpl.setView方法来完成。在setView内部通过requestLayout来完成异步刷新请求。下面的代码中scheduleTraversals实际上是view绘制的入口:

public void requestLayout() {
  f (!mHandlingLayoutInLayoutRequest) {
       checkThread();
       mLayoutRequested = true;
       scheduleTraversals();
    }
}

然后通过WindowSession完成Window的添加过程。

mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
         getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
         mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
         mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
setFrame(mTmpFrame);

上面的代码中,mWindowSession类型是IWindowSession,它是一个binder对象,实现类是Session,也就是说Window添加过程其实是一次IPC的过程。
Session内部通过WindowManagerService来实现Window的添加代码如下:

@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets, Rect outOutsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }

这样一来,Window添加请求就交给了WindowManagerService处理了,至于WindowManagerService是如何处理的,本文先不做分析,有兴趣的朋友可以自行查看WindowManagerService源码,本文主要是分析Window添加的整体流程。

Window删除过程:

Window删除和Window添加一样都是通过WindowManagerImpl调用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);
        }
    }

代码逻辑很简单,从mViews数组中找到View的索引,然后调用removeViewLocked进行删除:

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

        if (view != null) {
            InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
            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);
            }
        }
    }

removeViewLocked这边的逻辑也非常明确,直接调用ViewRootImpl的die方法:

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;
    }

die方法中针对immediate字段进行同步异步处理,immediate为false,说明异步删除,发送消息MSG_DIE,ViewRootImpl内部Handler接受到消息后调用dodie方法,同步则直接调用dodie方法。其余并无差别。

    void doDie() {
        checkThread();
        if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                dispatchDetachedFromWindow();
            }
            ...
            //destroy相关回收操作

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

doDie方法中调用了dispatchDetachedFromWindow方法,然后进行一些资源回收操作,最后删除WindowManagerGlobal中列表相关的数据。下面看一下dispatchDetachedFromWindow方法:

void dispatchDetachedFromWindow() {
        mFirstInputStage.onDetachedFromWindow();
        if (mView != null && mView.mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
            //调用view的方法
            mView.dispatchDetachedFromWindow();
        }
        //...资源回收
        try {
            mWindowSession.remove(mWindow);
        } catch (RemoteException e) {
        }
        unscheduleTraversals();
    }

在dispatchDetachedFromWindow中主要有一些操作:

  • 1.调用View的dispatchDetachedFromWindow方法,这个方法内部会调用View的onDetachedFromWindow等方法,而onDetachedFromWindow大家肯定不会陌生,当view从window中移出是都会回调,里面通常做一些资源回收,动效回收的操作
  • 2.资源回收相关操作
  • 3.通过IPC调用Session的remove方法删除数据。

Window的更新过程:

与上面两个方法一样更新的方法也是通过WindowManagerImpl调用WindowManagerGlobal来实现的:

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

        view.setLayoutParams(wparams);

        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

WindowManagerGlobal的updateViewLayout方法会更新view的LayoutParams,然后替换列表中的LayoutParams,然后更新ViewRootImpl中的LayoutParams。这一步操作是通过ViewRootImpl.setLayoutParams方法来完成的。setLayoutParams方法会调用ViewRootImpl的scheduleTraversals方法来对view进行重新布局,包括测量布局和重绘。同时ViewRootImpl会通过Session来更新视图,这个操作也是一个IPC操作,最终的调用是WindowManagerServicerelayoutWindow方法。
到这里Window的三大操作整体流程就介绍完了。

Window创建过程:

通过上面的分析,View是Android中视图的呈现方式,但是View不能单独存在,它必须附着在Window这个抽象的概率上面,因此有视图的地方就有Window。那么那些地方有视图呢?
Android中可以提供视图的地方有Activity、Dialog、Toast和其他一些依托window实现的视图比如PopupWindow、菜单等。下面我们分析一下这些视图元素的简单调用:

Activity的Window创建过程:

Activity的setContentView方法:

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
}

getWindow返回mWindow成员变量

public Window getWindow() {
   return mWindow;
}

这边就需要引入Activity启动流程相关的知识,在Activity启动中会调用performLaunchActivity方法通过类加载的方式创建Activity实例,然后调用Activity.attach方法:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
        ...

        activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback,
                        r.assistToken);
         ...

        return activity;
    }

mWindow变量在Activity的attach方法中初始化:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mAssistToken = assistToken;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);

        setAutofillOptions(application.getAutofillOptions());
        setContentCaptureOptions(application.getContentCaptureOptions());
    }

在attach方法中,创建Activity所属的window对象,并设置回调接口。Activity实现了Window的callback接口,因此当Window接受到外界的状态改变时会回调Activity的方法,mWindow变量是PhoneWindow类型,所以我们来看一下PhoneWindow.setContentView方法:

public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
setContentView大致有如下步骤:
1.如果mContentParent为空,通过installDecor,完成Decorview的装载工作。其中mContentParent是Decorview承载的View

创建的方法在installDecor方法中:

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            //标题相关UI处理

            if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {
                mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);
            }

            // Only inflate or create a new TransitionManager if the caller hasn't
            // already set a custom one.
            ....动画相关...
        }
    }

installDecor中判断Decorview是否为空,为空则调用generateDecor(-1)创建。然后判断mContentParent是否为空,为空则generateLayout(mDecor)创建。

protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

generateDecor方法总只有主活动界面使用ApplicationContext,其余界面使用上下文context。然后返回DecorView实例。

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        //window主题样式设置并设置布局

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        ...

        mDecor.finishChanging();

        return contentParent;
    }

generateLayout中针对styleable设置不同的flags,然后依据flags设置不同的layout布局。最后通过DecorView的onResourcesLoaded方法完成布局渲染。然后返回com.android.internal.R.id.content的view,该View是窗体的内容view。这时候mContentParent就被设置成这个view了。

2.将View添加到PhoneWindow的mContentParent中,mContentParent是DecorView布局中的id为com.android.internal.R.id.content的View

回到PhoneWindow的setContentView方法中:

public void setContentView(int layoutResID) {
        ...上述初始化view方法...

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

setContentView后续代码中,判断是否有动画,有则完成动画,然后在Scene中将layoutResID对应的布局添加到mContentParent布局中。没有则直接通过下面代码完成布局添加的操作。

mLayoutInflater.inflate(layoutResID, mContentParent);
3.回调Activity的onContentChanged方法通知Activity视图完成改变。

Activity实现了Window.Callback接口,并在ActivityThread对Window完成绑定。此时的回调是告知Activity的布局文件已经被添加到DecorView中了,需要Activity做相应的处理。这是一个空方法,需要Activity自己处理这个回调。

经过上面的步骤,DecorView已经完成初始化,Activity的布局也被添加到DecorView中对应content的View中,但是这时候DecorView还没有被WindowManager正式添加到Window中。
Window更多的表示的是一种抽象的功能集合,虽然在Activity的attach方法中就已经被创建了,但是因为没有被WindowManager识别,所以这个Window无法提供具体的功能,因为它无法接受到外界的输入信息。在ActivityThreadhandleResumeActivity方法中,首先会调用ActivityonResume方法,然后会调用r.activity.makeVisible(),makeVisible方法控制这DecorView正在完成添加与显示的过程,到这边Activity才会被用户看到:

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

到这边Activity的分析已经完成了,下面我们做一下总结:


ActivitysetContentView.png

Dialog的创建过程

应用层代码
 AlertDialog.Builder builder = new AlertDialog.Builder(this);
 builder.setView(view);
 builder.setNegativeButton();
 builder.setPositiveButton();
 builder.setTitle()
 AlertDialog alertDialog = builder.create();
 alertDialog.show();

属性设置到AlertDialog.Builder中,builder.create时通过P.apply(dialog.mAlert)方法配置到AlertController上。调用alertDialog.show()方法:

 public AlertDialog create() {
    // Context has already been wrapped with the appropriate theme.
    final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
    P.apply(dialog.mAlert);
    ...
    return dialog;
 }

 public void show() {
     if (mShowing) {
         if (mDecor != null) {
             if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                 mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
             }
             mDecor.setVisibility(View.VISIBLE);
         }
         return;
     }
     mCanceled = false;

     if (!mCreated) {
         dispatchOnCreate(null);
     } else {
         // Fill the DecorView in on any configuration changes that
         // may have occured while it was removed from the WindowManager.
         final Configuration config = mContext.getResources().getConfiguration();
         mWindow.getDecorView().dispatchConfigurationChanged(config);
     }

     ...
     mWindowManager.addView(mDecor, l);
     ...
     }

在show方法中判断是否创建,如果未创建的话调用dispatchOnCreate方法:

void dispatchOnCreate(Bundle savedInstanceState) {
    if (!mCreated) {
        onCreate(savedInstanceState);
        mCreated = true;
    }
}

在这个方法中判断未创建调用onCreate方法,onCreate是个空方法,供子类调用,也就是AlertDialog,因此我们来看一下AlertDialog.onCreate:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mAlert.installContent();
}

在此方法中调用AlertController的installContent方法,在此方法中完成Dialog内部view的设置:

public void installContent() {
    int contentView = selectContentView();
    mWindow.setContentView(contentView);
    setupView();
}

可以看到最终还是通过mWindow.setContentView来设置的。在Dialog的构造方法中mWindow完成初始化:

Dialog构造方法:

  Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == Resources.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

和Activity一样Window是PhoneWindow类型。所以Dialog也是通过PhoneWindow来设置布局的。在show方法的后续代码中完成Window在WinddowManager的注册:

mWindowManager.addView(mDecor, l);

移出代码则是在dialog消失的时候调用下面代码完成:

mWindowManager.removeViewImmediate(mDecor);

Dialog与Activity展示基本上一直,只不过有个特别需要注意的就是普通的Dialog必须采用Activity的context,如果采用Application的context则报错。

Caused by: android.view.WindowManager$BadTokenException: 
Unable to add window -- token null is not valid; is your activity running?

这个问题是没有应用token导致的,而应用token一般只有Activity有,所以这里需要使用Activity的context。我们知道系统dialog是不需要token的,因此可以设置dialog为系统弹窗,这样就可以正常弹出了。之前讲过WindowManager.LayoutParams中的type表示window的类型,而系统window层级范围是2000-2999.因此我们需要将type改成系统层级,本例采用TYPE_APPLICATION_OVERLAY来指定对话框window类型为系统window。
当然要注意权限:

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Toast Window机制

Toast使用
Toast.makeText(this,"sss",Toast.LENGTH_LONG).show();

参数有context、提示内容、和显示时长。
Toast.LENGTH_LONG展示时间长 3.5s
Toast.LENGTH_SHORT展示时间短 2s

Toast Window创建过程

Toast与dialog不同,它工作过程相对比较复杂。Toast是基于Window实现的但是因为Toast具有定时取消功能,所以采用了Handler。在Toast内部有两种IPC过程,第一种是Toast访问NotificationManagerService,第二种是NotificationManagerService回调Toast里的TN接口。
Toast属性系统Window,内部View有两种方式指定,一种是系统自带,另一种是通过setView方式指定一个自定义View。但是不管如何它们都对应一个属性mNextView
Toast提供showcancel方法来控制Toast展示和隐藏。这两种操作内部都是一个IPC过程:

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();

    try {
        service.enqueueToast(pkg, tn, mDuration, displayId);
    } catch (RemoteException e) {
        // Empty
    }
}

通过IPC机制,调用NotificationManagerService的enqueueToast方法,参数为包名、TN、时长和屏幕ID。其中TN是远程Binder对象,在NotificationManagerService中控制Window展示与隐藏方法触发后会通过TN再次触发IPC操作,回调TN本地的展示隐藏方法,从而控制Window的添加与隐藏。下面我们来分析下enqueueToast方法:

public void enqueueToast(String pkg, ITransientNotification callback, int duration,
                int displayId)
    {
        ...参数判断..

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
                // If it's already in the queue, we update it in place, we don't
                // move it to the end of the queue.
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
                             final ToastRecord r = mToastQueue.get(i);
                             if (r.pkg.equals(pkg)) {
                                 count++;
                                 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                     Slog.e(TAG, "Package has already posted " + count
                                            + " toasts. Not showing more. Package=" + pkg);
                                     return;
                                 }
                             }
                        }
                    }

                    Binder token = new Binder();
                    mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);
                    record = new ToastRecord(callingPid, pkg, callback, duration, token,
                            displayId);
                    mToastQueue.add(record);
                    index = mToastQueue.size() - 1;
                    keepProcessAliveIfNeededLocked(callingPid);
                }
                // If it's at index 0, it's the current toast.  It doesn't matter if it's
                // new or just been updated.  Call back and tell it to show itself.
                // If the callback fails, this will remove it from the list, so don't
                // assume that it's valid after this.
                if (index == 0) {
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }
enqueueToast分为以下几个步骤:
  • 1.参数判断
  • 2.判断消息是否已经在Toast列表中,存在则更新。不存在判断是否达到上限没有达到则加入Toast列表中
  • 3.showNextToastLocked展示Toast

我们接着看showNextToastLocked方法:

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            record.callback.show(record.token);
            scheduleDurationReachedLocked(record);
            return;
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to show notification " + record.callback
                    + " in package " + record.pkg);
            // remove it from the list and let the process die
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveIfNeededLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

showNextToastLocked方法获取Toast列表中第一位展示。展示是通过ToastRecord的callback完成的,这个callback就是TN对象的远程Binder,通过跨进程方式来完成TN方法的调用,最终被调用的TN方法运行在发现Toast请求的引用的Toast的Binder线程池中。因此就需要Handler来将其切换到当前线程中。所以意味着Toast不能在没有Looper的线程中弹出。弹窗展示之后会设置一个延时取消的消息,其他的延时时长取决于Toast展示时长。

private void scheduleDurationReachedLocked(ToastRecord r)
  {
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    // Accessibility users may need longer timeout duration. This api compares original delay
    // with user's preference and return longer one. It returns original delay if there's no
    // preference.
    delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
            AccessibilityManager.FLAG_CONTENT_TEXT);
    mHandler.sendMessageDelayed(m, delay);
  }

在Handler中调用handleDurationReached方法处理延时消息:

public void handleMessage(Message msg)
    {
        switch (msg.what)
        {
            case MESSAGE_DURATION_REACHED:
                handleDurationReached((ToastRecord) msg.obj);
                break;
                ....
         }

    }

handleDurationReached判断当前Toast是否在列表中,如果是的话则调用cancelToastLocked方法:

private void handleDurationReached(ToastRecord record)
{
    if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.callback);
        if (index >= 0) {
            cancelToastLocked(index);
        }
    }
}

在cancelToastLocked中会调用record.callback.hide方法,从而通过IPC机制调用TN中hide方法,同时将Toast从列表中删除,然后判断当前列表是否为空,不过不为空的话调用showNextToastLocked方法重复之前的展示取消逻辑。

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }

        ToastRecord lastToast = mToastQueue.remove(index);

        mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,
                lastToast.displayId);
        // We passed 'false' for 'removeWindows' so that the client has time to stop
        // rendering (as hide above is a one-way message), otherwise we could crash
        // a client which was actively using a surface made from the token. However
        // we need to schedule a timeout to make sure the token is eventually killed
        // one way or another.
        scheduleKillTokenTimeout(lastToast);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }
    }

在TN中show与hide方法,是通过Handler将线程切换到当前线程:

public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

/**
 * schedule handleHide into the right thread
 */
@Override
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
    mHandler.obtainMessage(HIDE).sendToTarget();
}

在Handler中分别调用方法handleShow与handleHide处理Window的添加与移出工作:

public void handleShow(IBinder windowToken) {
    ...
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    ....
    try {
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    } catch (WindowManager.BadTokenException e) {
        /* ignore */
    }
}

public void handleHide() {
    if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
    if (mView != null) {
        // note: checking parent() just to make sure the view has
        // been added...  i have seen cases where we get here when
        // the view isn't yet added, so let's try not to crash.
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeViewImmediate(mView);
        }

        // Now that we've removed the view it's safe for the server to release
        // the resources.
        try {
            getService().finishToken(mPackageName, this);
        } catch (RemoteException e) {
        }

        mView = null;
    }
}

到这里Toast的工作机制分析完成了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容