Android开发艺术探索:理解 Window 和 WindowManager

Android中的所有视图都是通过Window来呈现的,View都是被加载到Window里面,就是说Window是View的实际管理者,前面说的触摸事件就是从Window开始分发。

Window和WindowManager

添加Window:

private void addWindow() {
    final Button floatingButton  = new Button(this);
    floatingButton.setText("Floating Button");
    final WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
    final WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                                            WindowManager.LayoutParams.WRAP_CONTENT,0,0, PixelFormat.TRANSPARENT);
    params.gravity = Gravity.TOP|Gravity.LEFT;
    params.x = 0;
    params.y = 0;
    params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
    wm.addView(floatingButton,params);
    floatingButton.setOnTouchListener(new View.OnTouchListener() {
        float lastRawX;
        float lastRawY;
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(event.getActionMasked() == MotionEvent.ACTION_MOVE) {
                params.x += event.getRawX() - lastRawX;
                params.y += event.getRawY() - lastRawY;
                wm.updateViewLayout(floatingButton,params);
            }
            lastRawX = event.getRawX();
            lastRawY = event.getRawY();
            return false;
        }
    });
}

Flags表示Window的属性:

Window的位置需要结合gravity才能确定,上面的代码给 Button 设置了OnTouchEvent,不断的根据手指触摸的位置更新View的位置(书上的代码有问题,不是使用移动的增量来更新的,会出现手指与Button的位置不对应)。

LayoutParams另外一个重要的参数就是 type,总共有 type:

type也对应着Window的层次,层次大的会显示在最前端。

当我们添加一个Window的时候,系统会给我们带上 token 这个参数,以表明我们添加的是哪一种Window,并且是否合法,如果没有会被认为是一个System-Windows,需要添加权限。

Identifier for this window. This will usually be filled in for you.

关于Type的一些tip可以参考这个:Android 悬浮窗的小结

Window的内部机制

Window是一个抽象的概念,它是以View的形式存在的。我们一般操作Window都是使用 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);
}

Window添加的过程,WindowManager的真正实现类是WindowManagerImpl:

public final class WindowManagerImpl implements WindowManager {
    //是一个单例,真正执行操作的是WindowManagerGlobal
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        //检查参数,并给LayoutParams添加默认的token
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}
public final class WindowManagerGlobal {
    //用于记录添加Window的相关参数
    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>();

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) { 
        //前面检查参数,调整params的值...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            //...
            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

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

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

}

从上面的代码可以看的出来添加Window的操作实际是由 ViewRootImpl 来完成的:

//源代码实在太多而且很难看懂,以后用到再深究...通过mWindowSession来添加
mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);

最终是通过Binder机制,向WindowManagerService发起一次IPC调用来添加Window。

同理删除与更新也是通过Binder。

Window的创建过程

有视图(Activity,Dialog,Toast,PopUpWindow)的地方就有Window。

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) {
    //...
    mWindow = new PhoneWindow(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);
    }
    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();    
    //...
}

Window是一个抽象类,目前Android只提供了一种Window的实现,那就是 PhoneWindow。

之后我们的视图就是通过setContentView来把我们的View附加到Window上面的:

