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的,如下图。
而其实ViewGroup也是继承自View的,由这个树形结构我们可以明白一些事情,首先根节点或者父节点绘制好了才会去绘制分支节点,或者说事件首先要经过父节点才能到达根节点,这里面就有一些 测量绘制和事件分发的知识了,具体在后面说。
更准确的说,图中蓝色的根节点,又叫做ViewParent,由它来控制整个事件或者整个测量绘制流程。
于是findViewById() 这个很熟悉的方法,就是通过遍历这棵树来找到目标View的。
下面第二个问题,我们看一下手机屏幕上显示的界面是怎么个组织方式。
上面的图是一个标准的界面组织方式,最外层是一个Activity,每个Activity都会拥有自己的一个Window,而在Android种Window一般是由PhoneWindow实现。
可以看到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的流程是什么?
- 首先 attach 方法建立 PhoneWindow,在PhoneWindow的 setContentView 方法中 初始化 DecorView,
- 调用generateLayout方法选择合适主题布局加载到decorView上,最后对mContentParent 赋值,并且将setContentView所传入的xml布局加载在mContentParent 上。
- 到此为止实现了布局的加载。
最后补充一点,状态栏和导航栏也是在DecorView中的
这里借一张Hierarchy 图展示一下
可以看到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,里面有几个方法,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。
可以看到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的初步探索,我们发现后面会有很多内容,要完整的讲明白是一件难事,所以一般会从主线入手,下一篇文章就来真正的谈谈绘制过程。
可以试着画画流程图哦,看看我画的两张图综合起来是什么效果。