Window的创建

前言

上篇文章中讲到, Android中所有视图都是通过Window来呈现的, 如Activity, Dialog, Toast等, 本篇文章分别分析下Activity, Dialog, Toast中Window的创建过程.

Activity中Window的创建

通过startActivity启动Activity, 从该入口追溯源码到ActivityThread的performLaunchActivity(), 这个方法会创建Activity对象, 并调用其attach方法, 下面看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) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);
        //创建Window
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ......
        //设置WindowManager
        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);
    }

前面提到Window是一个抽象类, 这里可以看出它的具体实现是PhoneWidow.
attach方法中创建了Window对象, 并为其设置一些回调接口, 以及WindowManager.
那么Activity的视图是怎么添加到Window中的呢?我们知道Activity是通过setContentView设置布局文件的, 下面看下setContentView的具体实现.

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

getWindow()即attach方法中创建的mWindow, 通过代码可以看出, Activity将具体操作交给了Window, 而Window的具体实现是PhoneWindow, 下面看下PhoneWindow中的setContentView方法.

    public void setContentView(int layoutResID) {
       
        if (mContentParent == null) {
            installDecor(); // 初始化DecorView及mContentParent
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();//移除所有View
        }
        
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);//转场动画
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);//把要加载的布局填充到mContentParent中
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();//回调
        }
        mContentParentExplicitlySet = true;
    }

该过程大致分以下步骤:
1. 创建DecorView.
DecorView是Activity的顶级View, 是一个FrameLayout, 一般包括标题栏和内容栏, 会根据设置的主题有所改变.

    private void installDecor() {
        mForceDecorInstall = false;
        //创建DecorView
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            ......
        } else {
            mDecor.setWindow(this);
        }
        //创建mContentParent 
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
                ......
            } else {
                //设置标题
                mTitleView = findViewById(R.id.title);
                ......
            }
            //转场动画相关属性
            if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
                ......
            }
        }
    }
    protected DecorView generateDecor(int featureId) {
        ......
        return new DecorView(context, featureId, this, getAttributes());
    }
    protected ViewGroup generateLayout(DecorView decor) {
    
        TypedArray a = getWindowStyle();
        //......获取样式的具体信息

        int layoutResource;
        int features = getLocalFeatures();
        //......根据features获取对应的布局layoutResource
        
        mDecor.startChanging();
        //将对应的布局layoutResource加载到DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //找到布局中id为R.id.content的容器
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ......
        return contentParent;
    }

generateDecor创建了一个DecorView, generateLayout根据主题获取对应到的布局, 并加载到DecorView中, 然后返回布局文件中id为R.id.content的容器, 及mContentParent.

2. 把Activity要加载的布局添加到mContentParent中.
mLayoutInflater.inflate(layoutResID, mContentParent);
上一步已经分析过, mContentParent是DecorView中的内容栏, 这一步是将Activity的视图添加到DecorView的内容栏.
这里DecorView的结构就跟清晰了, DecorView包括标题栏和内容栏, 我们通过Activity的setContentView就是将Activity的视图添加到了DecorView的内容栏.

3. 回调onContentChanged通知Activity视图内容发生了改变

        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }

上面Activity在attach方法中有mWindow.setCallback(this), 而Activity实现了Window的Callback接口, 到此为止, 已经将Activity的视图添加到了DecorView内容栏中, 并通过onContentChanged接口通知Activity视图已经发生改变.
注意setContentView方法只是将Activity的视图添加到DecorView的内容栏中, 那么什么时候添加到Window中显示呢?
上面讲到Activity的启动之后最终会到ActivityThread的handleLaunchActivity方法, 该方法首先调用performLaunchActivity(创建Activity对象, 并调用其attach方法), 然后调用handleResumeActivity, 该方法会调用Activity的makeVisible方法, 如下所示.

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

这里才真正将DecorView添加到Window中显示, Activity视图才真正被用户看到.

总结一下Activity启动之后Window的创建及显示过程:

  1. 在attach方法中创建Window并设置回调;
  2. setContentView时创建Decor, 并把Activity的视图添加到DecorView的内容栏;
  3. makeVisible时将DecorView添加到WIndow中显示.

到这里, Activity, Window, DecorView之间的层级关系已经跟清楚了, 如下图所示:


Activity.png

2. Dialog中Window的创建

Dialog的使用很简单, new Dialog创建对象, 设置布局, 标题等 , 最后通过show方法显示.
首先看下Dialog构造函数.

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == ResourceId.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);
    }

可以看出, Dialog构造函数中主要是初始化Context, 创建Window对象, 并为其设置一些回调, 这一点和Activity中Window的创建类似.

然后看下Dialog的setContentView方法.

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

这个过程和Activity一样, 通过Window添加指定的布局文件, 具体细节也是先创建DecorView, 然后把要加载的布局添加到DecorView的内容栏中.

然后看show方法.

    public void show() {
        ......
        mDecor = mWindow.getDecorView();

        ......
        mWindowManager.addView(mDecor, l);
        mShowing = true;

        sendShowMessage();
    }

这里通过WindowManager把DecorView添加到Window中.

Toast的Window创建和Activity的Window创建一样, 大致分为三步:

  1. 在构造函数中创建Window对象并设置回调;
  2. setContentView时通过Window添加布局(创建DecorView, 然后把Dialog的布局添加到DecorView的内容栏);
  3. show时将DecorView添加到Window中显示.

3. Toast中Window的创建

Toast的简单使用:

Toast.makeText(this, "This is a Toast!", Toast.LENGTH_SHORT).show();