public void setContentView(@LayoutRes int layoutResID) {
    //这个Window就是刚才的那个PhoneWindow
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

//PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
    //...
    installDecor();
    //...
    mLayoutInflater.inflate(layoutResID, mContentParent);
    //...
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

private void installDecor() {
        //...
        if (mDecor == null) {
            mDecor = generateDecor();
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
        //...
}

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

就是创建一个DectorView(是一个FrameLayout,里面一般包含标题栏和内容栏,具体和主题有关系,例如NoTitle就是没有标题栏内容栏id为android.R.id.content),这个View就是mContentParent,然后会调用inflate把我们的View加载到mContentParent上,最后就会回调onContentChanged(),即Activity里面的内容已经发生改变。
到这个时候,DectorView还不能还没有添加到Window中,还需要调用ActivityThread#handleResumeActivity,里面会回调Activity#onResume,接着调用Activity#makeVisible(),这时候DectorView才能可:

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}
//Dialog.java
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
        if (themeResId == 0) {
            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.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
}

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

public void show() {
      //...
      mDecor = mWindow.getDecorView();
      //...
      mWindowManager.addView(mDecor, l);
      mShowing = true;
      //...
}

可以看见和Activity的Window创建类似,创建一个PhoneWindow与Dialog建立关联并设置Window.Callback,还有创建ListenersHandler,用于发送
Dialog#show,cancel,dismiss的回调消息。
然后通过setContentView设置我们的内容,调用show方法,把DectorView添加到Window里面。
另外,创建Dialog使用ApplicationContext会抛异常(里面没有包含Window的Context,例如Service,因为Dilog是做为Sub-Windows附加在Application-Windows上面的),因为里面没有应用的token(就是WindowManager.LayoutParams.token参数),一般使用Activity里面的Context,如果有使用ApplicationContext的需求,就需要改变Window的type,并声明权限:

dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR);
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW">

Toast属于系统Window,内部视图可以是系统的默认样式,另一种则是通过setView来指定。通过show()和cancel()来实现Toast的显示和隐藏,并且都是IPC过程调用。远端的服务就是NotificationManagerService。

Toast.makeToast里面加载默认的View,创建TN,这是一个Binder对象用于服务端回调

public Toast(Context context) {
    //...
    mTN = new TN();
    //...
}

获取一个远端的Binder对象(Proxy),发起IPC调用enqueueToast,TN用于服务端回调客户端

public void show() {
    //...
    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;
}

服务端NotifiactionManagerService,执行操作
首先会遍历ToastQueue大小是否超过了50,如果是那么就不再往队列里面添加,防止DOS
然后执行showNextToastLocked

public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
    //注意这个锁
    synchronized (mToastQueue) {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, callback);
            //如果已经存在就不会添加,只是更新duration
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                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++;
                             //每一个应用的ToastQueue只能存50个Toast
                             //所以遍历防止DOS
                             if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                 return;
                             }
                         }
                    }
                }
                record = new ToastRecord(callingPid, pkg, callback, duration);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                //...
            }
            if (index == 0) {
                showNextToastLocked();
            }
        }
}

获取客户端的TN回调show()方法,mHandler随后post一个Runnable对象,执行handleShow,往Window里面添加View

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
            //...
            //回调客户端里面的TN
            record.callback.show();
            scheduleTimeoutLocked(record);
            return;
            //...
    }
}

private static class TN extends ITransientNotification.Stub {
    public void show() {
        mHandler.post(mShow);
    }


    final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
    };

    public void handleShow() {
        if (mView != mNextView) {
            // remove the old view if necessary
            handleHide();
            mView = mNextView;
            //...
            mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            mWM.addView(mView, mParams);
            //...
        }
    }
}

根据Toast的duration,想Handler发送一个delay消息,用于控制Toast的显示时长

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

private final class WorkerHandler extends Handler {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_TIMEOUT:
                    handleTimeout((ToastRecord)msg.obj);
                    break;
            }
        }
}

取出显示的Toast,然后回调客户端的TN#hide,完成隐藏的操作,如果队列里面还有其他的Toast,那个继续调用showNextToastLocked,显示下一个Toast

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);
    record.callback.hide();
    mToastQueue.remove(index);
    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里面的回调,从Window去掉我们添加的View

private static class TN extends ITransientNotification.Stub {
    final Runnable mHide = new Runnable() {
        @Override
        public void run() {
            handleHide();
            //...
        }
    };
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.post(mHide);
    }


public void handleHide() {
    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) {
            mWM.removeView(mView);
        }
        mView = null;
    }
}

这里需要注意的一点是,TN是在Binder的线程池里面执行的,所以需要使用Handler向我们执行Toast#show或者Toast#cancel的线程发送请求,执行Runnable,如果我们是在子线程里面执行的show操作,那么将会抛异常,因为默认子线程是没有Looper对象的。
我们自己可以新建一个Looper,但是更推荐使用HandlerThread来处理。

Looper.prepare();
Looper.loop();