Android中View绘制原理分析


Androiod Sdk版本:api28

1.View绘制是从什么时候开始的?

先提供一个从Activity#onResume到View测量-布局-绘制的调用图:
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的视图逻辑
  • 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示意图
    每一个VSYNC信号过来之后,会驱动一次主线程doFrame+RenderThread线程DrawFrame过程,主线程doFrame执行measure、layout、draw的过程,RenderThread线程执行DrawFrame过程,RenderThread线程的执行逻辑是通过jni接口调用到native层由GPU完成的。也就是说主线程执行了CPU的计算过程,RenderThread执行了界面绘制过程,当然绘制过程也要区分开启硬件加速和关闭硬件加速,但是现在的设备配置都很高了,这里暂时只考虑开机了硬件加速的情况,如果关闭硬件加载的话,通过软件实现绘制过程是没有RenderThread线程执行过程的。
接受到VSYNC信号后触发doFrame+DrawFrame过程
    下面简单看下这个过程的源码脉路(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中,存储结构是单链表。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMix

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值