深入理解View知识系列二- View底层工作原理以及View的绘制流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yulong0809/article/details/79277594

一般我们都知道一个View到展示出来会经过onMeasure、onLayout、onDraw三个方法,但是在分析完了setContentView后发现这几个方法都还没有执行,这篇将会上一篇的基础上继续分析View的工作原理

本篇你会学到什么?

  • Activitiy是在哪里开始准备显示View的
  • View的三大方法是在什么时候开始执行的,又是再哪里被调用的
  • 我们经常使用的View.post后就可以获取到View的宽高,为什么呢?
  • View的requestLayout,invalidate请求重绘的原理
  • 子线程真的不能更新ui吗?为什么

上一篇回顾

我们带着问题在上一篇的基础上继续分析,View的绘制流程及工作原理,在正式分析之前我们先回顾一下上一篇的内容.

setContentView

  • 我们知道了Activity的三个setContentView方法内部全部调用了getWindow.setContentView()
  • Activity的getWindow类型是PhoneWindow,而且也是Window的唯一实现类,PhoneWindow在Activity的attach方法中被初始化,并且设置了一些回调接口指向自己,例如Window的Callback接口,这个接口中有不少我们熟悉的方法,例如dispatchTouchEvent等。
  • 在PhoneWindow的setContentView方法中主要执行三个逻辑

1.判断装在我们设置布局的FrameLayout是否为空,如果为空调用installDecor方法,方法中首先会先执行generateDecor()创建DecorView,接着执行generateLayout()方法设置一些Window的样式,根据样式来选择需要加载的布局,然后将布局添加到DecorView中,最后找到id为content的FrameLayout,也就是来装我们设置布局的父View。

2.通过LayoutInflater将我们设置的布局添加到id为content的FrameLayout中

3.回调Callback接口的onContentChanged()方法,这个方法在Activity中是个空实现。

LayoutInflater

  • PhoneWindow中的LayoutInflater是在构造函数中被创建,创建的方式和我们平时使用一样调用的是LayoutInflater.from(content);
  • LayoutInflater.from方法中就是封装了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  • LayoutInflater中一共有4个inflater方法,其中分为两类,一类是传入资源id的,一类是传入XmlPullParser的,但是最终都会执行带xml解析器的三参方法中,这里注意第三个参数源码中传入的是root!=null,该参数代表了是否将加载后的View添加到父View中,如果使用了两个参数的inflater,并且传入的父view不为空,则默认会将加载后的布局添加到父view,并最后返回的是父view。
  • inflater方法中首先会获取到xml的跟节点,接着判断该节点是否为merger标签,如果是merger标签则继续判断传入的父view是否为空,或者第三个参数是否为flase,如果满足一项则直接抛出异常,因为merger标签只是为了减少布局的嵌套才存在,他并不是View的子类,所以不能单独存在,反之则会执行rInflate方法进行递归加载所有子View。
  • 如果不是merger标签则会走到else方法中,先活着节点的名称,根据名称创建View,在创建View的逻辑中最终全部会调用createView方法,在这之前首先会判断是否存在自定义的加载工厂,如果存在则调用加载工厂的创建View方法,接着通过名称中是否包含 . 来判断是否为Android自带的控件,如果是Android自带的控件第二个参数中传入的值为android.view.,反之传入null
  • 在createView方法中创建View的时候会执行如下逻辑

1.首先会判断这个名称的构造函数是否存在,如果存在则还需要验证这个函数所属的ClassLoader是否合法,如果不合法则会置空这个构造函数并清除缓存。

2.接着继续判断构造函数是否为空,如果为空则会通过一个三元运算来加载这个名称的Class对象,这个三元运算主要是用来判断是否需要拼接要加载Class的名字。

3.然后会判断是否存在Filter过滤器,这个过滤器是用来判断是否可以创建这个Class的View对象,通过查看设置过滤器的RemoteViews和AppWeightHostView都是通过clazz.isAnnotationPresent(RemoteView.class)来判断的,如果返回为flase则执行抛出异常。

4.或者该Class的构造函数并存入HashMap中

5.在else逻辑中,主要是用来优化存在过滤器的情况,为了优化性能,系统只会在第一次的时候去验证是否允许创建这个View,并将结果存入HashMap中,下次只会取map中结果进行判断。

6.通过构造函数实例化这个View并返回

  • 在创建了跟节点的View后,接着同样会递归加载所有的子view,在加载的过程中会根据不同的标签进行不同的操作,例如merger、include。如果是merger则直接抛出异常,因为merger只能作为根节点。如果是include则会拿到layout属性设置的布局文件,如果未设置这个属性执行抛出异常,接着也是根据标签进行递归加载逻辑类似。
  • inflater最后根据传入的父view和第三个参数的值进行判断是返回父view还是返回刚加载的View

