注册

Android日记之View的绘制流程(一)

前言


View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK为为Android 9.0版本。



Activity的构成


在了解绘制流程之前,我们首先要了解Activity的构成,我们都知道Activity要用setcontentView()来加载布局,但是这个方法具体是怎么实现的呢,如下所示:


public void setContentView(@LayoutRes int layoutResID) {


getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

public Window getWindow() {
return mWindow;
}

这里有一个getWindow(),返回的是一个mWindow,那这个是什么呢,我们接在在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*/);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

......
}

我们发现了原来mWindow实例化了PhoneWindow,那意思就是说Activity通过setcontentView()加载布局的方法其实就是调用了PhoneWindow的setcontentView()方法,那我们接着往下看PhoneWindow类里面具体是怎么样的。



@Overridepublic 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(); //在这里
} 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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

点进PhoneWindow这个类我们可以知道这里的PhoneWindow是继承Window的,Window是一个抽象类。然后我们看里面的一些关键地方,比如installDecor()方法,我们看看里面具体做了啥。


private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1); //注释1
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

......
}
}

里面有一个标记注释为1的generateDecor()方法,我们看看具体做了啥。

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());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

发现创建了一个DecorView,而这个DecorView继承了Framelayout,DecorView就是Activity的根View,接着我们回到标记注释2的generateLayout()方法,看看做了啥。

    

protected ViewGroup generateLayout(DecorView decor) {


......

// Inflate the window decor.
//根据不同情况加载不同的布局给layoutResource

int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title; //在这里!!!!!!!
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}

......

mDecor.finishChanging();

return contentParent;
}

generateLayout()方法代码比较长,这里截取了一部分,主要内容就是根据不同的情况加载不同的布局给layoutresource,在这里面有一个基础的XML,就是R.layout.screen_title。这个XMl文件里面有一个ViewStub和两个FrameLayout,ViewStub主要是显示Actionbar的,两个FarmeLayout分别显示TitleView和ContentView,也就是我们的标题和内容。看到这里我们也就知道,一个Activity包含一个Window对象,而Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又分为两个区域,分别就是TitleView和ContentView,平常所显示的布局就是在ContentView中。


View的整体绘制流程路口


上一节讲了Activity的构成,最后讲到了DecorView的创建和加载它的资源,但是这个时候DecorView的内容还无法显示,因为还没用加载到Window中,接下来我们看它是怎么被加载到Window中。


还是老样子,DecorView创建完毕的时并且要加载到Window中,我们还是要先了解Activity的创建流程,当我们使用Activit的startActivity()方法时,最终就是调用ActivityThread的handleLaunchActivity方法来创建Activity。而绘制会从根视图ViewRootImpl的performTraversals()方法开始从上到下遍历整个视图树,每个 View 控制负责绘制自己,而ViewGroup还需要负责通知自己的子 View进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。


ViewRootImpl是WindowManager和DecorView的纽带,View的三大流程均是通过这里来完成的,在ActivityThread中,当Activity对象被创建,会将DecorView加入到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。


刚刚也说过了,View的绘制流程是从ViewRootImpl类的performTraversals()方法开始的,它会依次调用performMeasure()performLayout()performDraw()三个方法。其中在performTraversals()调用measure()方法,在measure()方法又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了依次measure过程,接着子元素会重复父容器的measure过程,如此返回完成了整个View数的遍历。



理解MeasureSpec


为了理解View的测量过程,我们还需理解MeasureSpec,它在很大程度上决定了一个View的尺寸规格,而且也参与到了View的measure过程中。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽和高。


MeasureSpec是View的内部类,它代表哦啦一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小,如以下代码所示:


public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

......

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

public static final int EXACTLY = 1 << MODE_SHIFT;

public static final int AT_MOST = 2 << MODE_SHIFT;

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode
int mode)
{
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}


@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}


public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}


......
}

MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而也可以通过解包得到SpecMode和SpecSize。这里说一下打包成的MeasureSpec是一个int值,不是这个类本身。


对于SpecMode,有三种类型:



  • UPSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。


  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。


  • AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。


Android日记之View的绘制流程(二)

作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1 个评论

都是精华

要回复文章请先登录注册