刨根问底setContentView
在平时的android开发中,经常会使用到Activity#setContentView方法来设置我们自己的布局,那么setContentView中到底做了什么,我们的布局
是怎么加载并显示到手机屏幕上的,这就是今天要讨论的内容,看下Activity#setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到这里是使用getWindow().setContentView(layoutResID);来进行具体的设置,那么getWindow()又是什么
public Window getWindow() {
return mWindow;
}
这里直接返回mWindow,其实mWindow是在Activity#attach方法中赋值的。
mWindow = new PhoneWindow(this);
根据前面的activity启动流程中知道Activity#attach方法是在activity启动过程中执行的,所以当该activity启动完成以后,mWindow其实就是PhoneWindow
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {//如果我们之前没有设置过布局,或者第一次设置布局
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {//默认为false
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 加载我们设置的布局到mContentParent中去
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
// 可以多次设置布局,当布局发生改变,会回调activity中的方法
final Window.Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
在activity中,我们可以多次设置布局,如果我们之前没有设置过布局,或者第一次设置布局,此时mContentParent就为null,mContentParent是我们设置布局的父布局,是一个ViewGroup,也就是系统中id是”com.android.internal.R.id.content”的ViewGroup
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
hasFeature(FEATURE_CONTENT_TRANSITIONS)默认值为false,通过inflate来加载我们设置的布局
创建DecorView
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(); // 1.创建一个DecorView
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) { // 如果第一次设置布局,mContentParent是我们设置布局的父布局
mContentParent = generateLayout(mDecor);
.......
}
}
将需要显示的布局添加到DecorView中
DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。在该布局下面,有标题view和内容view这两个子元素,而内容view则是上面提到的mContentParent
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
// 获取系统自定义的一些属性
TypedArray a = getWindowStyle();
if (false) {
System.out.println("From style:");
String s = "Attrs:";
for (int i = 0; i < R.styleable.Window.length; i++) {
s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
+ a.getString(i);
}
System.out.println(s);
}
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
// 下面是根据应用层在代码中或者样式文件里设置的requestFeature进行具体的设置,可以看到这里
// requestFeature(FEATURE_NO_TITLE)的设置就是在setContentView中设置的,这也就是为什么
// 我们必须要在setContentView之前设置requestFeature(FEATURE_NO_TITLE)的原因,其实不止这一个,
// 下面的属性都应遵循这样的规则
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
......
// 设置WindowManager窗口显示的一些布局属性
WindowManager.LayoutParams params = getAttributes();
if (!hasSoftInputMode()) {
params.softInputMode = a.getInt(
R.styleable.Window_windowSoftInputMode,
params.softInputMode);
}
if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
/* All dialogs should have the window dimmed */
if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}
.....
mDecor.startChanging();
// 加载设置的布局,并且添加到decor中
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);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
mDecor.finishChanging();
return contentParent;
}
DecorView和Window的关联
到现在位置,我们的布局已经加载到了DecorView中,那么DecorView是怎么和window关联起来的呢,在activity启动流程中,我们知道当activity启动的时候,会执行ActivityThread#handleLaunchActivity方法
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
....
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
}
....
}
final void handleResumeActivity(IBinder token,
....
// 获取当前activity对应的window,并将其和DecorView进行关联
// 这里多说一句,由于
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
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) {
a.mWindowAdded = true;
// 添加当前decor和对应的layoutparams到window中
wm.addView(decor, l);
}
}
.....
}
我们知道Window的实现类是WindowManagerImpl,看下WindowManagerImpl#addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// 每一个window都又一个唯一标识的token,这里如果没有,则设置系统默认的
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
mGlobal是一个WindowManagerGlobal类型对象,最终添加view到窗口显示也是由它来具体操作的
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);
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 {
// 2. 通过ViewRootImpl设置当前DecorView
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
View的绘制流程
继续,看下ViewRootImpl的setView方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
....
requestLayout(); // 核心的调用,requestLayout开始绘制当前DecorView
....
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread(); //检查当前是否是UI线程
mLayoutRequested = true;
scheduleTraversals();
}
}
在requestLayout方法中,又调用了scheduleTraversals,我们继续:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里开启一个TraversalRunnable线程
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals方法很长,实际上主要做了下面三件事情:
private void performTraversals() {
....
if (需要重新计算宽度和高度) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
if (需要重新布局) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
}
if (需要重新布局) {
performDraw();
}
....
}
在performMeasure中调用了对应的View的measure方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
....
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
....
}
可以发现,最终通过onMeasure方法设置我们的View的宽度和高度,并且一定要通过setMeasuredDimension来设置
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
....
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
....
}
public void layout(int l, int t, int r, int b) {
....
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
这里的onLayout主要是用来设置ViewGroup的子元素的位置的,这也就是为什么我们需要在自定义的ViewGroup中onLayout方法来设置子元素的位置了。
接下来是performDraw
private void performDraw() {
....
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
....
}
private void draw(boolean fullRedrawNeeded) {
.....
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
// 如果是动画的,比如scroller控制的,则又会接着重新调用scheduleTraversals进行绘制
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
....
mView.draw(canvas);
....
}
在看下View#draw方法
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
.....
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
在该方法中调用了onDraw方法进行具体的绘制操作.
ok,关于setContentView的流程就到这里了,其中包括DecorView是如何与Window建立关联的,以及View的绘制流程。如有补充,欢迎指教。