欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 开发艺术探索之Window和WindowManager解析

程序员文章站 2022-06-02 22:27:41
window表示一个窗口的概念,桌面上显示一个悬浮窗的东西就需要用到window来实现。window是一个抽象类,它的具体实现是phonewindow,创建可以通过windowmanager即可完成...

window表示一个窗口的概念,桌面上显示一个悬浮窗的东西就需要用到window来实现。window是一个抽象类,它的具体实现是phonewindow,创建可以通过windowmanager即可完成。windowmanager是外界访问window的入口,window的具体实现位于windowmangerservice中,windowmanager和windowmangerservice的交互是一个ipc过程。android中所有的视图都是通过window来呈现的,不管是activity、dialog还是toast,它们的视图实际上都是通过附加在window上的,因此,window实际上是view的直接管理者。

window和windowmanager

flags参数表示window的属性,通过这些选线可以控制window的显示特性,介绍如下常用的:

flag_not_focusable
表示window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用flag_not_touch_modal,最终事件会传递给下层的具有焦点的window。

flag_not_touch_modal
在此模式下,会将当前window区域以外的单击事件传递给底层的window,当前window区域以内的单击事件则自己处理。不开启此标记,其它window无法接收到单击事件。

flag_not_when_locked
开启此模式可以让window显示在锁屏的界面上。

type参数表示window的类型,window有三种类型,分别是应用window、子window和系统window。

应用类window对应着一个activity。子window不能单独存在,它需要附属在特定的父window之中,比如常见的一些dialog就是一个子window。系统window是需要声明权限才能创建的window,比如toast和系统状态栏这些都是系统window。

window是分层的,每个window都对应着z-ordered,层级大的会覆盖在层级小的window上面。应用window的层级范围是1~999,子window的范围1000~1999,系统window的层级范围是2000~2999,层级范围对应着windowmanager.layoutparams的type参数。系统window的层级是最大的,系统层级有很多值,我们一般选用type_system_error或者type_system_overlay;如果采用type_system_error,只需要为type参数指定mlayoutparams.flags = windowmanager.layoutparams.type_system_error;同时申明权限:


系统类型的window是要检查权限的。

windowmanager所提供的功能很简单,常用的只有3个方法,即添加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);
}

可以拖动的view效果我们只需要根据手指的位置来设定layoutparams中的x和y的值即可改变window的位置。首先给view设置ontouchlistener,然后在ontouch方法中不断更新view的位置即可:

public boolean ontouch(view v, motionevent event) {
                int rawx = (int) event.getrawx();
                int rawy = (int) event.getrawy();
                switch (event.getaction()){
                    case motionevent.action_move:
                        mlayoutparams.x = rawx;
                        mlayoutparams.y = rawy;
                        mwindowmanager.updateviewlayout(mfloatingbutton, mlayoutparams);
                        break;

                    default:
                        break;
                }
                return false;
            }

window的内部机制

window是一个抽象的概念,每一个window都对应着一个view和一个viewrootimpl,window和view通过viewrootimpl来建立联系,因此window并不是实际存在的,它是以view的形式存在,对window的访问必须通过windowmanager。

window的添加过程

window的添加过程需要通过windowmanager的addview来实现,windowmanager是一个接口,它的真正实现是windowmanagerimpl类。在windowmanagerimpl中window的三大操作实现如下:

 @override
    public void addview(@nonnull view view, @nonnull viewgroup.layoutparams params) {
        applydefaulttoken(params);
        mglobal.addview(view, params, mdisplay, 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来处理,windowmanagerglobal以工厂的方式向外提供自己的实例。windowmanagerglobal的addview方法主要分为如下几步。

检查参数是否合法,如果是子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);
   }

创建viewrootimpl并将view添加到列表中
在windowmanagerglobal内部有如下几个列表比较重要:

private final arraylist mviews = new arraylist();
private final arraylist mroots = new arraylist();
private final arraylist mparams =
        new arraylist();
private final arrayset mdyingviews = new arrayset();

在上面的声明中,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);

通过viewrootimpl来更新界面并完成window的添加过程
这个步骤由viewrootimpl的setview来完成,view的绘制过程是由viewrootimpl来完成的。在setview的内部会通过requestlayout来完成异步刷新请求,scheduletraversals是view绘制的入口:

public void more ...requestlayout() {
     if (!mhandlinglayoutinlayoutrequest) {
         checkthread();
         mlayoutrequested = true;
         scheduletraversals();
     }
 }

接着会通过windowsession最终来完成window的添加过程。mwindowsession的类型是iwindowsession,它是一个binder对象,真正的实现类是session,也就是window的添加过程是一次ipc调用。

            try {
                 morigwindowtype = mwindowattributes.type;
                 mattachinfo.mrecomputeglobalattributes = true;
                 collectviewattributes();
                 res = mwindowsession.addtodisplay(mwindow, mseq, mwindowattributes,
                         gethostvisibility(), mdisplay.getdisplayid(),
                         mattachinfo.mcontentinsets, 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);
             }

