Android设计模式(二) 续:WindowManager

目录

  1. 获取WindowManager
  2. Dialog的显示
  3. ViewRootImpl
    1.    new ViewRootImplviewgetContext display
      
    2. setViewview wparams panelParentView
    3.   requestLayout
      
  • 注意Dialog的使用系统级弹窗

博客地址
通过前面的分析可以知道,Android系统中,所有的界面内容显示都是通过Window来实现的,包括Activity,Dialog,Toast等。
先初步分析一下Window,WindowManager,WindowManagerService的关系。

获取WindowManager

Android设计模式(一)-单例模式中后面的内容可以看到,系统在启动的时候就注册了许多服务。其中就有这样的代码:

package android.app;
final class SystemServiceRegistry {
......
    static {
......
        registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx);
            }});
    }
}

这样就创建了一个WindowManager,而且也能看出来WindowManager的实现类是WindowManagerImpl。
同理,要获得一个WindowManager也很简单了:

WindowManager manager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

Dialog的显示

从头开始分析,先看AlertDialog的构造方法:

AlertDialog(Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        super(context, createContextThemeWrapper ? resolveDialogTheme(context, themeResId) : 0,
                createContextThemeWrapper);

        mWindow.alwaysReadCloseOnTouchAttr();
        mAlert = AlertController.create(getContext(), this, getWindow());
    }

调用了父类Dialog的构造方法:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
       ......
//获取WindowManager
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//创建一个PhonwWindow
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
//设置回调
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
//设置WindowManager
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

Window通过调用他的w.setWindowManager(mWindowManager, null, null)将WindowManager和Window联系起来。:

package android.view;
public abstract class Window {
        ......
        public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ......
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
}

最后一句代码很重要mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);,调用了WindowManagerImpl的createLocalWindowManager方法,这个方法里就一行代码

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

这里重新创建了一个WindowManagerImpl,但是参数比系统注册的时候多了一个构造参数parentWindow。这说明这个WindowManagerImpl是跟一个具体的Window绑定起来的。

package android.view;
public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;
......
    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }
......
    public void setDefaultToken(IBinder token) {
        mDefaultToken = token;
    }
    @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);
    }
    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }
......
    @Override
    public Display getDefaultDisplay() {
        return mContext.getDisplay();
    }
}

从中 很明显的可以看出真正实现一些关键方法如addview 、remoteView等方法的也不是WindowManagerImpl,而是在内部交给了WindowManagerGlobal,由WindowManagerGlobal来真正实现。

然后看一下真正的addView吧:

package android.view;
public final class WindowManagerGlobal {
    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>();

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
       ......
        ViewRootImpl root;
        View panelParentView = null;
        synchronized (mLock) {
            ...... 
//创建ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
//添加到列表中
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // 最后执行这个,将View显示在手机屏幕上
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
          ......
        }
    }
}

ViewRootImpl

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

上面的方法中先是new了一个ViewRootImpl,然后调用他的setView来显示布局。
Android中的View都是通过ViewRootImpl来完成绘制的。

书中说这个类继承Handler ,可能是以前是这样吧,我看的源码不是这样的

package android.view;
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {
    public ViewRootImpl(Context context, Display display) {
        mContext = context;
//获取IWindowSession,与WindowManagerService建立连接
        mWindowSession = WindowManagerGlobal.getWindowSession();
  ......
//这里保存当前线程
        mThread = Thread.currentThread();
        ......
    }
}

继续追踪WindowManagerGlobal.getWindowSession():

public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager imm = InputMethodManager.getInstance();
//获取WindowManagerService
                    IWindowManager windowManager = getWindowManagerService();
//与系统的WindowManagerService建立一个IWindowSession
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            },
                            imm.getClient(), imm.getInputContext());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }

public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
//这里返回的是IBinder对象,进行IPC通信
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
               ......
            }
            return sWindowManagerService;
        }
    }

继续看 sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));这行:

package android.os;
public final class ServiceManager {
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }
}

所以IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));得到的是一个IBinder对象。

到这里,在ServiceManager的getService方法中通过getIServiceManager().getService("window")获取到一个IBinder,与WMS建立初步连接。
然后通过IWindowManager.Stub.asInterface方法将IBinder转换成IWindowManager对象。
通过这个对象调用openSession打开一个Session,实现通话。