首先看makeText的代码

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        //创建Toast对象
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        //系统默认显示的视图
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);
        //mNextView表设计要显示的视图, mDuration表示显示时长
        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

makeText比较简单, 创建了Toast对象, 然后给Toast对象指定一个系统默认的视图, 并设置消息内容及消息超时时长.
Toast构造函数中创建了一个TN对象, 并设置了位置, 看下TN的代码.

    private static class TN extends ITransientNotification.Stub {
        ......
        TN(String packageName, @Nullable Looper looper) {
            
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }
        @Override
        public void show(IBinder windowToken) {
            ......
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
        @Override
        public void hide() {
            .....
            mHandler.obtainMessage(HIDE).sendToTarget();
        }

        public void cancel() {
            ......
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }
    }

TN继承了ITransientNotification.Stub, 是一个Binder类, 它的构造函数中主要是初始化布局参数, 包名以及looper和handler, handler中处理了Toast的show, hide, cancel操作, 可见Toast的操作主要是通过TN来实现的.
因为TN运行在Binder线程池中, 所以需要使用Handler来切换线程, Handler消息处理机制戳这里.

接下来看show方法.

    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
        }
    }
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

首先获取INotificationManager, 然后调用其enqueueToast方法, INotificationManager是一个接口, 其实现类是NotificationManagerService,
它是系统为了管理各种App的Notification(包括Toast)的服务, 由这个服务来统一维护一个待展示Toast队列, 各App需要弹Toast的时候就将相关信息发送给这个服务, 服务会将其加入队列, 然后根据队列的情况, 依次通知各App展示和隐藏Toast.

下面看NotificationManagerService的enqueueToast方法.

        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            ......
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);
                    //mToastQueue中已经存在该Toast, 只更新它的显示时长
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        //如果是非系统应用, 一个应用内部最多只能容纳50个Toast
                        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++;
                                     //如果超过50, 直接返回, 不处理当前申请加入的Toast
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }
                        //封装一个ToastRecord添加到mToastQueue中
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    //显示当前Toast
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

首先判断mToastQueue中是否有当前要显示的Toast, 有则只更新显示时长, 无则构建一个ToastRecord对象添加到mToastQueue中.
对于非系统应用, mToastQueue最多只能容纳50个ToastRecord, 如果超过50, 当前申请加入的Toast直接抛弃不处理.
这里给该Token的Window设置了type为TYPE_TOAST, 说明Toast属于系统Window.
为什么只有在index=0时才调用showNextToastLocked()显示当前Toast呢? index=0说明mToastQueue中有且只有一个Toast或者有多个, 但是当前要显示的位于列表中第一个, 这时调用该方法显示该Toast. 如果mToastQueue中有多个Toast, 当前要显示的Toast不是第一个时就不调用该方法显示了吗? 看了后面的代码就清楚了.

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ......
            try {
                record.callback.show(record.token);
                scheduleTimeoutLocked(record);
                return;
            } catch (RemoteException e) {
                ......
                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方法里先取出mToastQueue中第一个ToastRecord, 通过callback的show显示, 这里的callback就是前面创建Toast时创建的TN. NotificationManagerService运行在系统进程, 所以要通过TN这个Binder类跨进程显示Toast.
然后调用scheduleTimeoutLocked处理超时逻辑.

上面分析过TN的show方法, 是发送了一个SHOW消息, 然后看下Handler中对应的处理方法handleShow.

        public void handleShow(IBinder windowToken) {
            ......
            if (mView != mNextView) {
                //View发生改变, 先隐藏之前的Toast, 再重新设置view
                handleHide();
                mView = mNextView;
                //......设置Context, 包名, WindowManager, 以及布局参数
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

这里完成了Toast视图添加到Window中显示的过程.

我们知道Toast显示几秒之后会自动消失, 下面看下NotificationManagerService中超时自动隐藏Toast的逻辑scheduleTimeoutLocked.

    @GuardedBy("mToastQueue")
    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);
    }

这里发送了一个MESSAGE_TIMEOUT消息, 并设置延迟时间, LONG_DELAY为3.5s, SHORT_DELAY为2s. 看下MESSAGE_TIMEOUT的处理.

    private void handleTimeout(ToastRecord record)
    {
        ......
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            ......
        }

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

延迟相应时间之后, NotificationManagerService通过cancelToastLocked方法隐藏Toast, 首先通过TN隐藏Toast, 然后将其从mToastQueue列表中移除, 如果mToastQueue列表中还有其他Toast, 就继续显示其他Toast, 这里就解释了前面的问题, 处理完mToastQueue列表中的第一个Toast并隐藏之后, 会继续显示列表中剩余Toast.

TN的hide方法是发送了一个HIDE消息, 看下TN的Handler中对应的处理方法handleHide.

        public void handleHide() {
            ......
            if (mView != null) {
                if (mView.getParent() != null) {
                    ......
                    mWM.removeViewImmediate(mView);
                }
                mView = null;
            }
        }

handleHide就是将Toast视图从Window中移除.

简单总结一下Toast中Window的创建过程.

  1. makeText时创建Toast对象, TN对象, 并设置要显示的视图, 消息及延时时长.
  2. show时, 跨进程到NotificationManagerService中, 构建ToastRecord对象加入mToastQueue列表中.
  3. NotificationManagerService依次取出mToastQueue列表中第一条Toast, 跨进程回调Toast中的TN进行显示, NotificationManagerService延迟相应时间之后, 跨进程到Toast中的TN进行隐藏, 然后将其从mToastQueue列表中移除.
  4. TN中的show和hide其实就是Window的添加和移除.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容