Android 开发艺术探索 - 读书笔记之第八章 理解 Window 和 WindowManager

8.1 Window 和 WindowManager

示例代码:简单地添加一个 Window

    Button btn = new Button(this);
    btn.setText("Button");
    LayoutParams params = new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
    params.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL | 
        LayoutParams.FLAG_NOT_FOCUSABLE |
        LayoutParams.FLAG_SHOW_WHEN_LOCKED;
    params.gravity = Gravity.LEFT | Gravity.TOP;
    params.x = 100;
    params.y = 300;
    windowManager.addView(btn, params);

params 两个重要参数 flags 和 type
1、几个常用的 FLAG:

FLAG_NOT_FOUCSABLE:直接把事件传递给下层具有焦点的 Window
FLAG_NOT_TOUCH_MODAL:将当前 Window 区域以外的事件传递给底层 Window
FLAG_SHOW_WHEN_LOCKED:可以显示在锁屏的界面上

2、window 的三种 type

  • 应用 Window:层级范围 1-99,优先级最低;一个应用 Window 对应一个 Activity
  • 子 Window:1000-1999,优先级中等;例如对应一个 Dialog 。
  • 系统 Window:2000-2999,一般为 TYPE_SYSTEM__OVERLAY 或 TYPE_SYSTEM_ERROR ,需要声明 SYSTEM_ALERT_WINDOW 权限

最终调用 WindowManager 的方法将控件呈现到屏幕上;WindowManager 继承 ViewManager,提供了对 View 的增删改三个方法:

addView(View view, ViewGroup.LayoutParams params);
updateViewLayout(View view, ViewGroup.LayoutParams params);
removeView(View view);

8.2 Window 的添加、删除、更新过程

  • 每一个 Window 都对应着一个 View 和一个 ViewRootImpl
  • Window 和 View 通过 ViewRootImpl 建立联系
  • 无法直接操作 Window,只能通过 WidowManager

8.2.1 Window 的添加过程

WindowManagerImpl 将所有对 View 的操作全部委托 WindowManagerGlobal 处理(桥接模式?);WindowManagerGlobal 添加 View 的过程:

  • 检查参数:view、display 是否为空,params 是否为 WindowManager.LayoutParams
if (view == null) throw ....
if (display == null) throw ...
if (!(params instanceof WindowManager.LayoutParams)) throw ...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if (parentWindow != null) parentWindow.adjustLayoutParamsForSubWindwo(wparams);
  • 创建 ViewRootImpl ,使用 ArrayList 管理 mViews、mRoots、mParams 和 mDyingViews
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
  • 通过 ViewRootImpl 来更新界面并完成 Window 的添加过程
    • ViewRootImpl.setView
    • ViewRootImpl.requestLayout
    • ViewRootImpl.scheduleTraversals
    • 最终,通过 WindowSession 完成 Window 的添加过程
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

在 scheduleTraversals() 方法中将出现:

mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel);

mWindowSession 的类型是 IWindowSession,他是一个 Binder 对象,真正的实现类是 Session,也就是 Window 的添加过程是一次 IPC 调用。

  • 在 Session 内部会通过 WindowManagerService 来实现 Window 的添加

mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outInputChannel);

8.2.2 Window 的删除过程

代码:WindowManagerGlobal.removeView()

public void removeView(View view, boolean immediate) {
    if (View == null)
        throw ....
    synchronized (mLock) {
        // 遍历数组,查找索引
        int index = findViewLocked(view, true);
        View curView = mRoots.get(index).getView();
        removeViewLocked(index, immediate);
        if (curView == view) 
            return;
        throw ***
    }
}

代码:removeViewLocked(int index, boolean immediate)

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 defered = root.die(immediage);
    if (view != null) {
        view.assignParent(null);
        if (defered) {
            // mDyingViews 待删除列表
            mDyingViews.add(view);
        }
    }
}
  • WindowManagerGlobal 通过 ViewRootImpl 来完成删除操作
  • removeView:异步删除,通过 ViewRootImpl 的 die 方法
  • removeViewImmediate:同步删除(不常用)

