View技术原理[1] -- DecorView加载原理

View绘制流程说简单也简单,仅仅是三个步骤,但是说难也是很难,看来无数的书和文章都不能完整的理解,最后还是亲手来总结一番,虽然学习Android有一段时间了,但对于原理层面的问题还从来没有认真的去写过,这是第一次尝试,就当作小白吧,共同进步。

0. 本篇目录

  • 对View的初步认识
  • 对UI架构图的分析
  • DecorView与Window的联系
  • 初探View绘制

1.初次见面

本部分内容需要了解以下几个知识点,这是学习View的入门,也是面试基础问题。

Q1:View是什么,ViewGroup又是什么,他们是以什么结构组织的?
Q2:UI界面架构图是什么样子的?或者说Window,Activity,和View都是什么?

下面来探索一下这几个问题。

View是所有UI控件的一个基类,我们平时用的Button,TextView先不管内部是个什么继承逻辑,但归根结底都有一个共同的父类就是View。

而ViewGroup按照字面意思理解,就是一个View集合,里面可以包括众多View,而ViewGroup是以来维护这些View的,如下图。

View树

而其实ViewGroup也是继承自View的,由这个树形结构我们可以明白一些事情,首先根节点或者父节点绘制好了才会去绘制分支节点,或者说事件首先要经过父节点才能到达根节点,这里面就有一些 测量绘制和事件分发的知识了,具体在后面说。

更准确的说,图中蓝色的根节点,又叫做ViewParent,由它来控制整个事件或者整个测量绘制流程。

于是findViewById() 这个很熟悉的方法,就是通过遍历这棵树来找到目标View的。

下面第二个问题,我们看一下手机屏幕上显示的界面是怎么个组织方式。


https://www.cnblogs.com/jycboy/p/6219915.html

上面的图是一个标准的界面组织方式,最外层是一个Activity,每个Activity都会拥有自己的一个Window,而在Android种Window一般是由PhoneWindow实现。

Window

可以看到Window是一个抽象类,而上面的doc已经提到了,PhoneWindow是这个类唯一的实现,稍后会验证Activty和PhoneWindow的绑定,我们继续往下看。

图中PhoneWindow会有一个DecorView,它是这个界面的根容器,但是本质上是一个FrameLayout,而DecorView内部是一个垂直的LinearLayout,这个LinearLayout包含两部分,TitleView和ContentView,其中TitleView有时我们常见的ActionBar部分的容器,而ContentView就是我们自己创建的界面,它本身是一个FrameLayout,我们平常用的setContentView就是设置它的子View。

梳理一下:
DecorView(FrameLayout) 包含一个 LinearLayout
而这个LinearLayout又包含 TitleView 和 ContentView(FrameLayout)
ContentView 内部才是我们自定义的布局

2. UI架构探索?

大部分人都会选择从setContentView() 方法谈起,因为这个方法是最常见的,用法极其简单的,但又是有很大的说头的。

下面是一段代码(为了清楚,我把自己写的代码和源码综合到一起了,并且省略一些不太重要的代码)

// 1.以下代码来自 一个新建的Activity :DemoActivity

public class DemoActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);   // to 2
    }
}

// 2.以下代码是Activity的setContentView方法

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);  
        ……        
}

首先要知道如果不调用setContentView,是无法显示出我们的界面的,然后调用了setContentView方法之后,Activity内部是调用了getWindow方法的setContentView方法。

那么getWindow方法是什么?

// 以下代码是Activity的getWindow方法

 public Window getWindow() {
        return mWindow;
    }

返回了一个mWindow,而根据返回值我们知道这个mWindow必然是一个Window对象。


在哪里做的初始化?



上面这个方法是Activity的attach方法。而这个attach() 方法会在onCreate()方法之前调用,这里就涉及到Activity启动流程了,暂时不做深究,我们只需要知道onCreate() 前,mWindow 已经完成了初始化,并且指向了PhoneWindow对象。

