源码分析:Window的创建过程

前言

View是Android中视图的呈现方式,但是View不能单独存在,它必须依附在Window上。因此有视图的地方就有Window,像Activity、Dialog、Toast等视图都对应着一个Window。接下来将分析这些Window的创建过程,加深对Window的理解。

Activity的Window创建过程

在Activity的启动过程中,最终会调用到ActivityThread的performLaunchActivity方法,在该方法内部会调用Activity的attach方法来初始化一些重要数据:

1
2
3
4
5
6
7
8
//...

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

//...

Activity#attach

在Activity的attach方法中,系统会创建Activity所属的Window对象并为其设置回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) {
//...

//创建PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
//给Window设置回调接口
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);

//...

}

Window创建完成后,接下来Activity的视图是怎样依附在Window上的,由于Activity的视图由setContentView提供,所以从Activity的setContentView开始看:

Activity#setContentView

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

可以看到,Activity将具体实现交给了PhoneWindow,接下来看PhoneWindow的setContentView方法。

PhoneWindow#setContentView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void setContentView(int layoutResID) {

//mContentParent是一个ViewGroup,用来存放DecorView的内容
if (mContentParent == null) {
installDecor();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//...
} else {
//将Activity的视图添加到DecorView的mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}

final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调onContentChanged方法,通知Acticity做相应处理
cb.onContentChanged();
}

//...
}

如果没有DecorView的话,要先创建DecorView,该过程在installDecor方法中。创建好DecorView后,再将Activity的视图添加到DecorView的mContentParent中。最后通过接口回调,通知Activity做相应处理。下面看一下installDecor方法:

PhoneWindow#installDecor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void installDecor() {

if (mDecor == null) {
mDecor = generateDecor(-1); //创建DecorView

//...
} else {
mDecor.setWindow(this);
}

if (mContentParent == null) {
//加载具体的布局文件到DecorView中
mContentParent = generateLayout(mDecor);

//...
}
}
}

在该方法中,如果DecorView为空,会先创建DecorView。创建完DecorView后,还要加载具体的布局文件到DecorView中,该过程通过generateLayout方法完成:

PhoneWindow#generateLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected ViewGroup generateLayout(DecorView decor) {

//...

mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

//ID_ANDROID_CONTENT对应的是com.android.internal.R.id.content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

//...

return contentParent;
}

可以看到,mContentParent所对应的id为com.android.internal.R.id.content。

到这里为止,DecorView已经被创建并初始化完毕,Activity的布局文件也已经成功添加到了DecorView的mContentParent中,但是这时候DecorView还没有正式添加到Window中。

前面的步骤发生在Activity启动过程的performLaunchActivity方法中,在执行完该方法后,还会执行handleResumeActivity方法,在该方法中会调用Activity的onResume方法,接着调用Activity的makeVisible方法。

Activity#makeVisible

1
2
3
4
5
6
7
8
9
10
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
//添加DecorView到Window中
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
//显示DecorView
mDecor.setVisibility(View.VISIBLE);
}

在该方法中,DecorView添加到Window并显示出来,到这里Activity的视图才能被用户看到。

小结

在Activity的启动过程中,执行到Activity的attach方法时,系统会创建Activity所属的Window对象并为其设置回调接口。Window创建完成后,还要将Activity的视图依附在Window上,这个过程由Activity的setContentView方法开始,而Activity将具体实现交给PhoneWindow来完成。PhoneWindow做了这几件事:

  1. 没有DecorView的话先创建DecorView
  2. 将Activity的视图添加到DecorView的mContentParent中
  3. 通过接口回调,通知Activity做相应处理

最后,还要将DecorView添加到Window中。该过程发生在handleResumeActivity方法中,该方法在调用了Activity的onResume方法后,会接着调用Activity的makeVisible方法,这时DecorView才被添加到Window中并显示出来。

Dialog的Window创建过程

创建Window

在Dialog的构造方法中,将会创建Window并设置相关接口回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Dialog(@NonNull Context context, @StyleRes int themeResId, 
boolean createContextThemeWrapper) {
//...

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

初始化DecorView并将Dialog的视图添加到DecorView中

该过程和Activity一样,也是在setContentView方法中交由PhoneWindow来实现:

1
2
3
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}

将DevorView添加到Window中并显示

在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中,并显示Dialog。

1
2
3
4
5
6
7
8
public void show() {
//...

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

//...
}

小结

首先在Dialog的构造方法中,创建Window并设置相关接口回调。然后和Activity一样,在setContentView方法中交由PhoneWindow来处理,PhoneWindow做了这几件事:

  1. 没有DecorView的话先创建DecorView
  2. 将Dialog的视图添加到DecorView的mContentParent中
  3. 通过接口回调,通知Dialog做相应处理