代码:ViewRootImpl.die() 方法

boolean die (boolean immediate) {
    if (immediate && !mIsInTraversal) {
        doDia();
        return flase;
    }
    
    if (!mIsDrawing) {
        destroyHardwareRenderer();
    } else {
        Log.e(TAG, "尝试摧毁正常绘制中的 Window");
    }
    // ViewRootImpl 的 mHandler 将处理此消息并调用 doDie
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

当 immediate 为 false,使用异步删除,就发送一个 MSG_DIE 的消息,ViewRootImpl 的 mHandler 将处理此消息并调用 doDie 方法;如果是同步删除,就是直接调用 doDie 方法;doDie 方法会调用 dispatchDetachedFromWindow 方法,内部真正实现了 View 的删除逻辑。
dispatchDetachedFromWindow 方法做了四步工作:

  • 垃圾回收,例如清除数据、消息、移除回调
  • 通过 Session 的 remove 方法删除 Window

mWindow.remove(mWindow)
WindowManagerService.removeWindow()

  • 调用 View 的 dispatchDetachedFromWindow 方法,内部调用 View 的 onDetachedFromWindow、onDetachedFromInternal()
  • 调用 WindowManagerGlobal 的 doRemoveView 方法刷新数据,包括 mRoots、mParams、mDyingViews

8.2.3 Window 的更新过程

代码:WindowManagerGlobal.updateViewLayout

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    if (View == null) throw ....
        
    if (!(params instanceof WindowManager.LayoutParams)) throw ...
    
    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);
        root.setLayoutParams(wparams, false);
    }
}
  • 更新 View 的 LayoutParams,更新 ViewRootImpl 的 LayoutParams,内部调用 scheduleTraversals 对 view 重绘,通过 WindowSession 更新 Window 的视图,最终调用 WindowMService 的 relayoutWindow() 具体实现。

8.3 Window 的创建过程

  • View 是 Android 视图的呈现方式
  • View 必须附着在 Window 上
  • 这一节将解释 Activity、Dialog、Toast 中 Window 的创建过程

8.3.1 Activity 的 Window 创建过程

代码: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);
    // 为 Activity 关联运行过程中所依赖的一系列上下文环境变量
    activity.attch(appContext, this, getInstrumentation(), r.token, 
            r.ident, app, r.intent, r.activityInfo, title, r.parent, 
            r.embeddedID, r.lastNonConfigurationInstances, config, r.voiceInteractor);
}

在 Activity 的 attach 方法内部,系统为 Activity 创建所属 Window 对象并设置回调,注意此时并未与 WindowManager 关联,最终 onResume 时才会完成关联:

mWindow = PolicyManager.makeNewWindow(this);
// 当 Window 接收到外界状态改变时回调 Activity 的接口实现
// onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent
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);
}

PolicyManager 实现了 IPolicy 定义的四个方法:

public interface IPolicy {
    public Window makeNewWindow(Context context);
    public LayoutInflater makeNewLayoutInflater(Context context);
    public WindowManagerPolicy makewNewWindowManager();
    public FallbackEventHandler makeNewFallbackEventHandler(Context context);
}

代码:PolicyManager 的实现 Policy,makeNewWindow

public Window makeNewWindow(Context context) {
    return new PhoneWindow(context);
}

setContentView 过程:

  • 1、如果没有 DecorView,那么就创建它,installDecor
protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

为了初始化 PhoneWindow 还要通过 generateLayout 加载具体的布局到 DecorView 中

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT);
    mContentRoot = (ViewGroup) in;
    ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
  • 2、将 View 添加到 DecorView 的 mContentParent 中

mLayoutInflater.inflate(layoutResId, mContentParent);

  • 3、回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
  • 4、ActivityThread.handleResumeActivity 方法中,调用 Activity onResume 方法,调用 Activity makeVisible(),在 makeVisible 方法中真正完成了添加和显示,才能被用户所看到
void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

