写在最前:本文涉及到源码的部分,查看的是 Android 8.1.0_r33 的源码,部分与原文中代码有出入。

附上查看源码的网址:http://androidxref.com/


学习内容:

  • Window 和 WindowManager
  • Window 的内部工作原理
    • Window 的添加、更新和删除
  • Actvitiy、Dialog 等类型的 Window 对象的创建过程

原文开篇部分:

  • Window 是一个抽象类,具体实现是 PhoneWindow
  • WindowManager 是外界访问 Window 的入口,Window 的实现位于 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一个 IPC 过程
  • Window 实际是 View 的管理者,视图都是通过 Window 呈现的。

1.Window 和 WindowManger

通过 WindowManager 添加一个 Window

mFloatingButton = new Button(this);mFloatintButton.setText("button");mLayoutParams = new WindowManager.LayoutParams(LayoutParams.RTAP_CONTENT,LayoutParams.WRAP_CONTENT,0,0,PixelFormat.TRANSPARENT);mLayoutParams.flags = KatiytOarams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_SHOW_WHEN_LOCKED;mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;mLayoutParams.x = 100;mLayoutParams.y = 300;mWindowManager.addView(mFloatingButton, mLayoutParams);

通过以上代码,可以将一个 Button 添加到坐标为(100,300)的位置上。

下面说明 WindowManager.LayoutParams 中的 flagstype 这两个参数:

  1. flags 参数表示 Window 的属性,下面列出一个常用的选项:

    1. FLAG_NOT_FOCUSABLE
      表示 WIndow 不需要获取焦点,也不需要接收各种输入事件,最终事件会传递给下层的具有焦点的 Window

    2. FLAG_NOT_TOUCH_MODAL
      系统会将当前 Window 区域以外的单击事件传递给底层的 Window,当前 Window 区域以内的事件则自己处理。一般开启,否则其他 Window 将无法收到单击事件

    3. FLAG_SHOW_WHEN_LOCKED
      开启此模式可以让 Window 显示在锁屏界面

  2. Type 参数表示 Window 的类型,Window 有三种类型:

    1. 应用 Window:对应一个 Activity;层级范围是 1 ~ 99。
    2. 子 Window:不能单独存在,需要附属再特定的父 Window 上,比如常见的 Dialog;层级范围是 1000 ~ 1999
    3. 系统 Window:需要声明权限才能创建的 Window,比如 Toast;层级范围是 2000 ~ 2999

    关于上面提到的层级范围,此处进行说明:Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖再层级小的 Window 的上面。

WindowManager 提供的功能

提供的功能很简单,一般只有三个方法:添加 View、更新 View 和删除 View,这三个方法定义在接口 ViewManager 中(WindowManager 继承了 ViewManager)

public interface ViewManager{    public void addView(View view, ViewGroup.LayoutParams params);    public void updateViewLayout(View view, ViewGroup.LayoutParams params);    public void removeView(View view);}//继承关系public interface WindowManager extends ViewManager {    //...}

原文中说道,”WindowManager 操作 Window 的过程更像是在操作 Window 中的 View“。


2. Window 的内部机制

Window 是一个抽象概念,每一个 Window都对应着一个 View 和一个 ViewRootImpl,WIndow 和 View 通过 ViewRootImpl 建立联系,因此 Window 以 View 的形式存在,View 才是 Window 存在的实体

实际开发中,对 Window 的访问必须通过 WindowManager。

2.1 Window 的添加过程

添加过程通过 WindowManager 的 addView 来实现,具体实现类是 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);    }

可以看到,实际上 WindowManagerImp 采用了桥接模式,将具体的实现 委托 给了 mGlobal(WindowManagerGlobal)来处理,WindowManagerGlobal 以工厂的形式向外提供自己的实例。