最后,在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中,并显示Dialog。

Toast的Window创建过程

Toast和Dialog、Activity不同,它的工作过程稍显复杂。在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService(以下简称NMS),第二类是NMS回调Toast中的TN。

接下来以Toast的显示为例,Toast的显示用到show方法:

Toast#show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}

//得到NotificationManagerService中的INotificationManager实例
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();

//TN是一个静态内部类,它是一个Binder类
TN tn = mTN;
tn.mNextView = mNextView;

try {
//跨进程调用NMS中的enqueueToast方法
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

由于NMS运行在系统进程中,所以必须使用跨进程的方式来显示和隐藏Toast。接下来看一下NMS的enqueueToast方法

NotificationManagerService#enqueueToast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private final IBinder mService = new INotificationManager.Stub() {

@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
//...

synchronized (mToastQueue) {

try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);

//该Toast已经存在于队列中,更新它
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
}
else {
//对于非系统应用,限制最多能同时存在50个Toast

//...

//将Toast请求封装为ToastRecord,并添加到mToastQueue队列中
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}

//如果当前Toast位于队头,显示当前Toast
if (index == 0) {
showNextToastLocked();
}
}
}
}

//...
}

enqueueToast方法的步骤:先判断当前Toast是否存在于队列,如果存在,直接更新该Toast;如果不存在,对于非系统应用,限制最多能同时存在50个Toast,多了之后不再加入队列,没问题的话就将Toast请求封装为ToastRecord并添加到队列中。最后,如果当前Toast位于队头,就直接显示它。

接着看下showNextToastLocked方法:

NotificationManagerService#showNextToastLocked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
//record.callback就是Toast中的TN对象
record.callback.show(record.token);
//发送一个延时消息
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
//...
}
}
}

在该方法中,首先通过ToastRecord的callback来跨进程访问TN中的方法来显示Toast,TN中的方法会运行在发起该Toast请求的应用的Binder线程池中。

Toast/TN#show

1
2
3
4
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

由于该方法运行在Binder线程池中,所以需要通过Handler回到主线程再执行显示操作。由于Toast需要用到Handler,所以Toast无法在没有Looper的线程中使用。因为Handler需要使用Looper才能完成切换线程的功能。

Handler在处理这条消息时,调用handleShow方法:

Toast/TN#handleShow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void handleShow(IBinder windowToken) {
//...

if (mView != mNextView) {
mView = mNextView;
//...

mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

//...

mWM.addView(mView, mParams);
}
}

可以看到,这里通过WindowManager的addView方法将Toast的视图添加到Window上。

Toast在显示完消息后,过一段时间就会消失,该操作在NMS的scheduleTimeoutLocked方法中:

NotificationManagerService#scheduleTimeoutLocked

1
2
3
4
5
6
7
8
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);
}

该方法在延迟相应的时间后,通过Handler发送消息,该消息调用handleTimeout方法:

NotificationManagerService#handleTimeout

1
2
3
4
5
6
7
8
9
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}

继续调用cancelToastLocked方法:

NotificationManagerService#cancelToastLocked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
//调用TN的hide方法隐藏Toast
record.callback.hide();
} //...

//从队列中移除该Toast
ToastRecord lastToast = mToastQueue.remove(index);

//...

//继续显示下一条Toast
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}

该方法调用TN的hide方法隐藏该Toast并从队列中移除该Toast,然后如果队列中还有Toast,就继续显示下一条Toast。

小结

以Toast的显示为例,在Toast的show方法中,跨进程调用NMS的enqueueToast方法。在该方法中:先判断当前Toast是否存在于队列,如果存在,直接更新该Toast;如果不存在,对于非系统应用,限制最多能同时存在50个Toast,多了之后不再加入队列,没问题的话就将Toast请求封装为ToastRecord后添加到队列中。最后,如果当前Toast位于队头,就直接显示它。在显示Toast的时候,通过ToastRecord的callback跨进程回调TN中的show方法。TN通过Handler切回到主线程后,通过WindowManager的addView方法将Toast的视图添加到Window上。

Toast在显示完消息后,过一段时间就会消失,该操作在NMS的scheduleTimeoutLocked方法中。该方法在延迟相应的时间后,通过Handler切回主线程,回调TN的hide方法隐藏该Toast并从队列中移除该Toast,然后如果队列中还有Toast,就继续显示下一条Toast。

参考

  • 《Android 开发艺术探索》
-------------    本文到此结束  感谢您的阅读    -------------
0%