Android中View绘制原理分析
Androiod Sdk版本:api28
1.View绘制是从什么时候开始的?
先提供一个从Activity#onResume到View测量-布局-绘制的调用图:
(1)想要知道View的绘制从哪里开始,其实可以找个Activity来看,去跟踪它怎么从一个startActivity方法到展示出来的。
启动一个Activity的大致流程是,应用代码中调用startActivity方法,然后中间会经过AMS的系统服务调用,从调用AMS开始,启动流程就进入到了system_server进程,最后会通过ActivityThread的内部类ApplicationThread通过ipc调用方法回到应用进程,然后通过反射的方式创建Activity实例,当然这里如果当前应用进程application实例未创建,也会先使用反射方式创建实例,再通过Handler(ActivityThread.H)回调Activity的生命周期方法。
上述过程有一个非常经典的调用图,我这里引用下:
通过查看源码,可以发现View的绘制是从ActivityThread调用handleResumeActivity开始的,通过调用WindowManager的addView()方法,最后会转调到ViewRootImpl的setView()方法,在setView()方法中转调到scheduleTraversals()方法,在scheduleTraversals()方法中会依次调用perfromMeasure()、performLayout()、performDraw()方法,这三个方法又会转调具体View的measure()、layout()、draw()方法。
2 ViewRootImpl实例的创建位置
ActivityThread->handleResumeActivity():
public void handleResumeActivity(IBinder token, boolean finalStateRequest,
boolean isForward, String reason) {
//(1)从ActivityThread的mActivities集合中查找到对应token的Activity实例
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
boolean willBeVisible = !a.mStartedActivity;
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();
if (a.mVisibleFromClient) { //该字段默认为true
if (!a.mWindowAdded) { //创建时是false
a.mWindowAdded = true;
//(2)
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
// (3)这里是重点,在这个函数里面将activity的decorView的可见性设置为visible
r.activity.makeVisible();
}
}
}
(2)这里是Activity正式通过WindowManager转调到ViewRootImpl执行View的布局流程,也是View呈现出来的启动方法,这里也说明了View的绘制是在onResume阶段触发的
(3)handleResumeActivity方法内调用activity.makeVisible(),让activity的decorView的可见性设置为visible
(4)当Activity执行onStop生命周期时,会把decorView的visibility改成INVISIBLE,这个结论可以通过重写View的onWindowVisibilityChanged、onVisibilityChanged两个回调方法得到。
/**
* Called when the window containing has change its visibility
* (between {@link #GONE}, {@link #INVISIBLE}, and {@link #VISIBLE}). Note
* that this tells you whether or not your window is being made visible
* to the window manager; this does <em>not</em> tell you whether or not
* your window is obscured by other windows on the screen, even if it
* is itself visible.
*
* @param visibility The new visibility of the window.
*/
protected void onWindowVisibilityChanged(@Visibility int visibility) {
if (visibility == VISIBLE) {
initialAwakenScrollBars();
}
}
/**
* Called when the visibility of the view or an ancestor of the view has
* changed.
*
* @param changedView The view whose visibility changed. May be
* {@code this} or an ancestor view.
* @param visibility The new visibility, one of {@link #VISIBLE},
* {@link #INVISIBLE} or {@link #GONE}.
*/
protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) {
}
- WindowManager最终的addView逻辑实现类是WindowManagerGlobal类
- 这里有一个误区就是,并不是说执行了onResume()再去view.getWidth()就能获取准确的值了,因为布局的绘制是通过Handler进行了,可能还未绘制完成。
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
ViewRootImpl root;
View panelParentView = null;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
- 可见ViewRootImpl是在WindowManagerGlobal#addView中创建的
- mViews、mRoots、mParams分别对ViewRootImpl实例、做绘制的View实例和View的LayoutParams做了缓存
- 每启动一个Activity都会创建一个ViewRootImpl然后创建一套上面的三个缓存集合,然后DecorView和内部的子布局在后续的View操作都是使用这里创建的ViewRootImpl去做布局绘制
- View的绘制流程是以Window为单位的,每一个Window中都有一个独立的绘制套件,可以把Window当做窗口的概念,内部是很多层的View再做展示,比如View内的addView、修改pading、margin或者位置、宽高等,都是View级别的操作,是不需要使用Window去操作的
- Activity、Window和View的关系如下:
- Activity、Window和View关系详情可以查阅:View工作原理之Activity,Window和View的关系
2.1 从Activity实例被创建到View展示到界面上经历了什么流程?
从ActivityThread中启动Activity到View的展示会经历以下流程:
在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联.
- RootViewImpl的performMeasure(),performLayout(),performDraw()
- View的绘制是从ViewRoot的performTraversals()开始的,其中会依次调用performMeasure()->performLayout()->performDraw()
android.view.ViewRootImpl#performTraversals()
//source path: frameworks/base/core/java/android/view
private void performTraversals() {
WindowManager.LayoutParams lp = mWindowAttributes;
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//实际调用的是mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//内部会调起view的layout(),可能还会多次调用performMeasure(),可以说明onMeasure()完成后,测量出的宽高和实际的宽高可能是不相等的
performLayout(lp, mWidth, mHeight);
performDraw();
}
ViewRootImpl中会依次调用:performMeasure(),performLayout(),performDraw()发起View的绘制流程
android.view.ViewRootImpl#performLayout()
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //调起View.layout()
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
//mLayoutRequesters是调用requestLayout的View集合
//对调用requestLayout()的view集合,做一些是否绑定到Window,parent是否为null等逻辑的过滤
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false);
int numValidRequests = validLayoutRequesters.size();
//遍历调用View的requestLayout()
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
//内部会再次调用performMeasure()??为啥,这里可以看到已经到layout()流程了,还是会调用测试方法
measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight);
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
}
android.view.ViewRootImpl#performMeasure()
该方法没有复杂的逻辑,直接转调mView.measure()方法了
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
android.view.ViewRootImpl#performDraw()
private void performDraw() {
mAttachInfo.mTreeObserver.dispatchOnDraw(); //使用观察者模式,转调到View的onDraw()方法
}
2.2 系统管理的顶层View:DecorView
Android任何一个界面最顶层的都是DecorView, 我们应用是页面Activity,Fragment,Dialog等都会有布局文件,系统的也是这样,会包括一个竖直方向的LinearLayout,从源码中可以看出顶层View是继承自FramaLayout的,那好办,当做一个ViewGroup来看源码就好了,找他的构造方法和measure(),onLayout(),onDraw()方法就好了
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
DecorView(Context context, int featureId, PhoneWindow window, WindowManager.LayoutParams params) {
super(context);
//持有PhoneWindow的实例
setWindow(window);
}
}
DecorContext:不依赖Activity的应用上下文
DecorView做为顶层View肯定是用来做为View树的根节点的,那么接下来就需要看是谁将他放入到界面上去的
查看他的构造方法调用这,发现是PhoneWindow()::generateDecor()方法创建实例的,
//PhoneWindow()::generateDecor()
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
generateDecor()是被PhoneWindow调用的,分别是:
- PhoneWindow#initializePanelDecor()
protected boolean initializePanelDecor(PanelFeatureState st) {
st.decorView = generateDecor(st.featureId); //转调generateDecor
...
}
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
- generateDecor直接根据传入的布局属性new了一个DecorView
- PhoneWindow#installDecor()
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-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);
}
...
}
看到这里需要关注一下PhoneWindow是持有DecorView的,
那么回顾一下Activity::setContentView()方法:
//另一个直接传View参数的同理
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到Activity的视图View状态工作其实是交给Window实现的,Window目前就只有一个实现类PhoneWindow,所以可以确定的是PhoneWindow是职责是有View的加载工作的,同时检查Activity类是不直接依赖View(ContentView)的,
那么此时Activity的布局加载工作就转到PhoneWindow中去做了
PhoneWindow#setContentView(int):
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
}
PhoneWindow#installDecor():调用generateDecor()初始化mDecorView变量
private void installDecor() {
if (mDecor == null) {
//这里找DecorView#mFeatureId声明位置就会有-1的说明,这里也提醒我们使用魔法值,就应该规范的在声明位置做一些关键描述,
//不然想知道用途就得一步步跟进去很耗时
/** The feature ID of the panel, or -1 if this is the application's DecorView */
mDecor = generateDecor(-1);
} else {
//PhoneWindow和DecorView是双向关联度的,PhoneWindow依赖DecorView,DecorView中也依赖PhoneWindow实例
//因为DecorView中是View层,展示成什么样子,还受PhoneWindow的Attribute参数影响的
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 其实是进行: ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;这个id就是DecorView的放置内容的布局文件了
mContentParent = generateLayout(mDecor);
}
可以看出来这个方法完成了mDecorView和mContentParent布局的初始化,DecorView是一个集成FrameLayout的自定义ViewGroup
2.3 Activity不同生命周期状态下DecorView的可见性更新逻辑
- 当Activity经过onResume之后,visibility=0(VISIBLE)
- 当Activity走到onStop时,对于onVisibilityChanged回调的changedView就是指的是DecorView,visibility=8(INVISIBLE)
3.对View/ViewGroup的measure(),layout(),draw()的理解
- measure:决定View的宽高,getMeasuredWidth()/getMeasuredHeight()获取测试的宽高
- layout:决定了View的四个顶点坐标和实际的View的宽高,完成后可通过getWidth()/getHeight()获取View的最终宽高
- draw决定了View的显示,只有draw完成后内容才能呈现在屏幕上
4.从View的工作原理上再看自定义View实现的一般思路
Activity就像是建造高楼大厦的基础骨架,这种基础骨架的东西是不关注房子里面会贴什么样的地板砖,我们在开发应用时,虽然是直接在Activity内加载我们自己写的layout,但是具体的setContentView()其实都是Window层去实现的,Activity只依赖Window,具体View加载到Activity上都是由Window去完成的。
5、卡顿、丢帧问题源码浅析
5.1、界面绘制过程
每一个VSYNC信号过来之后,会驱动一次主线程doFrame+RenderThread线程DrawFrame过程,主线程doFrame执行measure、layout、draw的过程,RenderThread线程执行DrawFrame过程,RenderThread线程的执行逻辑是通过jni接口调用到native层由GPU完成的。也就是说主线程执行了CPU的计算过程,RenderThread执行了界面绘制过程,当然绘制过程也要区分开启硬件加速和关闭硬件加速,但是现在的设备配置都很高了,这里暂时只考虑开机了硬件加速的情况,如果关闭硬件加载的话,通过软件实现绘制过程是没有RenderThread线程执行过程的。
下面简单看下这个过程的源码脉路(Android SDK33版本):
ViewRootImpl#setView注册Chographer的callback回调
这里回顾下View的绘制过程,可见Android中View绘制原理分析,从启动Activity流程,到AMS中完成system_server进程的Activity实例创建,再通过ApplicationThread的binder调用,回到应用进程的ActivityThread中,直到执行了performLaunch()方法,到Activity的onResume阶段,会执行WMS的addView,而WMS addView过程会委托给WindowManagerGlobal实现(单例实现的),在WindowManagerGlobal的addView方法中,一个界面的ViewRootImpl实例被创建,并且存入到一个List中。
- ViewRootImpl#setView():
public void setView(View view, WindowManager.LayoutParams attrs,
View panelParentView, int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
requestLayout();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// View的三大过程从这里开始
scheduleTraversals();
}
}
// 从scheduleTraversals()开始注册并接收Choregrapher的VSYNC信号,
// 每隔16.6ms会回调一次,
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// mTraversalRunnable的run方法中会调用doTraversal方法
// 回调类型:遍历回调。处理布局和绘制。
// 在处理完所有其他异步消息后运行。
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
- Choreographer的postCallback会将当前的Runnable注册到CallbackQueue中,存储结构是单链表。