理解Window和WindowManager(二)三种window的创建过程

我们知道view是安卓中视图的呈现方式,但是view不能单独存在,他必须依附在window这个抽象概念上,因此有视图的地方就有window。
我们都清楚安卓提供视图的地方有Activity,Dialog,Toast,还有一些依托window而实现的视图如popUpWindow等。其实这些就对应三种window类型,应用window、子系统、系统window。接下来便总结下Activity,Dialog,Toast的对应window创建流程。

要点

在这里插入图片描述

一、Activity的Window创建过程

1、Activity window的创建

要分析Activity中的Window创建过程,必须了解activity的启动过程,这里我们大概了解下即可。
Activty的启动最终会在ActivtyThread的performLaunchActivty()中完成(参看下面源码)

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // .....
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;
    try {
        // 获取类加载器
        java.lang.ClassLoader cl = appContext.getClassLoader();
        // 反射获取一个Activty实例
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        // ......
    try {
        // ......
        if (activity != null) {
            // ......
            // 调用attach()给activty关联变量
            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, r.configCallback);
            // ......
    return activity;
}

通过源码我们知道:
1、通过获得类加载器来获得activity的实例
2、调用activity的attach方法为其关联运行过程中依赖的上下文变量
继续深入attach()源码:

final void attach(/*一系列的参数,太长了......*/) {
    // ......
    
    // 创建Window对象 ,PolicyManager为策略类,其实现类Policy 的makeNewWindow内部创建了window对象
    mWindow = PolicyManager.makeNewWindow(this)
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    
    // ......初始化Window中的部分内容
    // ......赋值各种参数
    
 ;
}

Policy 的makeNewWindow:

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

通过attach源码我们可以知道:
1、 PolicyManager.makeNewWindow(this)主要功能创建window对象
2、PolicyManager为策略类,其实现类Policy 的makeNewWindow内部创建了window(PhoneWindow)对象
ps:PhoneWindow为window的唯一实现类

2、activity被附属在window过程

window被创建成功后就是activity被加载到window的过程了,activity的视图从setContentView开始

 public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    ....
 public Window getWindow() {
        return mWindow;// 这个window就是PhoneWindow对象
    }
...

 mWindow = new PhoneWindow(this, window, activityConfigCallback);

可以知道具体的实现类是由PhoneWindow来实现的,所以看setContentView源码:

@Override
public void setContentView(int layoutResID) {
    // 初始化DecorView
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // 如果已经创建过DecorView结构,已经有了content,就移除
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // ......
    } else {
        // mContentParent中加载传入的布局资源
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    // ......
    if (cb != null && !isDestroyed()) {
        // 回调通知Activity视图已经改变
        cb.onContentChanged();
    }
    // ......
}

installDecor的源码:

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        // 如果还没有DecorView,调用generateDecor()创建
        mDecor = generateDecor(-1);
        // ......
    } else {
        // 如果有DecorView对象了,就关联此Window
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        // 赋值mContentParent
        mContentParent = generateLayout(mDecor);
        
        // 初始化布局......
        
    }
}

generateLayout 源码:

protected ViewGroup generateLayout(DecorView decor) {
    
    // 根据Window的R.styleable等参数设置Window的flag、params、layoutResource......
    
    mDecor.startChanging();
    // DecorView加载布局
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    
    // ID_ANDROID_CONTENT = com.android.internal.R.id.content
    // setContentView()布局加载的ViewGroup
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    // ......
    
    mDecor.finishChanging();
    return contentParent;
}

1、通过installDecor来创建DecorView对象(内部调用generateDecor方法完成)
2、phoneWindow通过generateLayout加载具体的布局到DecorView(generateLayout代码)
3、ID_ANDROID_CONTENT这个id就是contentParent容器的id
4、decorview创建好后就是将view添加到decorview的contentParent容器中(phoneWindow的setContentView源码:mLayoutInflater.inflate(layoutResID, mContentParent);)
5、回调activity的onContentChanged通知activity视图改变(phoneWindow的setContentView源码中)

补充:
DecorView是一个FrameLayout,activity中的顶级view(参考下图理解)
DecorView包括标题栏和内容栏,标题栏受安卓系统和主题不同而不同,内容栏就是我们setContentView设置view添加之处。且内容栏是个容器,有固定的id:R.android.id.content

在这里插入图片描述

经过上面的几步后decorview已经被创建完毕,activity的布局文件也被加载到Decorview的contentParent容器中。但是这个时候decorview还没被 WindowManager正式添加到window中。
虽然前文中说到 在activity的attach方法中 window被创建。这时候Decorview还没被WindowManager识别。
等activityThread的handleResumeActivty方法中进行处理后用户才可以被用户看到view(参看源码)

activityThread的handleResumeActivty方法

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        // 调用Activity的makeVisible()
        r.activity.makeVisible();
        // ......
}

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

1、添加到window
2、设置view可见

二、Dialog的Window创建过程

1、window的创建

首先看Dialog构造

...
   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);
2、初始化Decorview并将Dialog的视图加载到DecorView中
public void setContentView(@LayoutRes int layoutResID) {
    mWindow.setContentView(layoutResID);
}
3、将DecorView添加到window中并显示(这个操作是在Dialog的show方法中完成的。)
public void show() {
    // ......
    // 监听通知
    onStart();
    mDecor = mWindow.getDecorView();
    // ......
    WindowManager.LayoutParams l = mWindow.getAttributes();
    // ......
    // 添加Window
    mWindowManager.addView(mDecor, l);
    mShowing = true;
    sendShowMessage();
}
4、window的移除(这个操作在Dialog的dismiss中完成)
@Override
public void dismiss() {
    if (Looper.myLooper() == mHandler.getLooper()) {
        dismissDialog();
    } else {
        mHandler.post(mDismissAction);
    }
}