回到上面 getWindow().setContentView(layoutResID),我们查看PhoneWindow里的setContentView方法。

   // 以下代码来自 PhoneWindow 的setContentView方法

    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            //1.初始化ContentView
            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 {
            //2.添加layoutResID布局到mContentParent
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

首次进入这个方法的时候,mContentParent 必然为null,所以上面代码我们只需要关心标号的两个部分,一个是installDecor() 初始化,二是inflate 解析加载,如代码注释所标。

而后者我们在写代码中也很常见,这里就不多说了,就是把layout解析加载到mContentParent 中,所以着重看一下installDecor。

   // 以下代码来自 PhoneWindow 的installDecor方法

private void installDecor() {
        mForceDecorInstall = false;
        //如果decorView为空,就生成decorView
        if (mDecor == null) {
            //1.初始化decorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        //如果mContentParent 为空,就初始化
        if (mContentParent == null) {
            //2.初始化mContentParent 
            mContentParent = generateLayout(mDecor);
            ...
        }
         ...
    }

这个方法代码很多,着重看两个地方,

1.mDecor = generateDecor(-1);
2.mContentParent = generateLayout(mDecor);

其中很显然,mDecor 是一个 DecorView,mContentParent 是 ContentView

先看generateDecor

// 以下代码来自 PhoneWindow 的generateDecor方法
protected DecorView generateDecor(int featureId) {
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
}

核心代码就是最后一句,生成了一个DecorView返回,而我们已经知道DecorView是一个FrameLayout,它是PhoneWindow的内部类。

而对于generateLayout代码很长,下面我写个伪代码确认以下流程即可。

protected ViewGroup generateLayout(DecorView decor) {
      int layoutResource;
      //确认布局
      if( xxx 主题 xxx 特性){
          layoutResource = R.layout.xxxxx1;
      } else if( xxxx  主题 xxx 特性){
           layoutResource = R.layout.xxxxx2;
     } else if(){
       ……
     }else{
      ……
    }

   mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
   ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
   ……
  
   return contentParent;


}

这段代码其实很简单,就是根据你设置的主题feature来选择默认加载的界面(xml),

什么是theme 和 feature ?
theme是,或者节点指定的themes
feature是requestWindowFeature()中指定的Features
相信这些你都用过。
这也就解释了为什么必须要在setContentView(...)之前才能执行requestWindowFeature(...)

选择好了之后将 布局加载到DecorView中,并且找到ID_ANDROID_CONTENT这个控件作为ViewGroup返回,也就是赋值给mContentParent

那么问题是ID_ANDROID_CONTENT到底是个什么?
很显然,他就是id为content的那个frameLayout(在各种各样的主题xml中,都是这个名字)

至于onResourcesLoaded,我们知道他是将layoutResource加载到mDecor里的方法,内部调用了addView,这里就不深究了。

总结

稍稍总结一下。

Q3: setContentView的流程是什么?

  1. 首先 attach 方法建立 PhoneWindow,在PhoneWindow的 setContentView 方法中 初始化 DecorView,
  2. 调用generateLayout方法选择合适主题布局加载到decorView上,最后对mContentParent 赋值,并且将setContentView所传入的xml布局加载在mContentParent 上。
  3. 到此为止实现了布局的加载。
拙劣的画技

最后补充一点,状态栏导航栏也是在DecorView中的
这里借一张Hierarchy 图展示一下

https://blog.csdn.net/dreamsever/article/details/78440417

可以看到DecorView中除了LinearLayout还有其他两部分,分别是状态栏和底部导航栏。

看到这里,我们就可以对一个界面有很深刻的认识,那么虽然这不是本文的重点,但是也是作为基础的重要一环,下面来看看View的绘制吧。

3. Decor与Window的小确幸

学习到这里,我们发现界面显示貌似与Activity无关,所有的View操作都是PhoneWindow来完成的,所以我们还要深究这个Window。

通过上面我们看到了UI界面的一个架构,我们肉眼所见的就是DecorView,那么DecorView的内容是如何加载到Window上的,你可能会说了上面setWindow不是吗?其实那只是配置一下关系而已,按照正常的流程需要Window 添加 DecorView才对不是吗?

Window偷偷说:你也太小瞧我了,我还没发话了,你们就自行搞定了???

那么这个流程是怎么完成的呢?
还记得我们刚才提到过的attach方法吧,我们说attach是onCreate方法调用之前所调用的,是属于Activity启动的一部分,在attach之前的过程,我们暂时先不说,我们说接下来的流程。

//以下代码Activity 的 attach方法,有省略

 final void attach( …… ){
    ……
   //1. 创建PhoneWindow
   mWindow = new PhoneWindow(this, window, activityConfigCallback);

    ……
   //2. setWindowManager
    mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
   
}
   ……
   //3 .getWindowManager
   mWindowManager = mWindow.getWindowManager();

可以看到上面的代码主要分为三部分,1.创建PhoneWindow,我们已经熟悉了,2是设置一个WindowManager,3是取用这个WindowManager。

那么WindowManager是什么?

WindowManager继承自ViewManager,是Android中一个重要的Service,全局唯一。WindowManager主要用来管理窗口的一些状态、属性、view增加、删除、更新、窗口顺序、消息收集和处理等。

ViewManager

如上图是ViewManager,里面有几个方法,addView等,是对View的操作,而ViewGroup也是实现了这个接口,可以自己去探索。

正是因为实现了这个接口,WindowManager和ViewGroup拥有了控制View的能力。
暂时不深究,我们继续看源码。

// 以下代码来自Window类的setWindowManager

 public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);

        //核心代码
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

检查传入的WindowManager 是否为null,为null就通过系统服务获取一个,(会发现调用前后都是通过系统服务获取,不知道为啥还要判断……)然后生成它的实现。

//以下代码来自WindowManagerImpl的createLocalWindowManager

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

根据上面这两步,我们发现mWindowManager 已经被初始化为WindowManagerImpl

那么我们就会明白,Activity内部的mWindowManager 是一个WindowManagerImpl,插个小曲,看一眼WindowManagerImpl

//WindowManagerImpl 部分代码节选

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
}

