不知不觉已经步入而立之年,从10年毕业后一直从事移动互联网工作,之前也零零散散的写过博客,但由于工作太忙,也觉得自己文笔不行,始终没有坚持下来。之前在满30岁前几天,在微信发过一条说说,给自己下的目标是:30岁,重新起航,深耕技术。坚持写博客。回归正题。
在开发中经常会遇到两个案列:一个是 setContentView() 作用原理,一个是 View 事件分发原理相关。要想弄清楚这两个问题,就必须探究Activity 与 Window、PhoneWindow、DecorView 之间的关系,因为这些都跟事件分发的源头有密切关系。
首先我们看下Activity的伪源码(抽取):
public class Activity extends ContextThemeWrappe{
private Window mWindow;
}
我们可以看到activity中有Windown对象,我看下window类的简介:
可以看到window是个抽象类,它的唯一实现类是PhoneWindow,也就是说 Activity 中的 window 实例就是一个 PhoneWindow 对象。
但是 PhoneWindow 终究是 Window,它并不具备多少 View 相关的能力。
不过 PhoneWindow 中持有一个 Android 中非常重要的一个 View 对象 Decor(装饰)View,它在 PhoneWindow 中的定义如下:
/**
* Android-specific Window.
*
* todo: need to pull the generic functionality out into a base class
* in android.widget.
*
* @hide
*/
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
}
查看 DecorView 继承关系得知,DecorView 继承自 FrameLayout。
public class DecorView extends FrameLayout {
}
现在的关系就很明确了,每一个 Activity 持有一个 PhoneWindow 的对象,而一个 PhoneWindow 对象持有一个 DecorView 的实例,所以 Activity 中 View 相关的操作其实大都是通过 DecorView 来完成。
一、setContentView()得原理
我们对 Activity 的 setContentView(int resId)方法都非常熟悉,通过该方法,Android 可以帮我们把自己写好的布局文件( resId )最终展示在 Activity 的内容区域中。但具体是怎么做到的呢?
这里其实就是通过不断的传递,把布局文件对应的资源 id 一直传递到这个 Activity 对应的 decorView 中,decorView 本身是一个 FrameLayout,当 decorView 接受到来自 Activity 传递过来的布局 id 后,通过 inflater,把布局资源 id 转换为一个 View,然后把这个布局 View 添加在自身中。
到此为止,我们就在 Activity 中最终看到了自己指定的布局样式。下面稍微看看源码中的逻辑流转。从 MainActivity 的 onCreate 方法开始
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
我们看下Activity中的实现:
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
很容易知道getWindow就是PhoneWindow了,我们看下PhoneWindow中的实现:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
上面的代码,我们挑重点看,installDecor()中对DecorView的一些初始化,最终通过
mLayoutInflater.inflate(layoutResID, mContentParent);
这个方法把布局转化成View ,添加到DecorView上。
通过上面的分析,我们再看下经典图,是不是更加容易理解呢?
到此为止,我们应该已经大概了解了 Activity 中的一些 View 相关的逻辑是怎么跟 window 发生关系的。
其实可以看到上面两个跟 View 操作相关的分析过程中,Activity、以及 Activity 的成员变量 mWindow 什么也没干,他们拿到参数都第一时间都是外抛,最终都会抛给 mWindow 的 decorView 去做具体的逻辑。
这里可能会想,难道 Activity Window 都是傀儡吗?为什么上面的分析中,他们接受到命令后都是一个劲的外抛,自己不处理呢?他们没作用吗?我想其实这里应该是一种特意的设计策略。
作为一个 Activity,它承载了很多功能和使命,它不仅仅是为 View 操作而服务的,所以它把 View 相关的操作交给 DecorView 去完成,通过这种 “外包” 的方式使得自己不用关心 View 操作的细节,到最后其实有点管理中经常说的 “授权” 的意思。