Android | 理解Window 和 WindowManager(下)

简介: Android | 理解Window 和 WindowManager(下)

Activity 的 Window 创建过程


Activity 的创建过程比较复杂,最终会通过 ActivityThread 中的 performLaunchActivity() 来完成整个启动过程,在这个方法中会通过类加载器创建 Activity 的实例对象,并调用其 attach 方法为其关联所需的环境变量。


##Activity.attach
  //创建 PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
  //设置 window 的回调
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
        mWindow.setSoftInputMode(info.softInputMode);
    }


通过上面代码可以知道在 attach 方法中,创建了 Window,并设置了 callback。


由于 Activity 的视图是通过 setContentView 方法提供的,我们直接看 setContentView 即可:


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


##PhoneWindow
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        // 1,创建 DecorView
        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 {
        //2 添加 activity 的布局文件
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        //3
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}


在上面代码中,如果没有 DecorView 就创建它,一般来说它内部包含了标题栏和内容栏,但是这个会随着主题的改变而发生改变。但是不管怎么样,内容栏是一定存在的,并且内容栏有固定的 id content,完整的 id 是 android.R.id.content 。


注释1::通过 generateDecor 创建了 DecorView,接着会调用 generateLayout 来加载具体的布局文件到 DecorView 中,这个要加载的布局就和系统版本以及定义的主题有关了。加载完之后就会将内容区域的 View 返回出来,也就是 mContentParent


注释2:将 activity 需要显示的布局添加到 mcontentParent 中。


注释3:由于 activity 实现了 window 的callback 接口,这里表示 activity 的布局文件已经被添加到 decorView 的 mParentView 中了,于是通知 onContentChanged 。


经过上面三个步骤,DecorView 已经初始完成,Activity 的布局文件以及加载到了 DecorView 的 mParentView 中了,但是这个时候 DecorView 还没有被 WindowManager 正式添加到 Window 中。


在 ActivityThread 的 handlerResumeActivity 中,会调用 activity 的 onResume 方法,接着就会将 DecorView 添加到 Window 中


@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
  //..... 
    //调用 activity 的 onResume 方法
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    final Activity a = r.activity;
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        if (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                //DecorView 完成了添加和显示的过程
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
    } else if (!willBeVisible) {
        if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
        r.hideForNow = true;
    }
    //..........
}


Dialog 的 Window 创建过程


创建 Window


Dialog 中创建 Window 是在其构造方法中完成,具体如下:


Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
        boolean createContextThemeWrapper) {
    //...
    //获取 WindowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  //创建 Window
    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    //设置 Callback
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}


初始化 DecorView,将 dialog 的视图添加到 DecorView 中


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


这个和 activity 的类似,都是通过 Window 去添加指定的布局文件


将 DecorView 添加到 Window 中显示


public void show() {
  //...
    mDecor = mWindow.getDecorView();
    mWindowManager.addView(mDecor, l);
  //发送回调消息
    sendShowMessage();
}


从上面三个步骤可以发现,Dialog 的 Window 创建和 Activity 的 Window 创建很类似,二者几乎没有什么区别。


当 dialog 关闭时,它会通过 WindowManager来移除 DecorView, mWindowManager.removeViewImmediate(mDecor) 。


普通的 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 即可。


另外,系统 Window 比较特殊,他可以不需要 Token,我们可以将 Dialog 的 Window Type 修改为系统类型就可以了,如下所示:


val dialog = Dialog(application)
    dialog.setContentView(textView)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
    }else{
        dialog.window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
    }
dialog.show()


需要注意的是需要申请悬浮窗权限


Toast 的 Window 创建过程


Toast 也是基于 Window 来实现的,但是他的工作过程有些复杂。在 Toast 的内部有两类 IPC 的过程,第一类是 Toast 访问 NotificationManagerService 过程。第二类是 NotificationManagerServer 回调 Toast 里的 TN 接口。下面将 NotificationManagerService 简称为 NMS。


