前言
上篇文章中讲到, Android中所有视图都是通过Window来呈现的, 如Activity, Dialog, Toast等, 本篇文章分别分析下Activity, Dialog, Toast中Window的创建过程.
Activity中Window的创建
通过startActivity启动Activity, 从该入口追溯源码到ActivityThread的performLaunchActivity(), 这个方法会创建Activity对象, 并调用其attach方法, 下面看Activity中attach方法.
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,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//创建Window
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
......
//设置WindowManager
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();
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
}
前面提到Window是一个抽象类, 这里可以看出它的具体实现是PhoneWidow.
attach方法中创建了Window对象, 并为其设置一些回调接口, 以及WindowManager.
那么Activity的视图是怎么添加到Window中的呢?我们知道Activity是通过setContentView设置布局文件的, 下面看下setContentView的具体实现.
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
getWindow()即attach方法中创建的mWindow, 通过代码可以看出, Activity将具体操作交给了Window, 而Window的具体实现是PhoneWindow, 下面看下PhoneWindow中的setContentView方法.
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor(); // 初始化DecorView及mContentParent
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();//移除所有View
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);//转场动画
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);//把要加载的布局填充到mContentParent中
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();//回调
}
mContentParentExplicitlySet = true;
}
该过程大致分以下步骤:
1. 创建DecorView.
DecorView是Activity的顶级View, 是一个FrameLayout, 一般包括标题栏和内容栏, 会根据设置的主题有所改变.
private void installDecor() {
mForceDecorInstall = false;
//创建DecorView
if (mDecor == null) {
mDecor = generateDecor(-1);
......
} else {
mDecor.setWindow(this);
}
//创建mContentParent
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
......
} else {
//设置标题
mTitleView = findViewById(R.id.title);
......
}
//转场动画相关属性
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
......
}
}
}
protected DecorView generateDecor(int featureId) {
......
return new DecorView(context, featureId, this, getAttributes());
}
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
//......获取样式的具体信息
int layoutResource;
int features = getLocalFeatures();
//......根据features获取对应的布局layoutResource
mDecor.startChanging();
//将对应的布局layoutResource加载到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//找到布局中id为R.id.content的容器
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
......
return contentParent;
}
generateDecor创建了一个DecorView, generateLayout根据主题获取对应到的布局, 并加载到DecorView中, 然后返回布局文件中id为R.id.content的容器, 及mContentParent.
2. 把Activity要加载的布局添加到mContentParent中.
mLayoutInflater.inflate(layoutResID, mContentParent);
上一步已经分析过, mContentParent是DecorView中的内容栏, 这一步是将Activity的视图添加到DecorView的内容栏.
这里DecorView的结构就跟清晰了, DecorView包括标题栏和内容栏, 我们通过Activity的setContentView就是将Activity的视图添加到了DecorView的内容栏.
3. 回调onContentChanged通知Activity视图内容发生了改变
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
上面Activity在attach方法中有mWindow.setCallback(this), 而Activity实现了Window的Callback接口, 到此为止, 已经将Activity的视图添加到了DecorView内容栏中, 并通过onContentChanged接口通知Activity视图已经发生改变.
注意setContentView方法只是将Activity的视图添加到DecorView的内容栏中, 那么什么时候添加到Window中显示呢?
上面讲到Activity的启动之后最终会到ActivityThread的handleLaunchActivity方法, 该方法首先调用performLaunchActivity(创建Activity对象, 并调用其attach方法), 然后调用handleResumeActivity, 该方法会调用Activity的makeVisible方法, 如下所示.
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
这里才真正将DecorView添加到Window中显示, Activity视图才真正被用户看到.
总结一下Activity启动之后Window的创建及显示过程:
- 在attach方法中创建Window并设置回调;
- setContentView时创建Decor, 并把Activity的视图添加到DecorView的内容栏;
- makeVisible时将DecorView添加到WIndow中显示.
到这里, Activity, Window, DecorView之间的层级关系已经跟清楚了, 如下图所示:
2. Dialog中Window的创建
Dialog的使用很简单, new Dialog创建对象, 设置布局, 标题等 , 最后通过show方法显示.
首先看下Dialog构造函数.
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
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.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
可以看出, Dialog构造函数中主要是初始化Context, 创建Window对象, 并为其设置一些回调, 这一点和Activity中Window的创建类似.
然后看下Dialog的setContentView方法.
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
这个过程和Activity一样, 通过Window添加指定的布局文件, 具体细节也是先创建DecorView, 然后把要加载的布局添加到DecorView的内容栏中.
然后看show方法.
public void show() {
......
mDecor = mWindow.getDecorView();
......
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}
这里通过WindowManager把DecorView添加到Window中.
Toast的Window创建和Activity的Window创建一样, 大致分为三步:
- 在构造函数中创建Window对象并设置回调;
- setContentView时通过Window添加布局(创建DecorView, 然后把Dialog的布局添加到DecorView的内容栏);
- show时将DecorView添加到Window中显示.
3. Toast中Window的创建
Toast的简单使用:
Toast.makeText(this, "This is a Toast!", Toast.LENGTH_SHORT).show();
首先看makeText的代码
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//创建Toast对象
Toast result = new Toast(context, looper);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//系统默认显示的视图
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
//mNextView表设计要显示的视图, mDuration表示显示时长
result.mNextView = v;
result.mDuration = duration;
return result;
}
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
makeText比较简单, 创建了Toast对象, 然后给Toast对象指定一个系统默认的视图, 并设置消息内容及消息超时时长.
Toast构造函数中创建了一个TN对象, 并设置了位置, 看下TN的代码.
private static class TN extends ITransientNotification.Stub {
......
TN(String packageName, @Nullable Looper looper) {
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) {
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
mNextView = null;
break;
}
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
@Override
public void show(IBinder windowToken) {
......
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
@Override
public void hide() {
.....
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
......
mHandler.obtainMessage(CANCEL).sendToTarget();
}
}
TN继承了ITransientNotification.Stub, 是一个Binder类, 它的构造函数中主要是初始化布局参数, 包名以及looper和handler, handler中处理了Toast的show, hide, cancel操作, 可见Toast的操作主要是通过TN来实现的.
因为TN运行在Binder线程池中, 所以需要使用Handler来切换线程, Handler消息处理机制戳这里.
接下来看show方法.
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
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
首先获取INotificationManager, 然后调用其enqueueToast方法, INotificationManager是一个接口, 其实现类是NotificationManagerService,
它是系统为了管理各种App的Notification(包括Toast)的服务, 由这个服务来统一维护一个待展示Toast队列, 各App需要弹Toast的时候就将相关信息发送给这个服务, 服务会将其加入队列, 然后根据队列的情况, 依次通知各App展示和隐藏Toast.
下面看NotificationManagerService的enqueueToast方法.
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
......
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
//mToastQueue中已经存在该Toast, 只更新它的显示时长
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
//如果是非系统应用, 一个应用内部最多只能容纳50个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++;
//如果超过50, 直接返回, 不处理当前申请加入的Toast
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
//封装一个ToastRecord添加到mToastQueue中
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
//显示当前Toast
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
首先判断mToastQueue中是否有当前要显示的Toast, 有则只更新显示时长, 无则构建一个ToastRecord对象添加到mToastQueue中.
对于非系统应用, mToastQueue最多只能容纳50个ToastRecord, 如果超过50, 当前申请加入的Toast直接抛弃不处理.
这里给该Token的Window设置了type为TYPE_TOAST, 说明Toast属于系统Window.
为什么只有在index=0时才调用showNextToastLocked()显示当前Toast呢? index=0说明mToastQueue中有且只有一个Toast或者有多个, 但是当前要显示的位于列表中第一个, 这时调用该方法显示该Toast. 如果mToastQueue中有多个Toast, 当前要显示的Toast不是第一个时就不调用该方法显示了吗? 看了后面的代码就清楚了.
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
......
try {
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
......
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
showNextToastLocked方法里先取出mToastQueue中第一个ToastRecord, 通过callback的show显示, 这里的callback就是前面创建Toast时创建的TN. NotificationManagerService运行在系统进程, 所以要通过TN这个Binder类跨进程显示Toast.
然后调用scheduleTimeoutLocked处理超时逻辑.
上面分析过TN的show方法, 是发送了一个SHOW消息, 然后看下Handler中对应的处理方法handleShow.
public void handleShow(IBinder windowToken) {
......
if (mView != mNextView) {
//View发生改变, 先隐藏之前的Toast, 再重新设置view
handleHide();
mView = mNextView;
//......设置Context, 包名, WindowManager, 以及布局参数
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
这里完成了Toast视图添加到Window中显示的过程.
我们知道Toast显示几秒之后会自动消失, 下面看下NotificationManagerService中超时自动隐藏Toast的逻辑scheduleTimeoutLocked.
@GuardedBy("mToastQueue")
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);
}
这里发送了一个MESSAGE_TIMEOUT消息, 并设置延迟时间, LONG_DELAY为3.5s, SHORT_DELAY为2s. 看下MESSAGE_TIMEOUT的处理.
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);
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();
}
}
延迟相应时间之后, NotificationManagerService通过cancelToastLocked方法隐藏Toast, 首先通过TN隐藏Toast, 然后将其从mToastQueue列表中移除, 如果mToastQueue列表中还有其他Toast, 就继续显示其他Toast, 这里就解释了前面的问题, 处理完mToastQueue列表中的第一个Toast并隐藏之后, 会继续显示列表中剩余Toast.
TN的hide方法是发送了一个HIDE消息, 看下TN的Handler中对应的处理方法handleHide.
public void handleHide() {
......
if (mView != null) {
if (mView.getParent() != null) {
......
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
handleHide就是将Toast视图从Window中移除.
简单总结一下Toast中Window的创建过程.
- makeText时创建Toast对象, TN对象, 并设置要显示的视图, 消息及延时时长.
- show时, 跨进程到NotificationManagerService中, 构建ToastRecord对象加入mToastQueue列表中.
- NotificationManagerService依次取出mToastQueue列表中第一条Toast, 跨进程回调Toast中的TN进行显示, NotificationManagerService延迟相应时间之后, 跨进程到Toast中的TN进行隐藏, 然后将其从mToastQueue列表中移除.
- TN中的show和hide其实就是Window的添加和移除.