可以看到,WindowManagerImpl 确实是 WindowManager 的实现类,而且内部有实现addView方法,但是它的addView却是调用 WindowManagerGlobal 的addView方法了,所以我们还要继续去探索WindowManagerGlobal 。

// 以下代码来自 WindowManagerGlobal 中的addView

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        
         ……
        
        //1. 关注 ViewRootImpl 
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {

             ……
            //2 . 对ViewRootImpl的初始化操作
            root = new ViewRootImpl(view.getContext(), display);

            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设置我们的布局
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

上面的代码比较长,但要是理解这个,我们本部分内容就基本上结束了,我们一点点看。

首先看注释1的地方,新建了一个ViewRootImpl 声明,并在2的地方进行了初始化,同时在3的地方调用了它的setView方法传入了view(这里的view就是我们上一部分讲的DecorView,先知晓一下,后面会验证。)

那么说白了最终我们的工作都交给了ViewRootImpl 去做,而ViewRootImpl是View中的最高层级,属于所有View的根,但ViewRootImpl不是View,只是实现了ViewParent接口,可以看到ViewRootImpl一头是View,一头是WindowManager

而在上面注释3的地方,我们说调用了ViewRootImpl的setView方法,这个方法我就不贴了,在这个方法里调用了addToDisplay方法来实现了Window中添加View。

mWindowSession实现了IWindowSession接口,它是Session的客户端Binder对象.
addToDisplay是一次AIDL的跨进程通信,通知WindowManagerService添加IWindow

到此结束。
但是好像有点不对劲啊?上面说的流程怎么这么乱?下面来串联一下吧。

工作流程图(图片最后一步有点问题,待修改)

首先我们上了一张图,这个图上有我们熟悉的,也有我们不熟悉的,我们从起点梳理一下。

当 startActivity方法调用的时候,首先执行handleLaunchActivity来创建新Activity。

//以下代码来自ActivityThread的handleLaunchActivity方法,只保留两句核心代码

public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
    ...
  
       Activity a = performLaunchActivity(r, customIntent);
    ...
      
       handleResumeActivity(……)    
   
}

首先是调用了performLaunchActivity方法 ,其次调用了handleResumeActivity方法,这里跟我们上面图中所画是一样的。