Toast 属于系统 Window,内部视图有两种定义方式,一种是系统默认的,另一种是通过 setView 方法来指定一个 View(setView 方法在 android 11 以后已经废弃了,不会再展示自定义视图),他们都对应 Toast 的一个内部成员 mNextView。


Toast.show()


Toast 提供了 show 和 cancel 分别用于显示和隐藏 Toast,它们的内部是一个 IPC 的过程,实现如下:


public void show() {
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();
    try {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            if (mNextView != null) {
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            } else {
                // ...
            }
        }
    } 
    //....
}
public void cancel() {
    try {
        getService().cancelToast(mContext.getOpPackageName(), mToken);
    } catch (RemoteException e) {
        // Empty
    }
    //....
}
static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(
                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
        return sService;
    }


从上面代码中可以看出,显示和影藏都需要通过 NMS 来实现,由于 NMS 运行在系统进程中,所以只通过能跨进程的调用方式来显示和隐藏 Toast。


首先看 Toast 显示的过程,它调用了 NMS 中的 enqueueToast 方法,上面的 INotificationManager 只是一个 AIDL 接口, 这个接口使用来和 NMS 进行通信的,对 IPC 通信过程不太清楚的同学可以看一下这篇文章。


NotificationManagerService.enqueueToast


我们来看一下 NMS 的 enqueueToast 方法,这个方法中已经属于别的进程了。调用的时候传了 五个参数,第一个表示当前应用的包名,第二个 token,第三个 tn 表示远程回调,也是一个 IPC 的过程,第四个 时长,第五个是显示的 id


public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
        int duration, int displayId) {
    enqueueToast(pkg, token, null, callback, duration, displayId, null);
}
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        final long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, token);
    //如果队列中有,就更新它,而不是重新排在末尾
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                int count = 0;
                final int N = mToastQueue.size();
                for (int i = 0; i < N; i++) {
                    final ToastRecord r = mToastQueue.get(i);
                    //对于同一个应用,taost 不能超过 50 个
                    if (r.pkg.equals(pkg)) {
                        count++;
                        if (count >= MAX_PACKAGE_TOASTS) {
                            Slog.e(TAG, "Package has already queued " + count
                                   + " toasts. Not showing more. Package=" + pkg);
                            return;
                        }
                    }
                }
                //创建对应的 ToastRecord
                record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,text, callback, duration, windowToken, displayId, textCallback);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveForToastIfNeededLocked(callingPid);
            }
            // ==0 表示只有一个 toast了,直接显示,否则就是还有toast,真在进行显示
            if (index == 0) {
                showNextToastLocked(false);
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}
    private ToastRecord getToastRecord(int uid, int pid, String packageName, boolean isSystemToast,
            IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback,
            int duration, Binder windowToken, int displayId,
            @Nullable ITransientNotificationCallback textCallback) {
        if (callback == null) {
            return new TextToastRecord(this, mStatusBar, uid, pid, packageName,
                    isSystemToast, token, text, duration, windowToken, displayId, textCallback);
        } else {
            return new CustomToastRecord(this, uid, pid, packageName,
                    isSystemToast, token, callback, duration, windowToken, displayId);
        }
    }


上面代码中对给定应用的 toast 数量进行判断,如果超过 50 条,就直接退出,这是为了防止 DOS ,如果某个应用一直循环弹出 taost 就会导致其他应用无法弹出,这显然是不合理的。


判断完成之后,就会创建 ToastRecord,它分为两种,一种是 TextToastRecord ,还有一种是 CustomToastRecord 。由于调用 enqueueToast 的时候传入了 Tn,所以 getToastRecord 返回的是 CustomToastRecord 对象。


最后判断只有一个 toast ,就调用 showNextToastLocked 显示,否则就是还有好多个 taost 真在显示。


接着我们看一下 showNextToastLocked


