背景
在每个 Activity 中,一般为了操作各种 view ,首先要做的就是 setContentView。我们总是在使用它,但是实际上它运作的背后到底发生了什么呢?一直想找时间深入了解一下,多读读源码,学习下别人优秀的表达。碰巧遇到视频在view上的播放问题,趁机走一波(源码版本,Android-25.)。
setContentView
通过下面的图,我们来看看这个方法到底做了些什么:
setContentView
对照实际关系,如下图所示:
实例
上图中所set进去的mContentView布局文件为:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/holo_blue_light"
android:gravity="center"
xmlns:android="http://schemas.android.com/apk/res/android" >
<TextView
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@android:color/holo_red_light"
android:text="Hello\nWorld"
android:textStyle="bold"
android:textSize="20sp"
android:gravity="center"
android:layout_gravity="center"/>
</LinearLayout>
是不是对“神秘button”有点好奇?布局文件中根本没有这个控件啊?是的。此处插播一个小曲,简单说明下这个button的来历,是用了addContentView方法。
addContentView(btn, new FrameLayout.LayoutParams(params));
该方法的源码为:
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// TODO Augment the scenes/transitions API to support this.
Log.v(TAG, "addContentView does not support content transitions");
}
//直接添加到mContentParent,与mContentView同级
mContentParent.addView(view, params);
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
结论
回到主题,在一个window中,所有view共同的祖先,是 decorview。先有 decorview,然后在 decorview上进行层层绘制。
setContentView前后,更进一步,更多细节
setContentView是在onCreate中执行的。在此之前,谁在调用onCreate呢?
在ActivityThread的performLaunchActivity方法中,有几个关键步骤:
...
//创建Activity
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
//创建该Activity的PhoneWindow对象
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);
...
//执行onCreate回调,同时,在该回调方法的setContentView中,decorView被set进PhoneWindow中
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
//执行onStart回调
if (!r.activity.mFinished) {
activity.performStart();
r.stopped = false;
}
...
到此,我们可以看到:
- 首先activity将会被创建;
- activity中创建Window(实际上是PhoneWindow)对象;
- 在OnCreate的setContentView中,将我们的布局放置到decorView中,然后,将decorView set进了Window中;
- onStart回调执行。
此时,其实我们仍然不清楚,setContentView之后,我们的布局该如何显示给用户呢?
其实,performLaunchActivity执行后,还有一个重要的方法将要执行,那就是handleResumeActivity,我们接着看它的细节。
...
//执行onResume回调
r = performResumeActivity(token, clearHide, reason);
...
r.window = r.activity.getWindow();
//拿出decorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//拿出WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
//将decorView 放置到WindowManager中
wm.addView(decor, l);
}
...
由上面一些列过程,可知最终,decorView交给了WindowManager处理。这个关键的交接节点,就是上面的wm.addView方法。
该方法的执行,内部充斥这本地进程和服务进程的跨进程通信过程。最终,在WindowManagerGlobal实现:
...
root = new ViewRootImpl(view.getContext(), display);
...
//decorView被交给了WindowManagerGlobal中的ViewRoot
root.setView(view, wparams, panelParentView);
...
此后,decorView被装进了ViewRoot中。而在ViewRoot里包含了一个Surface,decorView将被绘制在这个Surface中。
用户感知和性能提升
先来说明下帧率和刷新率。
所谓帧率,是对于GPU而言,每秒钟能够绘制的帧数,单位是FPS(帧数/秒),Android设备的标准帧率是60FPS;
所谓刷新率,是指每秒钟屏幕更新的次数,单位是赫兹(Hz),大部分Android设备的刷新率是60Hz。
其实当每秒钟帧数(帧率)达到10-12帧,人们就可以感知到运动,但是这会产生运动模糊。我们都知道,在Android设备上,App的帧率是60FPS,这就意味着每一帧必须在16.667ms内完成绘制,否则会出现丢帧。丢帧如果很严重,将会导致的糟糕的交互体验,更严重的是,由于资源消耗严重,造成卡顿。
就上面提到的View 的层次关系,是否存在改善性能的空间呢?
首先需要尽可能减少布局层次,然后,再从每个view的绘制上下功夫。
SDK文档中指出:
Draw traversal performs several drawing steps which must be executed
in the appropriate order:
1. Draw the background
2. If necessary, save the canvas' layers to prepare for fading
3. Draw view's content
4. Draw children
5. If necessary, draw the fading edges and restore layers
6. Draw decorations (scrollbars for instance)
在绘制View的时候,首先将要绘制的就是背景。实际上,这么多View的层次关系,我们需要的只有一个背景,其他的对于GPU来说都是毫无意义的负担。
所以,对于一个Window来说,注意设置一个背景就够了。
getWindow().getDecorView().setBackground(null);
或者
view.setBackground(null);
网友评论