但是,WMS只负责管理View的z-order,也就是管理当前那个View在最上层显示,并不管理绘制。

setView(view, wparams, panelParentView);

既然addView就肯定要把view显示在屏幕上,那么绘制View的任务就在ViewRootImpl的setView方法中。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            ......
//请求绘制View
            requestLayout();
            ......
            int res; /* = WindowManagerImpl.ADD_OKAY; */
            try {
                   ......
//请求WindowManagerService,让WMS实现Window的添加。
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } 
              ......
       }
    }

setView主要做了两件事:
(1)requestLayout();
(2)向WMS请求添加Window;

requestLayout();

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

这里我另外注意一下checkThread()这个方法。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

是不是看到了熟悉的异常提示?这个方法要判断当前更新UI 的线程是不是创建ViewRootImpl时的线程,只有在创建ViewRootImpl的线程中更新对应的UI才不会报错。所以不能在子线程中更新UI也是这个原因。

回到requestLayout(),查看scheduleTraversals();

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ......
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
           ......
        }
    }

这里的mTraversalRunnable是个Runnable对象,mChoreographer.postCallback最终会通过一个handler把这个任务发送出去:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

看doTraversal();

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
......
            performTraversals();
......
        }
    }

看performTraversals();
这个方法里将近一千行代码,大致是四个过程:

private void performTraversals(){
  // 1 获取Surface对象,用于图形绘制
  //2 测量整个视图树中各个View的大小,用performMeasure方法
  //3 布局整个视图树,用performLayout方法
  //4 绘制整个视图树,用performDraw方法
}

在第四步中,performDraw方法里会调用ViewRootImpl的draw()方法。

draw()中获取到绘制表面Surface,里面最后调用ViewRootImpl的drawSoftware方法,调用GPU绘图。

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        final Canvas canvas;
        try {
//获取canvas
            canvas = mSurface.lockCanvas(dirty);

            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
        }

        try {
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
            try {
                ......
//从这里开始绘制整个视图树,从DecorView开始
                mView.draw(canvas);
            } finally {
                ......
            }
        } finally {
            try {
//解锁canvas,并通知SurfaceFlinger更新这块区域。
                surface.unlockCanvasAndPost(canvas);
            } catch (IllegalArgumentException e) {
              ......
            }
        }
        return true;
    }

综上所述,视图树的绘制主要有以下步骤:
(1)判断使用CPU还是GPU绘制
(2)获取绘制表面Surface对象
(3)通过Surface对象获取并锁住绘图对象
(4)从DecorView开始绘制整个视图树
(5)解锁Canvas,并通知SurfaceFlinger更新这块区域。

注意Dialog的使用,系统级弹窗

在创建AlertDialog时,如果传入的Context是ApplicationContext而不是Activity的Context,那么抛出一个异常:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
                                                                              at android.view.ViewRootImpl.setView(ViewRootImpl.java:583)
                                                                              at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:313)
                                                                              at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)

看出出错的地方是ViewRootImpl中的setView方法中。在这个方法中,调用mWindowSession.addToDisplay发送添加View的请求后会返回一个数值。
然后对这个数值进行判断

int res; 
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
if (res < WindowManagerGlobal.ADD_OKAY) {
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");
                        case WindowManagerGlobal.ADD_APP_EXITING:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- app for token " + attrs.token
                                    + " is exiting");
                        case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- window " + mWindow
                                    + " has already been added");
                        case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- another window of type "
                                    + mWindowAttributes.type + " already exists");
                        case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- permission denied for window type "
                                    + mWindowAttributes.type);
                        case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified display can not be found");
                        case WindowManagerGlobal.ADD_INVALID_TYPE:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified window type "
                                    + mWindowAttributes.type + " is not valid");
                    }
                    throw new RuntimeException(
                            "Unable to add window -- unknown error code " + res);
                }

通过异常提示可以看出,异常的原因是应用没有token造成的。而token一般只有activity才有,所以要传入activity的context才行。

有一个特殊的对话框不需要传入activity,那就是系统对话框。不传入activity的context也能正常弹出系统对话框。
使用过程:

  • 在AndroidManifest.xml中加上权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
  • 然后在调用dialog.show()之前加上下面一句
alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);

系统的type有很多都可以用,TYPE_SYSTEM_ERROR,TYPE_SYSTEM_OVERLAY都可以。

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

推荐阅读更多精彩内容