void showNextToastLocked(boolean lastToastWasTextRecord) {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
  //...
        if (tryShowToast(
                record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
            scheduleDurationReachedLocked(record, lastToastWasTextRecord);
            mIsCurrentToastShown = true;
            if (rateLimitingEnabled && !isPackageInForeground) {
                mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
            }
            return;
        }
        int index = mToastQueue.indexOf(record);
        if (index >= 0) {
            mToastQueue.remove(index);
        }
        //是否还有剩余的taost需要显示
        record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    }
}
    private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
            boolean isWithinQuota, boolean isPackageInForeground) {
  //.....
        return record.show();
    }


上面代码中最后调用的是 record.show() 这个 record 也就是 CustomToastRecord 了。


接着我们来看一下他的 show 方法:


@Override
public boolean show() {
    if (DBG) {
        Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
    }
    try {
        callback.show(windowToken);
        return true;
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "+ pkg);
        mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
        return false;
    }
}


可以看到,调用的是 callback 的 show 方法,这个 callback 就是在 CustomToastRecord 创建的时候传入的 Tn 了。这里回调到了 Tn 的 show 方法中。


Toast# Tn.show


我们接着看:


TN(Context context, String packageName, Binder token, List<Callback> callbacks,
   @Nullable Looper looper) {
    mPresenter = new ToastPresenter(context, accessibilityManager, getService(),packageName);
    mHandler = new Handler(looper, null) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW: {
                    IBinder token = (IBinder) msg.obj;
                    handleShow(token);
                    break;
                }
             //.....
            }
        }
    };
}
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
public void handleShow(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                         + " mNextView=" + mNextView);
    //...
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
        mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                        mHorizontalMargin, mVerticalMargin,
                        new CallbackBinder(getCallbacks(), mHandler));
    }
}


由于 show 方法是被 NMS 夸进程的方式调用的,所以他们运行在 Binder 线程池中,为了切换到 Toast 请求所在的线程,这里使用了 Handler。通过上面代码,我们可以看出,最终是交给 ToastPresenter 去处理了


ToastPerenter.show


public class ToastPresenter {
//....
    @VisibleForTesting
    public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;
    /**
     * Returns the default text toast view for message {@code text}.
     */
    public static View getTextToastView(Context context, CharSequence text) {
        View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
        TextView textView = view.findViewById(com.android.internal.R.id.message);
        textView.setText(text);
        return view;
    }
    //....
    public ToastPresenter(Context context, IAccessibilityManager accessibilityManager, INotificationManager notificationManager, String packageName) {
        mContext = context;
        mResources = context.getResources();
        //获取 WindowManager
        mWindowManager = context.getSystemService(WindowManager.class);
        mNotificationManager = notificationManager;
        mPackageName = packageName;
        mAccessibilityManager = accessibilityManager;
        //创建参数
        mParams = createLayoutParams();
    }
    private WindowManager.LayoutParams createLayoutParams() {
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = R.style.Animation_Toast;
        params.type = WindowManager.LayoutParams.TYPE_TOAST; //TYPE_TOAST:2005
        params.setFitInsetsIgnoringVisibility(true);
        params.setTitle(WINDOW_TITLE);
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        setShowForAllUsersIfApplicable(params, mPackageName);
        return params;
    }
     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback) {
        show(view, token, windowToken, duration, gravity, xOffset, yOffset, horizontalMargin,
                verticalMargin, callback, false /* removeWindowAnimations */);
    }
     public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,int xOffset, int yOffset, float horizontalMargin, float verticalMargin,@Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
  //.....
        addToastView();
        trySendAccessibilityEvent(mView, mPackageName);
        if (callback != null) {
            try {
                //回调
                callback.onToastShown();
            } catch (RemoteException e) {
                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
            }
        }
    }
    private void addToastView() {
        if (mView.getParent() != null) {
            mWindowManager.removeView(mView);
        }
        try {
            // 将 Toast 视图添加到 Window 中
            mWindowManager.addView(mView, mParams);
        }
    }
}


总结一下


通过上面的研究后,突然间发现弹一个 Toast 还是挺麻烦的。主要的就是内部的 IPC 比较绕。


至于说为什么要进行 IPC ,主要就是为了统一管理系统中所有 Toast 的消失与显示,真正显示和消失操作还是在 App 中完成的。