好了,回顾完了,在上一次讲的setContentView过程中只是初始化了PhoneWindow、DecorView并将我们设置的View入到了id为content的FrameLayout中,这时Activity的界面还是不可见的,因为View还没有开始绘制的流程呢,那么Activity中DecorView绘制的流程到底在哪里开始的呢?其实是在ActivityThread中的handleResumeActivity()方法中,下面正式开始分析。

本篇源码基于Android 7.1.1

1.逻辑开始点:ActivityThread中的handleResumeActivity()

这里又提到了ActivityThread这个类,简单说一下这个类,要知道所有的程序都是需要一个入口的,Android也不例外,ActivityThread就是Android应用的入口类,Activity、Service、ContentProvider几乎都直接或者间接在这里调度。现在暂时知道这么多就好了,先不要纠结这个类,后续的会专门在四大组件系列去讲。

源码位置:/frameworks/base/core/java/android/app/ActivityThread.java

    final void handleResumeActivity(IBinder token,
unscheduleGcIdler()
;
mSomeActivitiesChanged = true;
//ActivityClientRecord 在AMS中代表一个Activity,这个方法中会调用
//Activity的onResume方法
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
//得到Activity
final Activity a = r.activity;
if (localLOGV) Slog.v(
TAG, "Resume " + r + " started activity: " +
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished);
final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
。。。
if (r.window == null && !a.mFinished && willBeVisible) {
//获取Activity中的Window,也就是PhoneWindow
r.window = r.activity.getWindow();
//获取PhoneWindow中的DecorView
View decor = r.window.getDecorView();
//设置DecorView隐藏
decor.setVisibility(View.INVISIBLE);
//获取Activity的WindowManager
ViewManager wm = a.getWindowManager();
//获取PhoneWindow的参数
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;
//在Window中添加DecorView
wm.addView(decor, l);
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(
TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
...
if (r.activity.mVisibleFromClient) {
//设置DecorView显示
r.activity.makeVisible();
}
}
...
} else {
try {
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null, false);
} catch (RemoteException ex) {
}
}
}

2. 上面的代码中主要执行如下了三个逻辑。

  1. 首先调用执行了performResumeActivity方法,在这里方法中会调用Activity的onResume方法

  2. 获取Activity的PhoneWindow,接着拿到PhoneWindow中的DecorView并设置为隐藏,获取Activity的中WindowManger并执行addView方法将DecorView添加到Window中。

  3. 执行Activity的makeVisible方法展示DecorView,方法中就是调用了View.VISIBLE

而且我们通过上面的可以总结两个问题。

  1. 其实在Activity的onResume方法执行了以后才开始将DecorView添加到Window中,换句话说,也就是在onResume方法中是不能直接获取View的宽度等参数的,因为这个时候连DecorView都没有添加到Window中呢,所以这时也还没有执行View的三大流程呢,又如何产生这是东西。

  2. DecorView最终也是通过WindowManager来添加的,或者说Activity可以显示内容其实也是通过WindowManager添加View来实现的,其实Android中所有设计View展示的最终全部都是通过WindowManager的addView来添加的,例如PopupWindow、Dialog、Toast等,下一篇会详细分析

3.在上面的代码中,使用了ViewManager执行了addView方法,在上一篇我们简单的说过了,WindowManger是继承自ViewManger的,而且WindowManger中的addView、updateViewLayout、removeView全部都是ViewManger中的方法,下面先简单看一下这两个类的声明

//ViewManager
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
//WIndowManager
public interface WindowManager extends ViewManager {
....
}

4.我们可以看到WindowManager也是一个接口,它的具体实现是WindowMangerImpl,那么我们继续上面的逻辑去看一下它的addView方法,顺便看一下ViewManger中的其余两个方法

源码位置:/frameworks/base/core/java/android/view/WindowManagerImpl.java

public final class WindowManagerImpl implements WindowManager {
//WindowManagerGlobal是单例的
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
//addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//跳转到WindowManagerGlobal中
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
//updateView
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//跳转到WindowManagerGlobal中
mGlobal.updateViewLayout(view, params);
}
//removeView
@Override
public void removeView(View view) {
//跳转到WindowManagerGlobal中
mGlobal.removeView(view, false);
}
...