我们看一下performLaunchActivity源码

//以下代码来自ActivityThread的performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //创建Activity所需的Context
    ContextImpl appContext = createBaseContextForActivity(r);
    Activity activity = null;

    try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //Activity通过ClassLoader创建出来
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            //创建Application
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            appContext.setOuterContext(activity);
            //将Context与Activity进行绑定,并调用Activity的attach方法
            activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
            //调用activity.oncreate
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        }
}

可以看到这里面调用了newActivity,调用了attach,然后还有callActivityOnCreate(回调onCreate)方法,这也跟我们图中所画的生命周期相关方法及流程一样。

对于粉色对话泡泡1处,我们已经了然于胸了,上文不止一次提到过attach里面所做的工作,包括下面的windowManager的创建,在attach完成window及windowmanager的初始化工作之后,

紧接着onCreate开始大展拳脚,setContentView的调用,粉色对话泡泡2,本文一开始就说了,这里也不再赘述。

接下来onStart就会被调用,(至于怎么被调用?我们不深究了,其实可以推断应该也是在DecorView初始化完成后的一个回调)。

接着来到了主线程的流程。
handleResumeActivity的调用。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {

        //1. 调用activity.onResume
        ActivityClientRecord r = performResumeActivity(token, clearHide);
 
        if (r != null) {
            final Activity a = r.activity;
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow(); 
                //2. DecorView的获取
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                //3. 获取一个WindowManger
                ViewManager wm = a.getWindowManager();
                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;
                    //4.把当前的DecorView与WindowManager绑定一起
                    wm.addView(decor, l);
                }

            ... 
    }

我画出了4条重点,其中1处是调用了onResume方法,2是获取了DecorView,3是获取了WindowManagerImpl,4是调用了WindowManagerImpl的addView方法将DecorView传入进去,这也就印证了我们刚才的铺垫,DecorView就是从这里传入进去的,并且与Window建立连接的,至于WindowManagerImpl的addView都做了啥,我们上面都说过了,你不会忘记吧。(也就是粉色对话泡泡3的流程)

到此为止,我们的View就加载到Window里面了,那么接下来要做什么神奇的事情呢?

4 . 初探View绘制

上面我们提到一个关键的内容ViewRootImpl,我们说他是沟通View与Window的桥梁,而且我们也对它的setView方法做了较为简单的分析,下面我们详细分析,setView方法内容也比较多,这里就不贴了,里面除了调用addToDisplay来绑定window与decorView外,还在此前调用了requestLayout() 方法

requestLayout()主要是让View经历measure layout draw三个阶段,

//以下代码来自ViewRootImpl的requestLayout

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

这个代码比较简单,关注最后一句,调用了scheduleTraversals,我们来看一下它。

//以下代码来自ViewRootImpl的scheduleTraversals

 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //核心内容
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

关注postCallback方法,传入了一个mTraversalRunnable任务。而通过定位我们会发现这个mTraversalRunnable其实是一个TraversalRunnable。

ViewRootImpl内定义

可以看到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;
            }
        }
    }

辗转反侧,终于找到了绘制入口,而这个方法内容比较多,我们只需要它内部是如下类似的逻辑即可。

private void performTraversals() {
   //测量
   performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
  //定位  
  performLayout(lp,desiredWindowWidth,desiredWindowHeight);
  //绘制 
  performDraw();
}

好嘞,就到这里。

什么情况?就这样就结束了?
NO,更关键的内容等着我们去发现嘞,明天我们继续吧。

5 . 总结

我们本文从setContentView入手讲了DecorView是如何初始化的,然后又从ActivityThread的角度来分析了DecorView是如何加载到Window上的,最后对View绘制进行了简单的探索,通过对performTraversals的初步探索,我们发现后面会有很多内容,要完整的讲明白是一件难事,所以一般会从主线入手,下一篇文章就来真正的谈谈绘制过程。

可以试着画画流程图哦,看看我画的两张图综合起来是什么效果。

你可能感兴趣的:(View技术原理[1] -- DecorView加载原理)