Toast 的窗口类型是 TYPE_TOAST,属于系统类型,Toast 有自己的 token,不受 Activity 控制。


Toast 通过 WindowManager 将 view 直接添加到了 Window 中,并没有创建 PhoneWindow 和 DecorView,这点和 Activity 与 Dialog 不同。


最后总结了 Toast 显示的调用流程图,可参考一下:


0a2653c851af460fa595bd959398a8f1.png


最后总结一下

每一个 Window 都对应着一个 View 和 一个 ViewRootImpl 。Window 表示一个窗口的概念,也是一个抽象的概念,它并不是实际存在的,它是以 View 的方式存在的。


WindowManager 是我们访问 Window 的入口,Window 的具体实现位于 WindowManagerService 中。WindowManager 和 WindowManagerService 交互是一个 IPC 的过程,最终的 IPC 是在 RootViewImpl 中完成的。


相关文章
|
2月前
|
Android开发 UED 开发者
Android经典实战之WindowManager和创建系统悬浮窗
本文详细介绍了Android系统服务`WindowManager`,包括其主要功能和工作原理,并提供了创建系统悬浮窗的完整步骤。通过示例代码,展示了如何添加权限、请求权限、实现悬浮窗口及最佳实践,帮助开发者轻松掌握悬浮窗开发技巧。
311 1
|
2月前
|
API Android开发 数据安全/隐私保护
Android经典实战之窗口和WindowManager
本文介绍了Android开发中“窗口”的基本概念及其重要性。窗口是承载用户界面的基础单位,而`WindowManager`系统服务则负责窗口的创建、更新和移除等操作。了解这些概念有助于开发复杂且用户体验良好的应用。
57 2
|
4月前
|
消息中间件 前端开发 Android开发
Android面试题自定义View之Window、ViewRootImpl和View的三大流程
Android开发中,View的三大核心流程包括measure(测量)、layout(布局)和draw(绘制)。MeasureSpec类在测量过程中起到关键作用,它结合尺寸大小和模式(EXACTLY、AT_MOST、UNSPECIFIED)来指定View应如何测量。onMeasure方法用于自定义View的测量,布局阶段,ViewGroup调用onLayout确定子元素位置,而draw阶段按照特定顺序绘制背景、内容、子元素和装饰。整个流程始于ViewRootImpl的performTraversals,该方法触发测量、布局和绘制。
117 0
|
5月前
|
Android开发
Android WindowManager工具类
Android WindowManager工具类
44 0
|
XML 存储 设计模式
Android Framework知识整理:WindowManager体系(上)
本篇是Android framework的第一讲《WindowManager体系-上》,重在讲解Window在被添加到WindowManagerService前的流程。
|
API uml Android开发
Android | 深入理解View.post()获取宽高、Window加载View原理
深入理解View.post()获取宽高、Window加载View原理
430 0
|
存储 Android开发 索引
Android | 理解Window 和 WindowManager(上)
Android | 理解Window 和 WindowManager(上)
Android | 理解Window 和 WindowManager(上)
|
Android开发
Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?
Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?
162 0
Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?
|
存储 消息中间件 API
下沉式通知的一种实现 | Android悬浮窗Window应用
当你浏览公众号时来了一条新消息,通知在屏幕顶部会以自顶向下动画的形式入场,而且它是跨界面的全局浮窗(效果如下图)。虽然上一篇中抽象的浮窗工具类已经能实现这个需求。但本文在此基础上再封装一些更加友好的
370 0
下沉式通知的一种实现 | Android悬浮窗Window应用
|
存储 Android开发 Kotlin
悬浮窗的一种实现 | Android悬浮窗Window应用
本文以业务应用为出发点,从零开始抽象一个浮窗工具类,它用于在任意业务界面上展示悬浮窗。它可以同时管理多个浮窗,而且浮窗可以响应触摸事件,可拖拽,有贴边动画。
830 0
悬浮窗的一种实现 | Android悬浮窗Window应用
下一篇
无影云桌面