Android中Activity、Dialog、Toast的Window创建过程

Android中Activity、Dialog、Toast的Window创建过程

Android中可以提供视图的地方有Activity、Dialog、Toast,除此之外还有一些依托Window而实现的视图,比如PopUpWindow、菜单,因此Activity、Dialog、Toast等视图都对应着一个Window,本篇将分析这些视图元素中的Window创建过程。

Activity的Window创建过程

activity的启动过程很复杂,最终会由ActivityThread中的performLaunchActivity()来完成整个启动过程,这个方法内部会通过类加载器创建Activity的实例对象,并调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量,代码如下:

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 (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
            + r.activityInfo.name + " with config " + config);
    activity.attach(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对象并为其设置回调接口,window对象的创建是由PolicyManager的makeNewWindow方法实现的。由于activity实现了window的callback方法接口,因此当window接受到外界的状态改变时就会回调activity的方法。callback接口中的方法很多,但是有几个确实我们非常熟悉的,你如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等等:

mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
public interface Callback {
    public boolean dispatchTouchEvent(MotionEvent event);
    public void onWindowFocusChanged(boolean hasFocus);
    public void onAttachedToWindow();
    public void onDetachedFromWindow();
    ...
}

从上面的分析来看,activity的window是通过PolicyManager的一个工厂方法来创建的,但是从PolicyManager的类名来看是一个策略类,PolicyManager中实现的几个工厂方法全部在策略接口IPolicy中声明了,IPolicy定义如下:

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

在实际的调用中,PolicyManager的真正实现是Policy类,mackNewWindow方法来实现如下,由此可以发现,window的具体实现的确是phoneWindow。

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

关于PolicyManager是如何关联到IPolicy中,这个无法从源码中调用关系得到,这里的猜测可能是编译环节动态控制的,到这里window已经创建完成了。

下面分析activity的视图是怎么依附在window上的,由于activity的视图是由setContentView开始的,所有先看下这个方法:

public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

可以看到具体实现交给了Window来处理,所以只需要看PhoneWindow的setContentView相关逻辑即可。它大致遵循如下几个步骤:

1. 如果没有DecorView就创建

DecorView是一个FrameLayout,是activity中的顶级View,一般来说它的内部包含标题栏和内部栏,内容栏是一定要存在的,并且有固定的id就是android.R.id.content。DecorView的创建由installDecor方法来完成,在方法内部会通过generateDecor方法来直接创建DecorView,这个时候它只是一个空白的FrameLayout:

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

为了初始化DecorView的结构,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);

其中ID_ANDROID_CONTENT的定义如下,这个id所对应的ViewGroup就是mContentParent:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

2. 将View添加到DecorView的mContentParent中

这个过程比较简单,由于在第一步的时候已经初始化了DecorView,因此这一部就直接将activity的视图添加到DecorView的mContentParent中既可:

mLayoutInflater.inflate(layoutResID, mContentParent);

3. 回调Activity的onContentChanged方法通知Activity视图已经发生改变

由于Activity实现了window的Callback接口,这里表示布局已经被添加到DecorView的mContentParent中了,于是需要通知activity,使其可以做相应的处理。这个过程的代码如下所示:

final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
    cb.onContentChanged();
}

Activity的onCreateChanged是一个空实现,我们可以在子activity中处理这个回调

4. 将docorView添加到Window中

在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible方法,在该方法中DecorView真正地完成了添加和显示过程,到这里Activity的试图才能被用户看到:

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

流程图如下:

在这里插入图片描述

Dialog的Window创建过程

Dialog的Window创建过程和Activity类似,有如下几个步骤:

1. 创建Window