在session内部会通过windowmanagerservice来实现window的添加,如下所示:
window的添加请求就交给windowmanagerservice去处理了,在windowmanagerservice内部会为每一个应用保留一个单独的session。

 public int addtodisplay(iwindow window, int seq, windowmanager.layoutparams attrs,  
            int viewvisibility, int displayid, rect outcontentinsets, inputchannel outinputchannel) {  
        return mservice.addwindow(this, window, seq, attrs, viewvisibility, displayid,  
                outcontentinsets, outinputchannel);  
    }  

window 的添加请求就交给windowmanagerservice去处理了,在windowmanagerservice内部会为每一个应用保留一个单独的session。

window的删除过程

window的删除过程和添加过程一样,都是先通过windowmanagerimpl,再进一步通过windowmanagerimpl来实现的。代码如下:

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来完成删除操作的。在windowmanager中提供了两种删除接口removeview和removeviewimmediate,分别表示异步删除和同步删除,后者使用需要注意,一般不需要使用此方法。removeviewimmediate具体的删除由viewrootimpl的die方法来完成。在异步删除的情况下,die方法只是发送一个请求删除的消息后就返回了,这个时候view并没有完成删除操作,最后会将其添加到mdyingviews中,mdyingviews表示待删除的view列表。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(tag, "attempting to destroy the window while drawing!\n" +
                    "  window=" + this + ", title=" + mwindowattributes.gettitle());
        }
        mhandler.sendemptymessage(msg_die);
        return true;
    }

如果异步删除,就发送一个msg_die的消息,viewrootimpl中的handler会处理此消息并调用dodie方法,如果是同步删除,那么就不发消息直接调用dodie方法,这就是这两种删除方式的区别。在dodie的内部会调用dispatchdetachedfromwindow 方法,dispatchdetachedfromwindow 主要做四件事:

垃圾回收相关操作,比如清楚数据、移除回调。 通过session的remove方法删除window,同样是一个ipc过程。 调用view的dispatchdetachedfromwindow方法,做一些资源回收的相关工作,比如终止动画、停止线程等。 调用windowmanagerglobal的doremoveview方法刷新数据。

window的更新过程

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

首先需要更新view的layoutparams 并替换掉老的layoutparams ,接着更新viewrootimpl的setlayoutparams 方法来实现。在viewrootimpl中会通过scheduletraversals方法来对view重新布局,包括测量、布局、重绘三个过程。除了本身的重绘以外,viewrootimpl还会通过windowsession来更新window的视图。

window的创建过程

view 是 android 中视图的呈现方式,但是 view 不能单独存在,它必须附着在 window 这个抽象的概念上面,因此有视图的地方基友 window。android 中可以提供视图的地方就有activity、dialog、toast。

activity 的 window 创建过程
activity 的启动过程很复杂,最终会由 activitythread 中的 performlaunchactivity 来完成整个启动过程:

 java.lang.classloader cl = r.packageinfo.getclassloader();
            activity = minstrumentation.newactivity(
                    cl, component.getclassname(), r.intent);
...
 if (activity != null) {
                context appcontext = createbasecontextforactivity(r, activity);
                charsequence title = r.activityinfo.loadlabel(appcontext.getpackagemanager());
                configuration config = new configuration(mcompatconfiguration);
                if (r.overrideconfig != null) {
                    config.updatefrom(r.overrideconfig);
                }
                if (debug_configuration) slog.v(tag, "launching activity "
                        + r.activityinfo.name + " with config " + config);
                window window = null;
                if (r.mpendingremovewindow != null && r.mpreservewindow) {
                    window = r.mpendingremovewindow;
                    r.mpendingremovewindow = null;
                    r.mpendingremovewindowmanager = null;
                }
                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);
...

在 activity 的 attach 方法里,系统会创建 activity 所属的 window 对象并为其设置回调接口,window 对象的创建是通过 phonewindow 来实现的,是 window 的具体实现类。

phonewindow 的 setcontentview 方法遵循如下几个步骤:
1.如果没有 decorview, 那么就创建它
decorview 是一个framelayout, decorview 是 activity 的* view,一般来说包含标题栏和内部栏,会随着主题的变换而发生改变。内容栏固定的 id 就是 “content” ,完整 id 就是 android.r.id.content。decorview 的创建过程由 installdecor 方法来完成,通过 generatedecor 方法来直接创建 decorview。为了初始化 decorview 的结构,phonewindow 还需要通过 generatelayout 方法来加载具体的布局文件到 decorview 中,具体的布局文件和系统版本以及主题有关。

2.将 decorview 添加到 mcontentparent 中
将 activity 视图添加到 decorview 的 mcontentparent 中,所以叫 setcontentview 更加准确。

3.回调 activity 的 oncontentchanged 方法通知 activity 视图已经发生改变
activity 实现了 window 的 callback 接口,这里表示 activity 的布局文件已经添加到 decorview 的 mcontentparent 中了,需要通知 activity 做相应的处理。

经过上面三个步骤,decorview 已经被创建并初始化完毕,但是 decorview 还没有被 windowmanager 正式添加到 window 中。window 更多的是表示一种抽象的功能集合,在 activitythread 的 handhandleresumeactivity 方法中,首先会调用 activity 的 onresume 方法,接着会调用 activity 的 makevisible() ,decorview 真正的完成了添加和显示这两个过程。

