前言
Android现有的View体系是一个十分庞大的结构体系,单凭这一篇文章肯定不可能面面俱到,但我会尽我的理解来尽可能地将直观的体系展现给大家。
View的体系
结构
目前,Android的View体系都基于View和ViewGroup两个大类,同时ViewGroup又是View的子类。其结构设计基于“组合模式”,ViewGroup是容器,View是叶子节点,这就意味着ViewGroup中既可以包含View,又可以包含ViewGroup,但是反之则不行,这就保证依赖关系的安全。
关于组合模式,本文不做展开,未来会在设计模式的系列文章中进行讲解,感兴趣的同学可以先做一个了解。
所以View的的树状结构所呈现的如下:
做过Android的同学都知道,我们在写布局文件时,ViewGroup和View都是层层嵌套的,那他们是怎么展示到我们屏幕上的呢?
可以看到View的结构是树状的,由于树的特性,View的展示过程是从上到下逐级绘制(实际过程当然很复杂),可以说每一个有视图Activity都包含一个View Tree用来描述呈现在屏幕上的View关系,这方便让开发者更好地定位到每一个View的位置。
同时,View的绘制起点并不是我们所定义的那一个xml视图文件,在其外层还有系统所提供的一层东西所包裹,那这层东西又是什么呢?
顶层布局
本小节是关于源码的分析,如果不感兴趣的同学可以先跳过,先直接给出结论:
我们都知道在Activity的onCreate方法中需要实现setContentView方法来绑定我们定义的布局,那绑定的布局又是绑定在什么地方呢?直接绑定在Activity上吗?
这些问题的答案自然需要到源码中去找了,本文所使用的源码是基于Android 10。
分析的入口
我们的编码一般都是从Activity开始,在Activity中都有onCreate方法用于进行初始化设置,还记得其中的setContentView(R.layout.activity_main);
嘛?这就是我们设置xml布局的地方,也是我们分析的入口。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 分析的入口 --> 分析1.Activity 的 setContentView 方法
setContentView(R.layout.activity_main);
}
创建过程
分析1.Activity 的 setContentView 方法
首先会进入到代理中:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
再进到getDelegate()
下的setContentView
,会发现是一个抽象方法,这么快就断了线索吗?一回想,代理这个词,那就去找具体实现这个方法的对象吧。
其实在抽象方法的注释上也表明了要去哪里寻找:
/**
* Should be called instead of {@link Activity#setContentView(int)}}
*/
public abstract void setContentView(@LayoutRes int resId);
由于新建的Activity最终都是继承于Acivity类的,所以我们在Activty类中找到 setContentView
方法实现:
// Activity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
// 获取Window,设置布局 --> 分析2.从哪里获取的Window,怎么样设置的布局
getWindow().setContentView(layoutResID);
// 初始化ActionBar
initWindowDecorActionBar();
}
// 在Activity中有三个重载函数用于在不同的情况下创建布局:
// setContentView(@LayoutRes int layoutResID)
// setContentView(View view)
// setContentView(View view, ViewGroup.LayoutParams params)
可以看到在Activity的setContentView方法中有两步操作,从字面上的意思来看:
- 获取Window,设置布局
- 初始化ActionBar
这里就有两个疑问了:
- 获取了什么Window?
- 初始化的ActionBar是哪里的,Decor是啥?
下面我们来一一解决我们的疑问:
分析2.从哪里获取的Window,怎么样设置的布局
通过代码追踪我们发现getWindow
方法返回了一个mWindow对象:
public Window getWindow() {
return mWindow;
}
那么这个mWindow
实例是从哪创建的呢?通过搜索我们发现这是一个PhoneWindow
// mWindow是一个PhoneWindow的实例对象
mWindow = new PhoneWindow(this, window, activityConfigCallback);
这样我们就知道是通过PhoneWindow
来进行布局的设置的,那么我们进到PhoneWindow
当中,搜索一下setContentView
方法:
// PhoneWindow中的setContentView方法,省略无关的代码
@Override
public void setContentView(int layoutResID) {
// 判断内容父布局是否为空,如果为空则创建DecorView --> 分析3.创建DecorView的过程
if (mContentParent == null) {
installDecor();
}
// 将布局添加到内容父布局当中去 --> 展示过程
mLayoutInflater.inflate(layoutResID, mContentParent);
// 此外通过其它重载方法进行布局的设置会调用
// mContentParent.addView(view, params);方法
// 原理是一样的,只不过所获的参数不同需要通过不同的方式进行展示
// 因为最终都会通过addView(view, params)方法进行添加,展示过程将以这个方法作为入口
···
}
ps:PhoneWindow
类在源码中是被隐藏的不能直接通过代码追踪的方式直接进入,可以通过sdk中的源码进行阅读。
分析3.创建DecorView的过程
进入到installDecor方法中,来看一下DecorView这个东西具体是怎么被创建出来的。
// 省略无关代码,只关注DecorView的创建过程
private void installDecor() {
// 判断DecorView是否为空,为空则创建DecorView
if (mDecor == null) {
// 创建DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
// 不为空,则将DecorView依附到PhoneWindow上
else {
mDecor.setWindow(this);
}
// 如果内容父布局为空,则根据DecorView生成布局
if (mContentParent == null) {
// 获取内容布局
mContentParent = generateLayout(mDecor);
···
}
}
这里有两个方法generateDecor
和generateLayout
分别用来创建和生成DecorView,在这其中具体做了什么呢?
generateDecor方法,主要获取了DecorView所需要的相关参数之后,进行DecorView的创建,并返回DecorView实例
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
generateLayout方法,主要实现了DecorView中布局的创建
protected ViewGroup generateLayout(DecorView decor) {
···
// 布局资源
int layoutResource;
// 一般情况下会获得screen_simple布局
layoutResource = R.layout.screen_simple;
// 并加载到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 获取内容布局并返回
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
···
return contentParent;
}
至此,我们就获得了DecorView,那么疑问一个接着一个:DecorView中的布局是怎么样的呢?
我们找到R.layout.screen_simple所对应的文件来看看其中的布局到底是怎么样的?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
可以看到DecorView中包含一个LinearLayout布局,其中包括了两部分actionBar和FrameLayout(会被定义好的布局文件所替换)。
我们可以来画这么一个图:
整个嵌套关系就很清晰地展示出来了:
Activity-->PhoneWindow-->DecorView-->LinearLayout-->包含ActioneBar和定义的布局视图
创建完了视图,自然就是要展示出来了,下面来聊一聊展示的过程。
展示过程
回到PhoneWindow的setContent方法中,之前我们分析了DecorView的创建过程,接下来我们来看看DecorView创建完之后,是如何进行展示的。
// PhoneWindow中的setContentView方法,省略无关的代码
@Override
public void setContentView(int layoutResID) {
// 判断内容父布局是否为空,如果为空则创建DecorView --> 之前的分析.创建DecorView的过程
if (mContentParent == null) {
installDecor();
}
// 将布局添加到内容父布局当中去 --> 展示过程
mLayoutInflater.inflate(layoutResID, mContentParent);
// 此外通过其它重载方法进行布局的设置会调用
// mContentParent.addView(view, params);方法
// 原理是一样的,只不过所获的参数不同需要通过不同的方式进行展示
// 因为最终都会通过addView(view, params)方法进行添加,展示过程将以这个方法作为入口
···
}
展示过程最终都会通过ViewGroup
的addView
方法进行绘制展示,在这部分内容中我们会进入到View的绘制流程当中去。
public void addView(View child, int index, LayoutParams params) {
···
// 请求布局 --> 分析4.请求布局的过程
requestLayout();
// 无效化??? invalidate这个单词是无效化的意思 --> 分析5.真的是无效化吗?
invalidate(true);
addViewInner(child, index, params, false);
}
分析4.请求布局的过程
首先对requestLayout
方法进行分析,进入到方法中,核心方法是mParent.requestLayout();
的调用:
// View的requestLayout方法
public void requestLayout() {
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用ViewRootImpl的requestLayout方法
mParent.requestLayout();
}
···
}
这里出现了一个新的变量mParent
,那么它是什么呢?通过代码追踪,我们发现这是一个接口,紧接着找到了接口的实现类ViewRootImpl
,那我们进入到ViewRootImpl
的requestLayout
中来看看做了些什么?
// ViewRootImpl的requestLayout方法
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查线程,是否在UI线程
checkThread();
mLayoutRequested = true;
// 遍历表 --> 分析6.最终的流程
scheduleTraversals();
}
}
关于scheduleTraversals
先放一放,还记得之前和requestLayout
并列的invalidate
方法吗?我们先来分析它,避免把它忘记掉了。
分析5.真的是无效化吗?
经过requestLayout();
的过程后,代码会执行到invalidate(true);
,一开始看见这个方法名的时候很懵逼,什么叫无效化?既然不明白就只能带这个疑问去看源码了。
// 看到这个invalidateCache参数的名字有点明白这是什么意思了
public void invalidate(boolean invalidateCache) {
// 具体的操作,这里将一些尺寸相关的参数传了进去
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
看到那个参数名字,猜想会不会是这是一个使缓存的绘制数据失效,并重新绘制的过程呢?
我们继续往下看,进到invalidateInternal
方法中去,核心代码就一行。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
···
// 无效化子控件,老版本的源码是这一行代码,标记为废弃了
p.invalidateChild(this, damage);
// 新的版本使用了其它方式,如下
receiver.damageInParent();
}
那么就进到damageInParent
方法中:
// View的方法
protected void damageInParent() {
if (mParent != null && mAttachInfo != null) {
mParent.onDescendantInvalidated(this, this);
}
}
这里需要注意一下直接进到mParent.onDescendantInvalidated(this, this);
里的话就碰壁了,还记得mParent
的实现类ViewRootImpl
,我们就进到这个方法里去看它具体的实现是什么:
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread();
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
哈哈,看到invalidate()
这个方法终于是放心了,看来Android新的源码只是换了一种实现的方式(小声逼逼叨),那么再进到这个方法中去:
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
// 遍历表 --> 分析6.最终的流程
scheduleTraversals();
}
}
emmm,这绕了一大圈又到了scheduleTraversals方法了,那就进去看看这最终的方法里都干了什么吧。
分析6.最终的流程
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 注意这个mTraversalRunnable变量,是将某中操作进行了传递
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
注意其中mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
的mTraversalRunnable
变量,是将某中操作进行了传递那么就去看看是做了什么操作吧。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
代码追踪到了对应的实现,进到方法中:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 真正进行遍历的地方,也就是之后的View绘制流程的入口
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
代码追踪到这里,setContentView之后所做的事情我们大致就清楚了,之后就是具体的测量绘制流程了,关于View的绘制流程会在之后的文章中去讲解。
绘制流程
虽然关于View的绘制流程,本文暂不做展开,但是关于View的绘制过程还是要来讲一下的。View的绘制可以分为三个部分:
- 测量
- 布局
- 绘制
三个过程是View展示在了屏幕之上,这也很好理解,每一个步骤都对应了一般设计的步骤。
关于View绘制的各个步骤和过程的源码分析将在之后的文章中进行讲解。
功能
要实现一个View,需要进行以下但不止以下事件的处理,在后续的文章中会对他们进行讲解。
- 创建View
- 测量
- 布局
- 绘制
- 事件处理
- 焦点处理
- 显示处理
- 动画处理
总结
本文主要介绍了View的体系结构,以及在View进行绘制前所做的那些操作,关于源码分析的部分,并没有很详细地进行展开,只是讲解了一下其中最主要的一部分内容,其实还有很多其它的内容(跳转动画,线程判断)没有展开,由于文章重点不同,感兴趣的同学可以自己查阅相关资料先看看,未来可能也会讲到。