5.可以看到WindowManagerImpl中三个方法的实现全部都桥接到了WindowManagerGlobal中,并且这个类是单例的。我们只分析addView方法,其他方法原理相通

源码位置:/frameworks/base/core/java/android/view/WindowManagerGlobal.java

    public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow
)
{
//1.验证参数合法性
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
//2.如果存在父Window需要调整一些参数
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
//设置硬件加速
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
//创建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
//设置参数属性
view.setLayoutParams(wparams);
//保存要添加view、ViewRootImpl和LayoutParams
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
//调用了ViewRootImpl的setView
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;
}
}

6. 在WindowManagerGlobal中的addView方法中主要有三个逻辑如下,并且我们通过上面的代码可以看出来WindowManager每次在添加View的时候都需要创建一个ViewRootImpl,或者说每个Window中都会对应一个ViewRootImple。

  1. 检查参数的合法性
  2. 创建ViewRootImpl,并将要添加的view、ViewRootImpl和LayoutParams缓存起来
  3. 最后调用了ViewRootImpl的setView方法后逻辑跳转到ViewRootImpl中。

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
//最终在这里调用了绘制
requestLayout();
...
//通过WMS添加Window
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
...
//记住这里,下面分析View的requestLayout原理时会说
view.assignParent(this);
...
}

7. ViewRootImpl的setView方法相等的长,最重要的是执行了上面的两个方法,一个是requestLayout方法,这个方法中最终开始了绘制的流程,但是并不是直接开始的,一会我们再分析。还有一个就是通过mWindowSession的addToDisplay最终在WindowManagerService中完成Window的添加过程,至于到首先我们先来看一下requestLayout这个方法,WindowSession的addToDisplay逻辑和原理我们会在下次讲解Window时在去讲,还有就是要记住这里调用了view.assignParent(this),下面分析View的requestLayout原理时会说

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    @Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查线程
checkThread();
mLayoutRequested = true;
//绘制入口
scheduleTraversals();
}
}

8.上面的代码中绘制的入口跳转到了scheduleTraversals中,在看这个方法前我们先来看一下checkThread这个方法

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    void checkThread() {
if (mThread != Thread.currentThread()) {
//这个异常熟悉吗
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

9.这里看到了在开始绘制前首先会检查线程,而且上面抛出的异常再熟悉不过了,子线程更新ui了,这里就说到了最开始说的子线程真的不能更新ui吗?答案是可以的,但是有一个前提条件就是必须是在ViewRootImpl没有创建之前,简单说可以在Activitiy的onCreate里边开一个线程去更新ui是不会报错的,但是这个线程不能太耗时,因为Activity的绘制入口是在onResume以后开始的,但是也不能太耗时,毕竟我们的Activitiy只是一个回调方法而已,当ViewRootImpl被创建后就会抛出异常了。下面我们就去看一下scheduleTraversals的方法吧

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//消息屏蔽,这里的知识会在以后去讲
mTraversalBarrier =
mHandler.getLooper().getQueue().postSyncBarrier();
//发送一个异步的消息,最终会执行传入的Runnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

10. scheduleTraversals中开启个消息屏障,目的是为了使View更快速的布局和绘制完成。然后执行mChoreographer的postCallback,Choreographer这个类是用来异步更新ui的,里面也是使用了Handler,也就是在这里真正的开始了绘制的流程,这个类还涉及了Android中的VSYNC机制,感兴趣可以自己查阅。接着我们来看一下这个Runnable

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    //字段声明
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//TraversalRunnable的定义
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
//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;
}
}
}

11. 终于到了真正的绘制绘制起点了,这个方法相等的长,有800行代码,我们只截取部分重要的代码

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    private void performTraversals() {
//顶层的View,或者可以理解成是DecorView
final View host = mView;
...
//通知被添加到窗口了,传入了mAttachInfo
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
// 记住这个方法,一会回来看
getRunQueue().executeActions(mAttachInfo.mHandler);
...
//measure过程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
....
//layout过程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
....
//通知layout完成了
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
....
//draw过程
performDraw();
}

13.好了,到了这里就开始了View真正的绘制流程了,至于具体的每个流程会在下几次分别的详情的去分析,这一次我们只是为了弄清View的工作原理和整体的流程,我们来看一下getRunQueue().executeActions这个方法有什么用?,在说这个方法之前我们要先说一下平时我们经常使用View.post来获取View的宽高信息,但是为什么这样就可以获取到呢,我们去看一下View.post中的源码。

源码位置:/frameworks/base/core/java/android/view/View.java

    public boolean post(Runnable action) {
//如果mAttachInfo 不为空则使用mAttachInfo中的Handler去post
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//否则调用ViewRootImpl.getRunQueue()去post
ViewRootImpl.getRunQueue().post(action);
return true;
}

