[源码分析]Android之setContentView

背景


在每个 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;
}
...

到此,我们可以看到:

  1. 首先activity将会被创建;
  2. activity中创建Window(实际上是PhoneWindow)对象;
  3. 在OnCreate的setContentView中,将我们的布局放置到decorView中,然后,将decorView set进了Window中;
  4. 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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? 知道Android...
    CoorChice阅读 26,547评论 42 312
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,288评论 25 708
  • 时间颗粒度就是一个人安排时间的基本单位。 我觉得一个越成功的人,他的颗粒度才会越小!时间颗粒度一定和这个人的地位成...
    畅畅_阅读 219评论 0 3
  • 夏季六月十三日,永江携夫人秀红女士,返第二故乡葫芦岛,軍营战友去北站迎接,单位同事邹女士也从兴城赶到接站,几十年分...
    广仁阅读 510评论 0 0
  • 表弟可能要去找他对象和他对象的老公动刀子了,而我在幸灾乐祸。 这是一个复杂的故事。 他小我一天,我俩出生日期同年同...
    北芜菇凉阅读 356评论 0 0