WindowManagerGlobal 的 addView 方法有如下几步:

  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);}
  2. 创建 ViewRootImpl 并将 View 添加到列表中

    root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);

    关于上面代码中的 mViews,mRoots 等:

    //存储所有 Window 所对应的 Viewprivate final ArrayList mViews = new ArrayList();//存储所有 Window 所对应的 ViewRootImplprivate final ArrayList mRoots = new ArrayList();//存储所有 Window 中对应的布局参数private final ArrayList mParams = new ArrayList();//存储正在被删除的 View 对象,或者说已经调用 removeView 方法但是删除操作尚未完成 Window 对象private final ArraySet mDyingViews = new ArraySet();
  3. 通过 ViewRootImpl 来更新界面并完成 Window 的添加过程

    这个步骤通过 ViewRootImpl 的 setView 方法完成:

    //WindowManagerGlobal.addView() 方法内部root.setView(view, wparams, panelParentView);//ViewRootImpl.setView(...) 方法内部//...requestLayout();//...//ViewRootImpl@Overridepublic void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {    checkThread();    mLayoutRequested = true;    scheduleTraversals();}}

    可以看到,通过 requestLayout 发起异步刷新请求,而其中的 scheduleTraversals 实际上是 View 绘制的入口。

    接着会通过 WindowSession 最终来完成 Window 的添加过程。

    //ViewRootImpl.setView(..)方法内部try {mOrigWindowType = mWindowAttributes.type;mAttachInfo.mRecomputeGlobalAttributes = true;collectViewAttributes();res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, 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);}

    上面代码中,mWindowSession 类型是 IWindowSession,是一个 Binder 对象,真正的实现类是 Session,也就是说 WIndow 的添加过程是一个 IPC 调用。

    在 Session 内部会通过 WindowManagerService 实现 Window 的添加:

    @Overridepublic int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) {return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel);}

    如此,Window 的添加请求就交给 WindowManagerService 去处理了,在 WindowManagerService 中会为每一个应用保留一个单独的 Session,具体不再分析。
    (原文分析到此,认为到此添加流程已经很清晰,再深入 WindowManagerSercice 也只是一系列代码细节)

2.2 Window 的删除过程

Window 的删除过程和添加过程初始阶段类似,直接分析 WindowManagerGlobal 的 removeView 方法:

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

逻辑很清晰,首先通过 findViewLocked 方法查找待删除的 View 的索引,然后调用 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);        }    }}

removeViewLocked 是通过 ViewRootImpl 来完成删除操作的,调用了其 die 方法。

上面代码中的 immediate 参数需要注意,该参数对应了 WindowManager 中的两种删除接口 removeView(immediate 为 false) 和 removeViewImmediate(immediate 为 true),分别表示异步删除和同步删除。

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 方法内部做了简单判断:

  • 如果是同步删除,那么直接调用 doDie 方法;
  • 如果是异步删除,则会发送一个 MSG_DIE 的请求删除消息,ViewRootImpl 中的 Handler 会处理此消息并调用 doDie 方法;在此过程中,由于 View 尚未完成删除操作,因此在上面 removeViewLocked 方法末尾部分代码中,会将其添加到 mDyingViews 中。

在 doDie 方法内部会调用 dispatchDetachedFromWindow 方法,在其中实现真正删除 View 的逻辑,该方法中主要做 四件事:

  1. 垃圾回收相关的工作,比如清楚数据和消息、移除回调
  2. 通过 Session 的 remove 方法删除Window,同样是一个 IPC 过程,最终会调用 WindowManagerService 的 removeView 方法
  3. 调用 View 的 dispatchDetachedFromWindow 方法,内部会调用 View 的 onDetachedFromWindow(View 从 Window 中移除时的回调,做终止动画、停止线程等一系列资源回收工作) 以及 onDetachedFromWindowInternal。
  4. 调用 WindowManagerGloabl 的 doRemoveView 方法刷新数据,包括 mRoots、mParams 以及 mDyingViews,需要将当前 Window 关联的这三类对象从列表中删除。

2.3 Window 的更新过程

直接看 WindowManagerGlobal 的 updateViewLayout 方法:

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

这个过程就比较简单:

  • 首先更新 View 的 LayoutParams 并替换旧的 LayoutParams
  • 更新 ViewRootImpl 的 LayoutParams
  • ViewRootImpl 会通过 schedulTraversals 方法对 View 重新布局,包括 测量、布局、重绘三个过程
  • ViewRootImpl 还会通过 WindowSession 更新 Window 的视图,这个过程最终由 WindowManagerService 的 relayoutWindow 具体实现,同样是一个 IPC 过程

