对于 setContentView(layoutResId) 方法,相信大家再熟悉不过了,那么对于该方法的内部实现呢?对我个人而言,从来没研究过,前期学习过程中只要不报错,程序能跑起来,就ok,这也就是每次看到别人文章的时候,总是表现出一种我要是这么牛B就行了,哈哈,然而在群演的角色演绎上永无止境的大步向前走,要想改变这种咸鱼现状,只能强迫自己搞点事情啦——阅读操蛋的源码,好了,废话不多说,开始今天的主题。
先点进 Activity 的 setContentView(layoutResId) 瞧一瞧,代码如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
代码很少,第1行,可以看出通过 getWindow 获取到 Window 实例,然后调用该实例的 setContentView(layoutResID) 方法,第2行,从英文意思中可以看到是初始化 ActionBar 之类的东西,暂不做深究。
接着看 getWindow 方法,代码如下:
public Window getWindow() {
return mWindow;
}
该方法直接返回一个 Window 对象,看到这里,目前能肯定的就是在 setContentView 方法执行之前 mWindow 对象就已经被初始化了,这个应该没有什么分歧吧?要不然会报空指针异常的。
那么 mWindow 实例到底是在哪里被初始化?接着找会发现在 Activity 中的 attach 方法中发现了 mWindow 的初始化,代码如下:
mWindow = new PhoneWindow(this, window, activityConfigCallback);
PhoneWindow 又是个什么鬼?继续查阅源码:
public class PhoneWindow extends Window implements MenuBuilder.Callback
可以看出 PhoneWindow 是 Window 的一个子类,并且是唯一的子类。接着可以得出这样一个结论:mWindow 其实是一个 PhoneWindow 类对象,那么也就可以看出,在最上面的源码中,其实是调用了 PhoneWindow 类中的 setContentView 方法。
按照上面的分析,直接在 PhoneWindow 类中寻找 setContentView 方法,查到的结果总共有三个重载方法,代码如下:
1、public void setContentView(int layoutResID)
2、public void setContentView(View view)
3、public void setContentView(View view, ViewGroup.LayoutParams params)
根据上面传参,我们可以确定,调用的是第1个方法,接着展开方法,代码如下:
@Override
public void setContentView(int layoutResID) {
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;
}
mContentParent 是一个 ViewGroup 类型的全局变量,刚进入方法中,mContentParent 对象为 null,因此会执行 installDecor 方法,由于 installDecor 方法代码过多,这里只展示关键代码,代码如下:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
mDecor 是 DecorView 的一个实例对象,DecorView 继承于 Fragment 类,它被认为是 Android 系统中 View 视图的根节点。方法开始的时候 mDecor 并没有被初始化,所以会执行方法 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 对象,继续看?上面?的 installDecor 方法,接下来会执行一个 generateLayout 方法,继续点进去看,代码太多,精简一番,代码如下:
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks(contentParent);
}
mDecor.finishChanging();
return contentParent;
}
由上往下看,我们首先会根据不同的窗口特性,来筛选与之对应的布局文件,下面会依次列出 8 种布局文件,分析到这里说个题外话,如果想去掉标题栏 TitleBar,那么我们必须在 setContentView 方法之前调用 requestWindowFeature 方法,否则无效。 然后紧接着会调用 DecorView 中的 onResourcesLoaded 方法,看看该方法到底做了什么事,删减之后,代码如下:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT,MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
上面提到 8 种布局文件
先看 onResourcesLoaded 方法,很明显,传入的布局文件 layoutResource 通过 inflate 获得了 View 对象 root,然后调用 addView 方法,将 root 添加到 DecorView 中,换句话说,将传入的布局文件 layoutResource 添加到 FrameLayout 布局中。再回到 generateLayout 方法中,紧接着又定义了一个 ViewGroup 对象 contentParent,contentParent 是获取布局文件 layoutResource 中 id 为 content 的控件,且该控件为FrameLayout,最后将 contentParent 作为方法的返回值返回。
这 8 种布局文件路径是 frameworks / base / core / res / res / layout,并且这些布局中有一个公共点,全部有一个 id 为 content 的 FrameLayout 控件 ( ID_ANDROID_CONTENT 是Window 中定义的 int 型常量,值为com.android.internal.R.id.content )。
重头梳理一下 generateLayout 方法可以得出这样一个流程:
分析完了 generateLayout 方法,回到 setContentView 方法中,继续执行一个 if 判断语句,代码如下:
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
最后一步,将我们设置的布局 layoutResID 添加到顶级视图 ( DecorView ) 中 id 为 content 的 FrameLayout 控件中,正好也就解释了,设置布局的方法名叫做 setContentView 而不是 setView。
经过上面对 setContentView 的分析,PhoneWindow 和 DecorView 实例被创建完成,那么 DecorView 是何时被添加到主窗口 PhoneWindow 的呢?这个过程是在 onResume 后完成的,ActivityThread 在执行完 performLaunchActivity 后,便会执行 handlerResumeActivity() 代码精简后见下方:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
boolean willBeVisible = !a.mStartedActivity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
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;
wm.addView(decor, l);
}
}
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}
分析 handleResumeActivity 方法内部执行流程:
经过上述前 4 步,就会执行到 onResume 方法, 回到 handleResumeActivity 方法中接着往下看,通过 getDecorView 获取到 DecorView 对象实例 decor,这个没什么多说的,此时 decor 就是我们在上面分析 setContentView 中获取到的 DecorView,通过调用方法 getWindowManager 获取 ViewManager 实例 wm。
通过流程图的方式直观描述一下整体流程:
图片是盗的,不纠结 ~ _~
当获得DecorView对象后,先执行了一次setVisibility(View.INVISIBLE)操作,执行完addView()操作后才会重新设置为VISIBLE,此处的做法类似于SurfaceView绘制过程对Canvas的锁操作,页面的显示需要由过渡动画管理器TranslateManager进行控制,如果直接在可见状态下进行页面绘制,给用户一种页面加载卡顿的感觉,而等待页面全部绘制完毕后再整体展示给用户可以有效的避免这个问题。
Activity 中的 getWindowManager 方法:
public WindowManager getWindowManager() {
return mWindowManager;
}
继续找会发现 mWindowManager 在 Activity 中的 attach 方法中被初始化,关键代码如下:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
正是在 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);
}
继续看 createLocalWindowManager 方法,代码如下:
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
返回的是一个 WindowManagerImpl 对象,因此可以得出 ViewManager 实例 wm 其实是一个 WindowManagerImpl 对象,回到 handleResumeActivity 方法中接着往下看,关键的一句 wm.addView ,那么直接在 WindowManagerImpl 中查询 addView方法,代码如下:
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
可以看到,WindowManagerImpl 其内部方法始终持有 WindowManagerGlobal 的引用 mGlobal,实际也是调用了 WindowManagerGlobal 的 addView,在 WindowManagerGlobal 中查找对应的 addView 方法,代码精简如下:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
ViewRootImpl root;
View panelParentView = null;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
ViewRootImpl 就是一个 ViewTree 管理类,DecorView 作为 ViewTree 根布局。每个传进来的 DecorView 都会创建一个对应的 ViewRootImpl 来管理。ViewRootImpl 将实际控制着 DecorView 的绘制周期,同时还可以与WMS进行Binder通信。ViewRootImpl 在调用 setView 后,即向 WMS 发起了添加请求,WMS 便会将当前的 PhoneWindow 放入自身管理的 Window 列表中,将 DecorView 添加到 PhoneWindow 上,并且通知 ViewRootImpl 进行绘制操作,这样 View 和 Window 就关联起来了。这里关于通信方面的知识,就不多说了,毕竟自己还不是很懂,所以就引用别人的结论。之后,要是弄清楚通信相关的内容会及时更新。
本篇文章是对 View、Activity、Window 三者关系相关知识点的一个归纳、总结,一方面巩固知识,另一方面也要学会从源码角度去分析、处理问题,后续如有相关知识,会持续更新内容,文章中若有错误之处、或不足之处,欢迎指正!