总结就是,Activity 最终由 ActivityThread 的 performLaunchActivity() 方法中完成启动;其中会调用 Activity 的 attach 方法,将 Activity 实现的回调注入到刚创建的 PhoneWindow 对象中;在 Window 的 setContentView 方法中,首先检查 DecorView 是否存在,不存在则调用 installDecor,内部调用 generateDecor 创建 DecorView,调用 generateLayout 加载布局文件到 DecorView 的 contentParent 中(id:ID_ANDROID_CONTENT),最后回调 Activity 的 onContentChanged() 标识着视图初始化完毕;最后在 ActivityThread 的 handleResumeActivity 中,调用 Activity 的 onResume() -> makeVisible(),才真正完成了添加和显示的过程

8.3.2 Dialog 的 Window 创建过程

  • 1、创建 Window
  • 2、初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
  • 3、将 DecorView 添加到 Window 中并显示
  • 4、关闭时调用 WindowManager 的 removeViewImmediate(mDecor) 移除

Q:普通的 Dialog 为什么只能使用 Activity 的 Context,而不能使用 Application 的 Context?
A:Exception: Unable to add window -- token null is not for an application,意思是没有应用token,一般只有 Activity 才有;而如果是系统类型的 Dialog就可以正常弹出。

8.3.3 Toast 的 Window 创建过程

  • 由于 Toast 内部有定时取消的功能,所以系统采用了 Handler
  • 两个 IPC 过程
    • Toast 访问 NotificationManagerService
    • NotificationManagerService 回调 Toast 的 TN 接口
  • Toast 属于系统 Window,内部视图由两种方式指定,系统默认和自定义 View,对应内部成员 mNextView

代码:show()

public void show() {
    if (mNextView != null) {
        throw ...
    }
    
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    
    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        ...
    }
}

public void cancel() {
    mTN.hide();
    
    try {
        getService().cancelToast(mContext.getPackgetName(), mTN);
    } catch (RemoteException e) {
        ...
    }
}

TN 是一个 Binder 类,在 Toast 与 NMS 进行 IPC 过程中,当 NMS 处理 Toast 的显示或隐藏请求时会跨进程回调 TN 中的方法,这个时候由于 TN 运行在 Binder 线程池中,所以需要通过 Handler 将其切换到当前线程中。这里的当前线程是指发送 Toast 请求所在的线程。所以意味着,Toast 无法在没有 Looper 的线程中弹出。

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
    service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
    ...
}

NMS 的 enqueueToast 方法首先将 Toast 请求封装为 ToastRecord 对象并将其添加到一个名为 mToastQueue 的 ArrayList 队列中。对于非系统应用则最多存在50个 ToastRecord,防止 DOS(Denial of Service)。否则通过大量的循环去连续弹出 Toast,会导致其他应用没有机会弹出 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++;
            if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                return;
            }
        }
    }
}

当一个 ToastRecord 添加到 mToastQueue 后,NMS 立即调用 showNextToastLocked 显示当前 Toast。最终显示是 ToastRecord 的 callback 完成的。这个 callback 实际上就是 Toast 中的 TN 对象的远程 Binder,通过 callback 来访问 TN 中的方法是需要跨进程来完成的,最终被调用的 TN 中的方法会运行在发起 Toast 请求的应用的 Binder 线程池中。

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        try {
            record.callback.show();
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
            ... 
            // show 失败就移除该 ToastRecord,继续下一个
        }
    }
}

Toast 显示,NMS 调用了 scheduleTimeoutLocked,发送一个延时消息,即 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);
}

Toast 的隐藏也是通过 ToastRecord 的 callback 来完成的,同样是一次 IPC 过程:

try {
    record.callback.hide();
} catch (RemoteException e) {
    // empty
}

TN 的 show 和 hide 两个方法:

public void show() {
    mHandler.post(mShow);
}

public void hide() {
    mHandler.post(mHide);
}

mShow 和 mHide 是两个 Runnable,内部调用了 TN 的 handleShow 和 handleHide 方法:

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

推荐阅读更多精彩内容