3.Window 的创建过程

3.1 Activity 的 Window 的创建过程

首先应该了解 Activity 的启动过程,这一部分留待第九章再说。

简单来说,最终会由 ActivityThread 的 performLaunchActivity 来完成整个启动过程,此方法中会通过类加载器创建 Activity 的实例对象,并调用 attach 方法为其关联运行过程中所依赖的一系列上下文环境对象。

在 Activity 的 attach 方法里,系统会创建 Activity 所属的 Window 对象并为其设置回调接口,Window 的对象创建由 PolicyManager 的 makeNewWindow 方法实现。同时当 Window 接收到外界的状态改变时就会通过 Callback 接口,回调 Activity 的方法。

关于 Window 的创建:

  • Activity 的 Window 通过 PolicyManager 的工厂方法创建,PolicyManager 是一个契约类,实现了 IPolicy 策略接口,该接口中定义了众多工厂方法。
  • PolicyManager 的真正实现是 Policy 类,在 makeNewWindow 方法中,返回了 PhoneWindow 对象,由此得出 Window 的具体实现是 PhoneWindow。

关于 Activity 的视图如何附属到 Window:

Activity 的 setContentView 方法交由 Window 处理,直接分析 PhoneWindow 的 setContentView 即可

PhoneWindow 的 setContentView 方法遵循以下几个步骤

  1. 如果没有 DecorView,那么创建之。

    1. 通过 installDecor 方法内部的 generateDecor 直接创建 DecorView
    2. 通过 generateLayout 方法加载具体的布局文件到 DecorView
  2. 将 View 添加到 DecorView 的 mContentParent 中

    到此步,Activity 的布局文件已经添加到 DecorView 里面。

  3. 回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
    Activity 的 onContentChanged 方法是个空实现,可以在 子Activity 中处理该回调。

但是!!

经过上面的三个步骤,DecorView 尚未被 WindowManager 正式添加到 Window。由于此时 DecorView 未被 WindowManager 识别,所以这个时候的 Window 无法提供具体功能,因为它还无法接收外界的输入信息。

在 ActivityThread 的 handleResumeActivity 方法中,首先调用 Activity 的 onResume 方法,接着调用 Activity 的 makeVisible(),在 makeVisible 方法中,DecorView 真正完成添加和显式这两个过程,此时 Activity 的视图才能被用户看到,如下所示:

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

3.2 Dialog 的 Window 的创建过程

Dialog 的 Window 创建过程和 Activity 类似,有如下几个步骤:

1. 创建 Window
2. 初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
3. 将DecorView 添加到 Window 中显示

​ 在 Dialog 的 show 方法中,通过 WindowManager 的 addView 方法将 DecorView 添加到 Window 中。

Dialog 的 Window 创建和 Activity 的 Window 创建过程很类似,几乎没有什么区别;当 Dialog 被关闭时,会通过 WindowManager 的 removeViewImmediate(mDecor) 移除 DecorView。

需要注意

  • 普通 Dialog 必须采用 Activity 的 Context,如果采用 Application 的 Context 会报错,因为需要 应用token,而只有 Activity 拥有
  • 系统 Window 比较特殊,不需要 token,所以也可以采用 Application 的 Context 的同时,指定对话框的 Window 为系统类型即可正常弹出对话框

3.3 Toast 的 Window 的创建过程

首先 Toast 也是基于 Window 实现的,但是由于 Toast 具有定时取消这一功能,所以系统采用了 Handler。

Toast 内部有两类 IPC 过程:

  1. Toast 访问 NotificationManagerService(NMS)
  2. NMS 回调 Toast 里的 TN 接口

