Android中的Activity的显示原理会经历一连串的调用过程,通过了解这个流程,许多平时开发中遇到的问题的真正原理也就迎刃而解了。下面我们就一起探寻一下这显示背后的秘密吧。
直观的来讲,我们实现一个Activity的时候都会在onCreate()
方法中调用super.onCreate(),然后紧接着调用一句setContentView(R.layout.activity_XXX)
对吧。大家都知道我们调用这一句是将定制的Activity的布局页面xml加载进入Activity中,最终能够呈现。但是,setContentView到底做了什么,才能够最终显示出来呢。这就是我们今天这里想要探索的重点内容。下面我们先提出一些问题:
- Activity的显示过程中都经历了哪些重要步骤?
- Activity的显示过程都有哪些重量级的类参与其中?
- Activity的显示过程中,Window、DecorView、ViewRootImpl是如何各司其职的?
首先,大家都至少有所耳闻,Activity、Window、DecorView之间的大致的位置关系吧,这里贴一张自制的图。
记住这个空间关系,我们就从setContentView()开始吧。
setContentView中直接调用了window对象的setContentView()
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);//直接拿window操作。
initWindowDecorActionBar();
}
那么getWindow()
做了什么呢?不用纠结了,Activity中持有的变量mWindow,我们只要记住它的初始化赋值在ActivityThread
中,而且时机是Activity被attach到Application的时候,将Window的子类PhoneWindow
传入的。这个咱们以后在探索Activity启动原理和流程
的时候再聊。
追踪到PhoneWindow中,我们看到了setContentView()方法内容。在这个过程中,主要经历了两个步骤:1. 建立DecorView,并得到大名鼎鼎的android.R.id.content对象:mContentParent。2. 通过LayoutInflater实例化R.layout.xxx对象,并关联到一整个以DecorView为根节点的View树结构中
。
//PhoneWindow代码
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();//建立DecorView,得到mContentParent。
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);//LayoutInflate不用多说了吧,实例化View,关联到View树。
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
那我们先看installDecor()做了什么。我们发现重要的赋值和初始化其实都集中在方法上半部:如果首次执行,则decorView是空,走generateDecor()
。自此,Window和DecorView这两大角色历史性的关联就完成了
。 然后调用generateLayout()
,得到我们大名鼎鼎的contentParent:android.R.id.content对象
。
//PhoneWindow代码
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);//1. 实例化DecorView
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//2. 得到contentParent。
//省略...
} else {
//省略...
}
//省略...
}
}
//PhoneWindow代码
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());//实例化全新的DecorView,并将自身(也就是PhoneWindow)绑定到DecorView中去。
}
//PhoneWindow代码
protected ViewGroup generateLayout(DecorView decor) {
//省略...
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
//省略...
mDecor.finishChanging();
return contentParent;//返回android.R.id.content
}
好了,到这里我们发现,phoneWindow.setContentView()方法执行完了。那后面咋办呢?什么时候开始渲染啊?什么时候能够展示啊?
别急,我们仔细回忆一下,Activity的生命周期有onCreate、onStart、onResume等几个重要生命周期回调对吧。onStart只能说明Activity处于前台,但并不可见,更不能交互。真正可见且能交互的时候是在onResume回调之后
,请注意,这里说的是之后哦!那我们就直接来看onResume()和之后都做了什么吧。去哪里看呢?显然,对于四大组件生命周期的执行几乎都在ActivityThread
中调用的。
ActivityThread 代码
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
//省略...
r = performResumeActivity(token, clearHide, reason); //1. 调用activity的onResume
if (r != null) {
final Activity a = r.activity;
//省略...
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE); //2. 将decorView设置为不可见
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
//省略...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l); //3. WindowManagerImpl 添加View。
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(
TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
if (r.newConfig != null) {
performConfigurationChangedForActivity(r, r.newConfig);
r.newConfig = null;
}
WindowManager.LayoutParams l = r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode = (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible(); //3.最终令activity内容可见
}
}
if (!r.onlyLocalRequest) {
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(
TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
}
r.onlyLocalRequest = false;
// Tell the activity manager we have resumed.
if (reallyResume) {
try {
ActivityManager.getService().activityResumed(token);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
try {
ActivityManager.getService()
.finishActivity(token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
结论很明显了吧,handleResumeActivity阶段,1. performResumeActivity()
,执行对应Activity的onResume()方法。2. decor.setVisible(INVISIBLE)
,将DecorView设置为不可见状态。3. wm.addView(decor, l)
,将通过WindowManager(WindowManagerImpl),将DecorView与ViewRootImpl进行关联绑定。这一步就是将DecorView纳入到了ViewRootImpl的管控当中了
。 4. r.activity.makeVisible();
,最后调用activity的makeVisible()
,调用mDecor.setVisibility(View.VISIBLE);
,得以显示。
//WindowManagerGlobal代码
//3. wm.addView(decor, l),实例化ViewRootImpl,使其与DecorView关联,并记录于WindowManagerGlobal。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//省略...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//省略...
root = new ViewRootImpl(view.getContext(), display);//3.1 进行了ViewRootImpl的实例化
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);//3.2. 将DecorView与ViewRootImpl绑定。
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
走到这一步之后,ViewRootImpl.setView()就会通过调用requestLayout()
方法触发View从根到叶的绘制操作了。
//ViewRootImpl 代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
//省略。。。
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();//3.2.1 触发View的绘制流程
//省略。。。
}
}
}
好了,走到这里,基于Activity的显示流程就完整了。我们来回顾一下:
- Activity的显示流程起始于
setContentView()
,调用的是PhoneWindow的setContentView(),建立DecorView实例并将layout资源文件实例化并绑定到decorview中。 - 经过AMS(ActivityManagerService)调度,流程来到ActivityThread中的
handleResumeActivity()
,调用onResume()
,然后设置DecorView为不可见INVISIBLE,并通过WindowManagerGlobal.addView(decor,l)
,实例化ViewRootImpl,关联DecorView,建立了WindowManagerGlobal对ViewRootImpl的管控。最后,调用activity.makeVisible()
,令DecorView为可见状态。
最后,我们能够回答最开始的问题了:
-
Activity的显示过程中都经历了哪些重要步骤? Activity.setContentView()->PhoneWindow.setContentView(){1. 实例化DecorView 2. 得到mParentContent 3. 实例化layout资源文件,关联到DecorView树中 } ActivityThread.handleResumeActivity() {1. Activity.onResume() 2. DecorView设置不可见 3. WindowManagerGlobal.setView()实例化ViewRootImpl,关联ViewRootImpl与DecorView,调用ViewRootImpl启动绘制流程。 4. 调用Activity.makeVisible()令DecorView可见。}
-
Activity的显示过程都有哪些重量级的类参与其中? 按照基本出场顺序:Activity、PhoneWindow、DecorView、ActivityThread、WindowManagerGlobal、ViewRootImpl。如果在往后续流程说,还会涉及到Choreographer等等。
-
Activity的显示过程中,Window、DecorView、ViewRootImpl是如何各司其职的? 这些就不用多说了,上面的流程分析已经说得比较明确了。
最后的最后,我们开始说道,了解了以上的流程信息,平时开发中遇到的疑难杂症也就迎刃而解了,具体有哪些呢?下面就举几个例子吧:
-
为什么在onResume回调中无法获得某个View的measuredHeight和measuredWidth? 因为View的真正启动绘制的流程起始于ViewRootImpl的requestLayout(),这个时机是在ActivityThread调用Activity.onResume()之后。因此,在onResume中的时候,View都还没有开始绘制。所以无法获得宽高。
-
深一步想,如果我真的需要在onResume的时机去获取View的宽高,应该如何处理? a. 使用View.post() b. 自定义Handler.post()
-
子线程真的不能更新UI吗? 如上述分析可知,我们在ViewRootImpl.requestLayout()方法中,首先调用的就是检查线程checkThread()。若报错,则报错内容为
"Only the original thread that created a view hierarchy can touch its views."
,原意其实是说,只有创建了View树的线程才能更新对应的View,并没有说只有主线程才能更新View。什么意思呢?其实就是说,通过只有通过ViewRootImpl.setView()绑定View树的线程,才能够更新这些View。所以退一步讲,若ViewRootImpl实例并没有跑在主线程上而是某个子线程thread#1,那么只要调用方也在thread#1,就能够更新该UI。具体看下代码:
ViewRootImpl代码中:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
mThread变量是ViewRootImpl在构造器中被赋值的mThread = Thread.currentThread();
。再看检测线程的逻辑,是不是就全明白了?那既然这样,为什么我们一般会说,只有主线程才能更新UI呢?这个说法也没有错,因为几乎我们所有的View都最终会依赖Activity去承载呈现,而Activity伴随着ActivityThread的调度,他们都是直接运行在主线程的,因此在绝对大多数的情况下,我们会说只有主线程才能更新UI
这句话也是没错的。