如何寻找并阅读Android源码(一): setContentV
前言
我们平日可能会百度或谷歌搜xxx的原理,xxx的过程,为什么xxx,我们总是从会别人的文章里最后汲取原理和过程,有些文章甚者会贴出部分核心源码来进行分析说的头头是道。(非贬义
但是我每次看都会觉得很疑惑,他是怎么能找到这些源码的,从哪里开始的,这么多源码他是怎么找到这么关键的地方的呢?难道是我太菜了吗?
其实不然(可能真的是你菜,凡事都要遵守规律,我们不妨自己去跟踪源码并记录这个过程。因此有了这篇文章。
文章篇幅较长,以寻找setContentView原理过程为例,会贴出"大部分"源码并进行跟踪,也会提到一些快捷键的用法,或许废话较多,或许整个过程会柳暗花明峰回路转,转来转去。但若秉持一颗学习之心,相信还是能有所收获。
初见尾随:
首先我们从最开始的地方看:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
点进去我们平日调用的setContentView我们可以看到:
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
private AppCompatDelegate mDelegate;
...
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
...
}
嗯...看到是调用了getDelegate()返回对象的setContentView方法,我萌看看他是个啥:
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
里面调用了一个AppCompatDelegate.create()方法,那我们继续点进去看看:
public abstract class AppCompatDelegate {
...
/**
* Create an {@link androidx.appcompat.app.AppCompatDelegate} to use with {@code activity}.
*
* @param callback An optional callback for AppCompat specific events
*/
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
...
}
我们看到这里返回了AppCompatDelegateImpl类,看名字就知道这才是setContentView调用实现的真正方法:
AppCompatDelegateImpl解剖:
class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
private ViewGroup mSubDecor;
...
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
...
}
可以看到这里代码也是十分简洁,直接从一个叫mSubDecor的变量里找到R.id.content的ViewGroup然后直接把setContentView里我们设置的id渲染进去就好了,那么问题来了这个mSubDecor是什么东西,哪里初始化,为什么会有
R.id.content这个视图。
![](https://img.haomeiwen.com/i3525098/533380b93711c2d1.png)
从调用跟踪我们看到仅有一个叫ensureSubDecor的方法里初始化了他,也就是图前面代码里setContentView函数调用的第一行语句:ensureSubDecor():
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor(); //初始化mSubDecor
// If a title was set before we installed the decor, propagate it now
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
if (mDecorContentParent != null) {
mDecorContentParent.setWindowTitle(title);
} else if (peekSupportActionBar() != null) {
peekSupportActionBar().setWindowTitle(title);
} else if (mTitleView != null) {
mTitleView.setText(title);
}
}
applyFixedSizeWindow();
onSubDecorInstalled(mSubDecor);
mSubDecorInstalled = true; //初始化完设置的标志
// Invalidate if the panel menu hasn't been created before this.
// Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
// A pending invalidation will typically be resolved before the posted message
// would run normally in order to satisfy instance state restoration.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!mIsDestroyed && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
可以看到ensureSubDecor函数确保mSubDecor初始化成功,并会用mSubDecorInstalled的布尔值去标志他,而其他代码也不用怎么看,毕竟不是我们的目的。因此真正的初始化是在createSubDecor()函数里:
private ViewGroup createSubDecor() {
---------------------- 拿取定义好的属性值然后设置,并不怎么用关注的代码
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
//这里到下面都是拿其中R.styleable.AppCompatTheme定义的属性值然后设置而已。
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
----------------------
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView(); //这段代码意义不明
----------------------根据不同情况inflate出来不同的subDecor,可以看看判断语句的条件了解了解
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
/**
* This needs some explanation. As we can not use the android:theme attribute
* pre-L, we emulate it by manually creating a LayoutInflater using a
* ContextThemeWrapper pointing to actionBarTheme.
*/
TypedValue outValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
Context themedContext;
if (outValue.resourceId != 0) {
themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
} else {
themedContext = mContext;
}
// Now inflate the view using the themed context and set it as the content view
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
/**
* Propagate features to DecorContentParent
*/
if (mOverlayActionBar) {
mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (mFeatureProgress) {
mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
}
if (mFeatureIndeterminateProgress) {
mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
}
}
} else {
if (mOverlayActionMode) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
if (Build.VERSION.SDK_INT >= 21) {
// If we're running on L or above, we can rely on ViewCompat's
// setOnApplyWindowInsetsListener
ViewCompat.setOnApplyWindowInsetsListener(subDecor,
new OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
final int top = insets.getSystemWindowInsetTop();
final int newTop = updateStatusGuard(top);
if (top != newTop) {
insets = insets.replaceSystemWindowInsets(
insets.getSystemWindowInsetLeft(),
newTop,
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom());
}
// Now apply the insets on our view
return ViewCompat.onApplyWindowInsets(v, insets);
}
});
} else {
// Else, we need to use our own FitWindowsViewGroup handling
((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
new FitWindowsViewGroup.OnFitSystemWindowsListener() {
@Override
public void onFitSystemWindows(Rect insets) {
insets.top = updateStatusGuard(insets.top);
}
});
}
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
----------------------
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
---------------------- 从这里开始是我们真正关注的代码:
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).
// Remove this as we handle it ourselves
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
----------------------
contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
@Override
public void onAttachedFromWindow() {}
@Override
public void onDetachedFromWindow() {
dismissPopups();
}
});
return subDecor;
}
ps:代码用非常不专业的分割线切割了,可以跟着提示看看,我们最主要看最后一段代码:
我先描述一下这里的代码逻辑:
1.首先前面已经根据各种属性(如有无标题,是不是悬浮窗等)初始化出subDecor。
2.从subDecor中找出一个id为R.id.action_bar_activity_content的子视图ContentFrameLayout,同时我们通过mWindow变量找出一个叫android.R.id.content的子视图ViewGroup。为了方便描述我们将上述两个分别称为sdChild和mwChild
3.将mwChild即id为android.R.id.content子视图的内容全部移除并加入到sdChild中
4.将mwChild的id取消,反而将sdChild的id设置为android.R.id.content
5.最后调用mWindow的setContentView方法,将subDecor设置进去。
概括:其实上面的步骤就是将原本mWindow里id为android.R.id.content的子视图替换为subDecor的id为R.id.action_bar_activity_content的子视图。所以从结果上来看,我们在Activity中调用setContentView方法只是替换了mWindow中id为android.R.id.content的子视图罢了。也就是我们set的不是全部,只是一小部分。
那么问题来了这个sdChild到底是个啥?
刚刚说了,上面会根据设置属性的情况初始化不同xml的subDecor,但无论如何最后都会用id:R.id.action_bar_activity_content找到这个子视图,那么我们随便找一个初始化时的xml文件看看就行:
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
...
} else if (mHasActionBar) {
...
} else {
...
} else{
...
}
}
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
那我们就找这个叫abc_dialog_title_material的布局文件吧,然后你会发现你的文件夹里从来么的这个东西,因为他是属于源码不可访问的一部分,我们必须去看系统源码,于是为了节省空间(整个安卓源码很大,我是指真的很大的那种)我们选择在线阅读:https://www.androidos.net.cn/sourcecode
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.appcompat.widget.ActionBarOverlayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/decor_content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<include layout="@layout/abc_screen_content_include"/>
<androidx.appcompat.widget.ActionBarContainer
android:id="@+id/action_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
style="?attr/actionBarStyle"
android:touchscreenBlocksFocus="true"
android:gravity="top">
<androidx.appcompat.widget.Toolbar
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationContentDescription="@string/abc_action_bar_up_description"
style="?attr/toolbarStyle"/>
<androidx.appcompat.widget.ActionBarContextView
android:id="@+id/action_context_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:theme="?attr/actionBarTheme"
style="?attr/actionModeStyle"/>
</androidx.appcompat.widget.ActionBarContainer>
</androidx.appcompat.widget.ActionBarOverlayLayout>
没错就是儿他,别忘了我们是为了找到他里面一个id叫action_bar_activity_content的布局,然而并没有发现。但是仔细看你会发现上面的布局文件里有一个include的标签:
<include layout="@layout/abc_screen_content_include"/>
点开之后:
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.appcompat.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
就是他了!那个id叫action_bar_activity_content的控件,他就是最终替换掉mwindow变量里id叫android.R.id.content布局的控件!说白了就是当我们每次在activity里调用setContentView(id)时,都会把你调用方法里的id布局用 LayoutInflater.from(mContext).inflate进去,也就是我们在AppCompatDelegateImpl里看到的那个代码:
class AppCompatDelegateImpl extends AppCompatDelegate
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
private ViewGroup mSubDecor;
...
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
...
}
我们可以看到,我们setContentView的内容是被inflate进去的,而inflate进去其实就是add进一个父布局里,也就是说我们setContentView的内容会成为id为android.R.id.content的视图的子视图,即上面布局文件中ContentFrameLayout的子视图。
那么ContentFrameLayout是什么呢?我们尝试用ctrl+shift+n搜这个文件:
public class ContentFrameLayout extends FrameLayout {
...
}
然后我们发现这个所谓的ContentFrameLayout 其实是FrameLayout 的子类,也就是说我们setContentView的内容会成为FrameLayout的子视图。
没错这就是我们经常听到的"我们所写的布局都会默认在FrameLayout布局里面"的原因所在。
除了我们用setContentView设置的布局外,其实按照属性设置的不同还会有其他不同的控件,就是上面mSubDecor初始化的过程。
好那么问题来了,我们一定要用setContentView这个方法来显示布局吗?
想必如果大家认真看过上面的过程且文章语句流畅无太大错别字的话就很明显发现:
window早在我们调用setContentView前存在,而我们调用setContentView只是替换了window里一个叫android.R.id.content的视图而已
所以我们只要直接找到这个window里id的内容,对其进行操作就好:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewGroup: ViewGroup = window.findViewById(android.R.id.content)
viewGroup.addView(TextView(this).also { it.text = "aaaa" })
//setContentView(R.layout.activity_main)
}
}
欢呼吧~从今天开始你们就从"不调用setContentView就不能初始化视图"的固有印象中脱离了。
余韵
这个window到底是什么?他原来的视图是什么?为什么会包含id为android.R.id.content的子视图?原来我们调用setContentView方法只是替换视图,而不是调用视图的绘制方法绘制出来。其实在决定写这篇文章之前我一直觉得setContentView是一定要调用的。但在看了view绘制过程相关文章以及这次setContentView的跟踪后,我进一步确信,window起着一个很关键的作用,你是否可还记在文章里调用方法createSubDecor用来将mSubDecor初始化时出现的这句话,就是在用:
...
mWindow.setContentView(subDecor);
...
最终content内容已替换好的subDecor被setContentView进去了mWindow里。因此下一篇文章,就是对mWindow的setContentView的跟踪。