Toast 提供 show 和 cancel 分别用于显示和隐藏 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;    try {        service.enqueueToast(pkg, tn, mDuration);    } catch (RemoteException e) {        // Empty    }}public void cancel() {    mTN.cancel();}

显示和隐藏都需要 NMS 实现,而 NMS 无法运行在系统的进程中,所以需要通过远程调用的方式。

关于 TN 这个类,它是一个 Binder 类,在 Toast 和 NMS 进行 IPC 的过程中,当 NMS 处理 Toast 的显示或隐藏请求时会跨进程回调 TN 中的方法,此时由于 TN 运行在 Binder 线程池中,所以需要通过 Handler 将其切换到当前线程中。同时这也意味着 Toast 无法在没有 Looper 的线程中弹出。

首先分析 Toast 的显示过程( show 方法)

  • 调用了 NMS 中的 enqueueToast 方法
  • enqueueToast 首先将 Toast 请求封装为 ToastRecord 对象并将其添加到 mToastQueue 队列中。

    mToastQueue 是一个 ArrayList,对非系统应用而言,mToastQueue 最多同时存在 50 个 ToastRecord,此举是为了防止 DOS 拒绝服务攻击,避免其他应用没有机会弹出 Toast

  • 添加后,NMS 通过 showNextToastLocked 方法显示当前 Toast。

    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);        scheduleTimeoutLocked(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;          }    }}}

    上面的代码很简单,Toast 的显示通过 ToastRecord 的 callback 来完成,这个callback 实际上就是 Toast 的 TN 对象的远程 Binder,通过 callback 来访问 TN 中的方法是需要跨进程来完成的,最终被调用的 TN 中的方法会运行在发起 Toast 请求的应用的 Binder 线程池中。

  • Toast 显示之后,NMS 会通过 scheduleTimeoutLocked 方法发送延时消息,具体延时取决于 Toast 的时长:LONG_DELAY 是 3.5s,SHORT_DELAY 是 2s

  • private void scheduleTimeoutLocked(ToastRecord r){mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;mHandler.sendMessageDelayed(m, delay);}

    延时时间过后,NMS 通过 cancelToastLocked 方法隐藏 Toast 并将其从 mToastQueue 中移除,此时如果 mToastQueue 中还有其他 Toast,那么 NMS 就继续显示其他 Toast。

    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, true, DEFAULT_DISPLAY);  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();}   }

再分析 Toast 的隐藏过程(cancel 方法)

  • 同样是通过 ToastRecord 的 callback 完成的,同样是一次 IPC 过程,其工作过程和 Toast 的显示过程是类似的

    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}

再看 TN 类

通过上面的分析,Toast 的显示和隐藏实际上是通过 Toast 的 TN 这个类来实现的,它有两个方法 show 和 hide,分别对应 Toast 的显示和隐藏。这两个方法运行在 Binder 线程池中,内部使用了 Handler。

/*** schedule handleShow into the right thread*/@Overridepublic void show(IBinder windowToken) {    if (localLOGV) Log.v(TAG, "SHOW: " + this);    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}/*** schedule handleHide into the right thread*/@Overridepublic void hide() {    if (localLOGV) Log.v(TAG, "HIDE: " + this);    mHandler.obtainMessage(HIDE).sendToTarget();}

上面代码中,分别发送了 SHOW 和 HIDE 两种消息,在 handleMessage 中分别调用 handleShow 和 handleHide 方法,这二者才是真正完成显示和隐藏 Toast 的地方。

  • handleShow 中 addView 方法将 Toast 的视图添加到 Window 中
  • handleHide 中调用 remoteView 方法将 Toast 的视图从 Window 中移除

到这里 Toast 地 Window 的创建过程就分析完了。


本章结束。

更多相关文章

  1. Android(安卓)中Dialog点击空白处會消失问题
  2. ANDROID 后台服务 service
  3. Android(安卓)Studio中获取sha1证书指纹数据的方法
  4. Android中Intent传递类对象的两种方式
  5. Android中Alarm的机制
  6. 获取调试版SHA1和发布版SHA1的方法
  7. android默认焦点设置的方法
  8. android动态加载Jar/dex--原创
  9. 你需要了解下Android(安卓)View的更新 requestLayout 与重绘 inv

随机推荐

  1. 写在20110626:NDK、JNI
  2. Android(安卓)Toast 长期显示解决方案
  3. Android(安卓)怎么样使用shape
  4. Android中的adapter
  5. Android应用程序组件概述
  6. SqlServer 复制中将大事务分成小事务分发
  7. 数据库SQL中having和where的用法区别
  8. sql中 order by 和 group by的区别
  9. CentOS安装SQL Server vNext CTP1教程
  10. SQL SERVER中强制类型转换cast和convert