14.这个AttachInfo是在哪里被赋值呢?我们再回去看一下ViewRootImpl中performTraversals的一行代码host.dispatchAttachedToWindow(mAttachInfo, 0),host就是一个顶层的View,但是这个View,而这个AttachInfo则是在ViewRootImpl中传过去的,我们去View中看一下

源码位置:/frameworks/base/core/java/android/view/View.java

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
//给AttachInfo赋值
mAttachInfo = info;
...
//回调Viwe自己的onAttachedToWindow
onAttachedToWindow();
...
}

15.好了接着上边的View.post说,这个方法是在View被添加到窗口后,马上绘制前被回调的方法,那么也就是说我们的View在被添加到窗口之前这个mAttachInfo就是null,那么就会走到 ViewRootImpl.getRunQueue().post(action)这个代码,我们去VIewRootImpl中去看一下

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

    //ViewRootImpl中的getRunQueue方法
static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
// ViewRootImpl中RunQueue
static final class RunQueue {
private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();
//被View调用的post方法
void post(Runnable action) {
postDelayed(action, 0);
}
void postDelayed(Runnable action, long delayMillis) {
//封装成一个HandlerAction
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
//存入到ArrayList中
mActions.add(handlerAction);
}
}
...
}

16.看上面的代码可以看出最终是调用了ViewRootImpl类中的RunQueue.post方法,接着会将传过来的Runnable封装成一个HandlerAction,然后存入ArrayList中,那么我们现在再来看一下ViewRootImpl中performTraversals方法里的一句代码, getRunQueue().executeActions(mAttachInfo.mHandler),上面我们已经知道了getRunQueue就是返回了RunQueue,我们看看RunQueue的executeActions方法

源码位置:/frameworks/base/core/java/android/view/ViewRootImpl.java

   void executeActions(Handler handler) {
synchronized (mActions) {
//遍历集合中所有的HandlerAction
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
//使用handler发送消息
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}

17.到这里是不是豁然开朗了呢?还不明白?好吧,其实就是在View还没有View完成的时候是不会执行View.post过来的Runnable的,只是将他添加到了一个集合中,然后当绘制的时候会执行executeActions这句话遍历所有的消息然后将这些消息添加到队列里依次的执行,真正执行被post过来的Runnable时View已经执行完了绘制流程,所以我们可以通过View.post获取到View的宽高属性。

18.完了我们再想一个问题,我们经常会使用View.requestLayout来重绘界面,那么它到底是怎么回事呢,我们来看一下View中的这个方法

源码位置:/frameworks/base/core/java/android/view/View.java

 @CallSuper
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
//调用了mParent的requestLayout
mParent.requestLayout();
}
..
}
//View中的mParent赋值的方法
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}

18.mParent是ViewParent类型,而且我们知道了被赋值的方法是assignParent,那么这个方法是怎么调用的呢?首先我们来回顾一下之前分析的东西,在上面的ViewRootImpl中的setView方法中有这个方法的调用view.assignParent(this),那么这个时候这个View是视图的最顶层View,也就是DecorView,它的Parent是ViewRootImpl,那么DecorView是一个FrameLayout的ViewGroup,它又会在添加View的时候给所属子View赋值,接着子View如果还是ViewGroup则是一样的逻辑,直到所有的View添加完毕,那么这个时候所有的View也就存在了Parent的值,还记得我们分析的LayoutInflater的过程吗,最终也是通过root.addView来完成添加的,所以说所有的子View都会存在它的父Parent。

源码位置:/frameworks/base/core/java/android/view/ViewGroup.java

    public void addView(View child, int index, LayoutParams params) {
...
//
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout
)
{
....
//设置子View的Parent为当前ViewGroup自己
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
...
}

19.那么到这里我们就知道了所有的View都会存在它自己的Parent,而最顶层的View的Parent是ViewRootImpl,那么也就是说View在调用requestLayout的会层层向上调用,直到最顶层的ViewRootImpl,也就是我们之前分析完了的ViewRootImpl,在这里会开始View的绘制流程,我们上面已经分析过了,至于invalidate的原理其实和requestLayout的原理是一样的,最终都会执行到ViewRootImpl中,这里由于篇幅的缘故就不再贴代码了,还有就是这一篇我们还有一个分支没有分析就是WMS添加window的过程,会在下一篇去讲。

总结:

猜你喜欢

转载自blog.csdn.net/yulong0809/article/details/79277594