1.前言
在android当中对于UI体系当中往往我们会在绘制UI的时候碰到各种各样的问题而不知道从何解决, 也有时需要开发更改自定义组件时,需要做自己的调整,或者是实现某个自定义特效时的思路不明确,因此了解UI绘制流程及原理是十分必要的,本文就UI绘制流程之前的相关知识进行简单的分析和梳理,便于后续进一步了解UI绘制原理
2.View是如何添加到屏幕窗口上的
要弄清楚UI绘制流程和原理,我们首先要了解的就是View是如何被添加到屏幕窗口上的。带着这个问题我们来进行源码分析,关于界面的展示,立马浮现在脑海的就是这样一段代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
通过传入布局资源ID,setContentView方法又做了什么事情呢?经过一系列线索最终找到了的位置也就是Window的唯一实现类PhoneWindow:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
// This is the top-level view of the window, containing the window decor.
// 这是在窗口当中的顶层View,包含窗口的decor
private DecorView mDecor;
// 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.
// 这是窗口内容放置的视图,它要么是mDecor本身,要么是mDecor的子类的内容
ViewGroup mContentParent;
...
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// 注释1
installDecor();
}
...
// 注释2
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}
}
注释1处installDecor方法顾名思义就是初始化操作,注释2处就是将布局资源ID填充到mContentParent内容布局容器。先进入installDecor方法:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 注释1
mDecor = generateDecor(-1);
...
}
if (mContentParent == null) {
// 注释2
mContentParent = generateLayout(mDecor);
...
}
}
注释1处,如果mDecor为空,就调用generateDecor方法,进入该方法就发现通过返回一个new出来DecorView,然后赋值给mDecor。注释2处调用generateLayout方法,那么该方法是如何给mContentParent赋值的呢?
protected ViewGroup generateLayout(DecorView decor) {
...
// 注释1
// 根据系统主题的属性设置了许多了特性
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
...
// 注释2
// Inflate the window decor.
int layoutResource;// 布局资源id
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
// 注释3
// 根据不同feature, 对layoutResource进行不同的赋值操作
// 即后续加载不同的布局,这就很好的解释了为什么我们自己要去getWindow.requestFeature时
// 必须在setContent之前的原因
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
...
}
mDecor.startChanging();
// 注释4
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 注释5
// ID_ANDROID_CONTENT = com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
return contentParent;
}
省去了很多类似的特性设置代码,在注释1处我们发现根据系统属性的不同,通过requestFeature和setFlag方法设置了许多属性。在注释2处,看到解析窗口decor的提示,继续往下看,如注释3处,会根据不同的特性对布局资源进行不同的赋值,即后续加载不同的布局(就是不同的ActionBar,TitleBar之类的)。这就是为什么我们自己要去getWindow.requestFeature时必须在 setContent之前的原因。再看注释4处的onResourcesLoaded方法:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
...
mContentRoot = (ViewGroup) root;
initializeElevation();
}
主要逻辑就是将传入layoutResource即布局资源通过addView添加到DecorView中。在回到注释5处,通过findViewById,获取id为com.android.internal.R.id.content的contentView,即内容布局容器,最后返回。分析完installDecor方法,再回到PhoneWindow的setContentView方法的注释2处,调用inflate方法,就是将MainActivity的layoutResID即对应的资源布局,添加到mContentParent内容布局容器。至此setContentView的分析就告一段落。
方法内部逻辑比较多,主要做了以下几件事:
-
installDecor方法内部的generateDecor方法初始化DecorView
-
installDecor方法内部的generateLayout
-
根据不同的系统属性,通过requestFeature和setFlag方法设置不同(feature)特性
-
根据不同的feature,通过onResourcesLoaded方法的addView加载不同的layoutResource(布局资源,一般是ActionBar,Title等)
-
通过findViewById获取固定id为com.android.internal.R.id.content的内容布局容器contentParent
-
返回contentParent
-
-
setContentView方法内通过inflate方法将初始的layoutResID对于的布局添加到contentParent布局容器
总结一下,View是如何添加到屏幕窗口上的,主要分为三个步骤:
- 创建顶层布局容器DecorView
- 在顶层布局中加载基础布局容器ViewGroup
- 将ContentView添加到基础布局中的FrameLayout中
3.View的绘制流程
3.1绘制入口
谈到View的绘制入口,就需要知晓Activity的启动过程,如果还不太清楚可以查阅下面两篇文章了解相关细节
Activity的启动流程分析与总结
Application创建流程分析
受篇幅所限,就不具体分析了。就Activity启动过程的部分与View绘制相关的流程进行简单的梳理,如下图
在handleLaunchActivity方法中调用performLaunchActivity后续会调用Activity的onCreate方法,在performLaunch之后会调用handleResumeActivity方法,顾名思义就知道它会是onResume方法的入口,走进该方法:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
// 注释1
// 回调Activity的生命周期方法onResume
r = performResumeActivity(token, clearHide, reason);
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();
// 注释2
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
// 注释3
// 调用Activity的getWindowManager获取wm
ViewManager wm = a.getWindowManager();
// 注释4
// 获取窗口的布局属性对象
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 注释5
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
...
}
在注释2处调用window的getDecorView方法,最终还是调用PhoneWindow的相关方法获取DecorView,在注释3处调用Activity的getWindowManager方法获取ViewManager,在注释4处获取窗口的布局属性对象,在注释5处调用WindowManager的addView方法,进入Activity的getWindowManger方法:
public WindowManager getWindowManager() {
return mWindowManager;
}
在Activity中搜索mWindowManager赋值的逻辑:
final void attach(Context context, ActivityThread aThread,
...
mWindowManager = mWindow.getWindowManager();
...
}
接着进入Window中查找mWindowManager赋值的地方
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
...
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
接着进入createLocalWindowManager方法,来到了WindowManagerImpl 即WindowManager的实现类:
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
进入WindowManagerImpl的addView方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
接着进入mGlobal即WindowManagerGlobal的addView方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
...
// 注释1
root = new ViewRootImpl(view.getContext(), display);
// 注释2
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 {
// 注释3
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
throw e;
}
}
}
在注释1处,实例化了一个ViewRootImpl,在注释2处,设置布局参数,添加到相关集合,在注释3处通过ViewRootImpl的setView方法将View和布局参数等进行了关联,进入setView方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// 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();
...
}
需要关心的代码就这一句requestLayout,我们知道该方法会触发View的绘制流程,进入该方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
进入scheduleTravels方法:
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 注释1
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
注释1处方法参数mTraversalRunnable是一个Runnable,进入查看它的run方法:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
继续追踪进入doTraversal方法
void doTraversal() {
...
performTraversals();
...
}
进入performTraversals方法, 正式进入View绘制的三大流程
private void performTraversals() {
...
// 执行测量
performMeasure(xxx)
...
// 执行布局
performLayout(xxx);
...
// 执行绘制
performDraw();
...
}
绘制入口的简单小结
- ActivityThread.handleResumeActivity()
- WindowManagerImpl.addView(decorView, layoutParams)
- WindowManagerGlobal.addView()
- ViewRootImpl.addView()
3.2绘制涉及的类及方法
- ViewRootImpl.setView(decorView, layoutParams, parentView)
- ViewRootImple.requestLayout()–>scheduleTraversals()–>doTraversal()–>performTraversals()
3.3绘制三大步骤
- 测量:ViewRootImpl.performMeasure()
- 布局:ViewRootImpl.performLayout()
- 绘制:ViewRootImpl.performDraw()
结语
ViewRootImpl是连接WindowManager和DecorView的纽带,View绘制的三大流程均是通过它来完成的,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImple对象,并将ViewRootImpl和DecorView建立关联。到performTraversals方法的主要调用流程大致如下图:
View的具体绘制从ViewRootImpl的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上。