Dialog(Context context, int theme, boolean createContextThemeWrapper) {
    ...
    mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    Window w = PolicyManager.makeNewWindow(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(int layoutResID) {
    mWindow.setContentView(layoutResID);
}

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

在Dialog的show方法中,会通过windowManager将DecorView添加到window中

mWindowManager.addView(mDecor, l);
mShowing = true;

当Dialog被关闭时,它会通过WindowManager来移除DecorView:mWindowManager.removeViewImmediate(mDecor)

系统Dialog

普通的dialog有一个特殊的地方,那就是必须用activity的context,否则会报没有应用token的错误,而系统Window比较特殊,可以不需要token,只需要指定对话框的Window为系统类型就可以。WindowManager.LayoutParams中的type标识Window的类型,而系统Window的层级范围是2000~2999。

使用TYPE_SYSTEM_OVERLAY来指定对话框的Window类型为系统Window:dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)

在AndroidManifest文件中声明权限<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

Toast的Window创建过程

Toast工作过程稍微复杂一点,由于Toast具有定时取消的功能,所以系统采用了handler,在toast内部有两类IPC的过程,第一类Toast访问NotificationManagerService,第二类NMS回调toast的TN接口

Toast属于系统Window,它内部的视图有两种方式指定,一种是系统默认的样式,另一种是通过setView来指定一个自定义View,它们都对应Toast的一个View类型内部成员mNextView,Toast提供了show和cancel用于显示和隐藏Toast,它们内部都是IPC过程,实现如下:

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通过IPC实现远程调用,需要注意的是TN这个类,它是一个Binder类,当NMS处理Toast的请求时会跨进程回调TN中的方法,由于TN在binder线程池中进行回调,所以需要handler切换到主线程中,所以这就意味着Toast无法再没有Lopper的线程中弹出

首先来看下Toast的显示过程,它调用了NMS中的enqueueToast方法:

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

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

NMS的enqueueToast方法的第一个参数表示当前应用的包名,第二个参数tn表示远程回调,第三个表示Toast时长。enqueueToast会将Toast请求封装为ToastRecord对象并将其添加到一个名为mToastQueue的ArrayList队列中,对于非系统应用来说,该队列最多能同时存在50个ToastRecord(MAX_PACKAGE_NOTIFICATIONS),这样是为了防止dos。

ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
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++;
                if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                    Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg);
                    return;
                }
            }
        }
    }

record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);

当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示当前的Toast,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是TN对象的远程Binder

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        ...
        try {
            record.callback.show(record.token);
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
            ...
        }
    }
}

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

在上面代码中,LONG_DELAY和SHORT_DELAY分别是3.5s和2s。
延时后NMS会通过handler发送消息最终调用cancelToastLocked方法来隐藏Toast,隐藏也是通过ToastRecord的callback来完成一次IPC过程的,最后将其从mToastQueue中移除,如果这时mToastQueue中还有其他Toast则继续显示其他Toast:

void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        record.callback.hide();
    } catch (RemoteException e) {
        ...
    }

    ToastRecord lastToast = mToastQueue.remove(index);
    mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

    keepProcessAliveIfNeededLocked(record.pid);
    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();
    }
}

通过上面的分析,可以知道Toast的显示和隐藏实际上是通过TN这个类的show和hide方法来实现的,由于这两个方法是NMS以跨进程的方式回调的,因此运行在Binder线程池中,为了切换到Toast请求所在线程,在内部使用了handler:

/**
 * schedule handleShow into the right thread
 */
@Override
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(0, windowToken).sendToTarget();
}

/**
 * schedule handleHide into the right thread
 */
@Override
public void hide() {
    if (localLOGV) Log.v(TAG, "HIDE: " + this);
    mHandler.post(mHide);
}

上述的代码,mShow和mHide是两个Runnable,它们内部分别调用了handleShow和handleHide方法,handleShow会将Toast的视图添加到Window中,handleHide会将Toast的视图从Window中移除:

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
mWM.addView(mView, mParams);
if (mView.getParent() != null) {
    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
    mWM.removeViewImmediate(mView);
}

流程图如下:

在这里插入图片描述

发布了174 篇原创文章 · 获赞 119 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/lj402159806/article/details/100937884