void dismissDialog() {
    if (mDecor == null || !mShowing) {
        return;
    }
    if (mWindow.isDestroyed()) {
        Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
        return;
    }
    try {
        // 移除Window
        mWindowManager.removeViewImmediate(mDecor);
    } finally {
        if (mActionMode != null) {
            mActionMode.finish();
        }
        // 置空回收
        mDecor = null;
        mWindow.closeAllPanels();
        onStop();
        mShowing = false;
        sendDismissMessage();
    }
}

上面的前三步思路是不是和activity的window创建过程很像。这里注意:
普通的Dialog有个特殊指出就是context必须是Activity的context如果采用application的context就会报错。原因是没有应用token所导致的。而应用token一般只有activity拥有,所以只需要使用activity来作为context显示对话框即可。
另外系统window比较特殊,不需要token,所以我们为对话框指定window类型为系统window类型也可以正常显示对话框。但是要加权限哦。如下:

// 代码中:
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR);
// 清单文件:
  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

三、Toast的Window创建过程

1、Toast属于系统Window
2、Toast有定是取消的功能,所以使用了Handler
3、内部会有两类IPC过程:

  • Toast访问NotificationManagerService(简称NMS)的过程
  • NotificationManagerService回调Toast里的TN接口

我们平时创建吐司十分简单

 Toast.makeText(this, "xxx", Toast.LENGTH_SHORT).show();
1、首先看下吐司的显示隐藏方法(show#cancel)
   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 {
         // 调用NMS中的enqueueToast  参数 包名,TN回调,显示时长
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

   public void cancel() {
        mTN.cancel();
    }

1、从上面的代码看显示隐藏都需要NMS来实现,由于NMS运行在系统进程中所以只能通过远程调用的方式显示隐藏吐司。
2、注意TN这个类他是一个Binder类,在Toast和NMS进行IPC过程中当NMS处理Toast显示或者隐藏的请求时会跨进程回调TN中的方法。这时由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程(当前线程:发送Toast请求所在线程)
3、由于使用了Handler所以意味着Toast无法在没有Looper的线程中弹出(Handler使用Looper才能完成线程切换)

2、吐司的显示 show#enqueueToast方法
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
    // ......
    synchronized (mToastQueue) {
        // ......
        try {
            // Toast的封装对象
            ToastRecord record;
            int index;
            if (!isSystemToast) {
                // 如果不是系统应用的Toast,就遍历ToastRecord集合。
                // 看是否存在来自于同一个包的Toast。如果存在就返回对应下标,没有就返回-1。
                index = indexOfToastPackageLocked(pkg);
            } else {
                // 如果是系统Toast,除了对比包名还要对比是不是同一个callback,返回下标。
                index = indexOfToastLocked(pkg, callback);
            }
            if (index >= 0) {
                // 如果找到了,就直接跟新内容和时间。
                record = mToastQueue.get(index);
                record.update(duration);
                record.update(callback);
            } else {
                // 创建一个token,显式加入WMS,显式加入就需要显式移除,在后面会看到。
                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                // 如果不存在,就新创一个ToastRecord,并加入集合。
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
            }
            keepProcessAliveIfNeededLocked(callingPid);
            // 如果添加前还没有ToastRecord,就开始显示
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

1、首先把吐司对象封装为ToastRecord 对象
2、ToastRecord 添加到mToastQueue队列
3、NMS通过showNextToastLocked显示吐司(源码如下)

void showNextToastLocked() {
    // 第一个Toast
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            // 调用TN接口的show(),在应用的Binder线程中执行
            record.callback.show(record.token);
            // 发送一个延时消息,取决于Toast显示时长
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
            // ......
            // 处理IPC调用失败,显示失败的情况
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                // 如果显示失败的Record还在队列中就移除
                mToastQueue.remove(index);
            }
            keepProcessAliveIfNeededLocked(record.pid);
            // 如果队列中还有Record,就继续处理下一个
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

1、toast的显示是由ToastRecord的callbak来完成的,调用TN接口的show来完成的
callback其实就是Toast中TN对象的远程binder
2、NMS通过scheduleTimeoutLocked发送延时消息(具体时长取决于Toast时长)
系统时长为3.5秒和2秒两种
3、延时相应的时间后NMS会通过cancelToastLocked来隐藏Toast并把他从队列中移除

3、吐司的隐藏


void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        // 远程调用TN的hide()
        record.callback.hide();
    } catch (RemoteException e) {
        // ......
    }
    // 将这个record从Toast队列中移除
    ToastRecord lastToast = mToastQueue.remove(index);
    // 调用WMS显式移除Window的token,这个token是在enqueueToast()中添加的
    mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
    keepProcessAliveIfNeededLocked(record.pid);
    // 如果Toast队列中还有Record,就调用继续执行下一个
    if (mToastQueue.size() > 0) {
        showNextToastLocked();
    }
}

record.callback.hide();也是record的callback来完成,远程调用TN的hide()。

四、小结

通过磕磕绊绊的总结也明白了一些东西,还有一些东西也不太明白,总体来说收获不少,以后慢慢消化。。。。溜。
参考文章:https://www.jianshu.com/p/1e62a6412db2

The end

本文来自<安卓开发艺术探索>笔记总结

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值