void makevisible() {
        if (!mwindowadded) {
            viewmanager wm = getwindowmanager();
            wm.addview(mdecor, getwindow().getattributes());
            mwindowadded = true;
        }
        mdecor.setvisibility(view.visible);
    }

dialog 的 window 创建过程

1.创建window
dialog 中 window 的创建后的实际对象就是 phonewindow,和 activity 的 window 创建过程是一致的。

 dialog(@nonnull context context, @styleres int themeresid, boolean createcontextthemewrapper) {
        ...

        mwindowmanager = (windowmanager) context.getsystemservice(context.window_service);

        final window w = new phonewindow(mcontext);
        mwindow = w;
        w.setcallback(this);
        w.setonwindowdismissedcallback(this);
        w.setwindowmanager(mwindowmanager, null, null);
        w.setgravity(gravity.center);

        mlistenershandler = new listenershandler(this);
    }

2.初始化 decorview 并将 dialog 的视图添加到 decorview 中
过程和 activity 类似,都是通过 window 去添加指定的布局文件

public void setcontentview(@layoutres int layoutresid) {
        mwindow.setcontentview(layoutresid);
    }

3.将 decorview 添加到 window 中并显示

 mwindowmanager.addview(mdecor, l);
        mshowing = true;

在 dialog 的 show 方法中,会通过 windowmanager 将 decorview 添加到 window 中。
dialog 被关闭时,会通过 windowmanager 来移除 decorview:mwindowmanager.removeviewimmediate(mdecor);
普通 dialog 必须采用 activity 的 context,需要用到应用 token ,一般只有 activity 拥有,系统的 window 比较特殊,不需要 token。

toast的 window 创建过程

toast 也是基于 window 来实现的,但是由于 toast 具有定时取消这一功能,所以系统采用了 handler。 在 toast 内部有两类 ipc 过程,第一类是 toast 访问 notificationmanagerservice (简称nms), 第二类是 notificationmanagerservice 回调 toast 里的 tn 接口。

toast 属于系统 window,它内部的视图由两种方式指定,一种系统默认的样式,另一种是通过 setview 方法来指定一个自定义 view,对应 toast 的一个 view 类型的内部成员 mnextview。

 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.hide();

        try {
            getservice().canceltoast(mcontext.getpackagename(), mtn);
        } catch (remoteexception e) {
            // empty
        }
    }

显示和隐藏 toast 都需要通过 nms 来实现,tn 是一个 binder 类,在 toast 和 nms 进行 ipc 的过程中,当 nms 处理 toast 的显示或隐藏请求时会跨进程回调 tn 中的方法,由于 tn 运行在 binder 线程池中,所以需要通过 handler 将其切换到当前线程中。使用了 handler 意味着 toast 无法在没有 looper 的线程中弹出。
toast 的显示过程,首先调用了 nms 中的 enqueuetoast 方法, enqueuetoast 首先将 toast 请求封装为toastrecord 对象并且将其添加到一个名为 mtoastqueue 的队列中。 mtoastqueue 最多存在50个toastrecord ,为了防止 dos 。

if (!issystemtoast) {
                            int count = 0;
                            final int n = mtoastqueue.size();
                            for (int i=0; i= max_package_notifications) {
                                         slog.e(tag, "package has already posted " + count
                                                + " toasts. not showing more. package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

当 toastrecord 被添加到 mtoastqueue 中后, 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();
                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);
                }
                keepprocessalivelocked(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 方法来发送一个延迟消息

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

long_delay 是3.5s,, 而 short_delay 是2s。延迟相应的时间后, nms 会通过 canceltoastlocked 方法来隐藏 toast 并将其从 mtoastqueue 中移除,这个时候如果 mtoastqueue 中还有其他 toast,那么 nms 就继续显示其他 toast。

toast 的隐藏也是通过 toastrecord 的 callback 来完成的,也是一次 ipc 过程。

toast 的显示和隐藏过程实际上是通过 toast 的 tn 这个类来实现的,他有两个方法 show 和 hide,分别对应 toast 的显示和隐藏。 这两个方法由于是被 nms 跨进程的方式调用的,运行在 binder 线程池中。 为了将执行环境切换到 toast 请求所在的线程,在它们的内部使用了 handler ,如下所示。

 /**
         * schedule handleshow into the right thread
         */
        @override
        public void show() {
            if (locallogv) log.v(tag, "show: " + this);
            mhandler.post(mshow);
        }

        /**
         * schedule handlehide into the right thread
         */
        @override
        public void hide() {
            if (locallogv) log.v(tag, "hide: " + this);
            mhandler.post(mhide);
        }

tn 中的 handleshow 中会将 toast 的视图添加到 window 中

mwm = (windowmanager)context.getsystemservice(context.window_service);
mwm.addview(mview, mparams);

nt 中的 handlehide 中会将 toast 的视图从 window 中移除

if (mview.getparent() != null) {
                    if (locallogv) log.v(tag, "remove! " + mview + " in " + this);
                    mwm.removeview(mview);
                }