探究 Android 界面的显示机制(我可能看到了假的视图等级)

1,264 阅读8分钟

本文主要目的:
探究Android界面显示机制,在Android视图等级方面提出了个人见解,以及发现WindowsManager的真正用图是对Windows上的DecorView进行管理,而不是对Window进行管理。解释了requestWindowFeature(Window.FEATURE_NO_TITLE)用来设置全屏显示一定要放在setContentView()方法前面才会生效。还有自定义view的onMeasure、onLayout和onDraw方法的调用时机。

正文:
如果想展示一个界面,Android工程师的一般工作流程是,根据产品的原型图,设计布局文件,然后在Activity中的setContentView中加载布局文件,初始化各种view,编译运行,打开这个Activity的时候,就可以查看到布局文件的视图。
所以问题就来了,Android系统是怎样把我们自定义的布局文件显示出来的?

多数程序员认为的Android的视图等级


多数程序员认为的Android的视图等级


这个图应该很常见,基本上所有的资料在介绍视图等级的时候都会拿出上图来进行介绍介绍DecorView是顶级view,巴拉巴拉。。。
不过关于视图等级用这幅图进行表示,我一点都不认同,可以这么说,这样划分是错误的(仅仅是个人见解,有讨论的欢迎一起讨论)
为什么这么说呢?
View的中文意思是视图,既然要讲视图等级,而且DecorView又是顶级视图,所以我认为Window(窗口)和Activity(活动)都可以去掉,变成这样。


Android的真正视图等级



讲到这里,可能会有些人激动了,WTF!讲的啥玩意!Window呢?Window可是窗口啊!WindowsManager可是对窗口进行管理啊(其实我对WindowsManager是对窗口进行管理这句我也不认同,后续会说)

从简单的布局文件开始入手

为了不至于枯燥,先从一个小例子说起:
老板:当点击这个App的时候,要在界面上显示我的照片!五分钟后交工!
码农:好的,遵旨。
然后码农熟练的打开了IDE,
定义了BossActivity.java
自定义布局文件 activity_boss_layout.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.MainActivity" >
    <ImageView 
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:src="@drawable/activity_boss"/>
 </RelativeLayout>

里面设置了老板的照片当做背景
然后在BossActivity 的onCreate方法中设置布局文件

 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_boss_layout);
}

配置成主启动类,编译、执行,ok,老板的头像出来了,搞定!三分钟!
我们看到布局文件是在setContentView方法进行设置的,那我们就进入这个函数

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里调用的的getWindows的setContentView方法,那我看一下这个getWindo这个windows是什么,以及在哪进行的初始化

public Window getWindow() {
    return mWindow;
}

mWindow是在哪初始化的呢?继续定位,我们发现是在Activiy的Attach方法进行的初始化,它是一个PhoneWindow类。

 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) {
    attachBaseContext(context);
//...省略一些代码
    mWindow = new PhoneWindow(this);
//...省略一些代码
}

所以,这里调用的的PhoneWindow的setContentView方法。

 public void setContentView(int layoutResID) {
 //当mContentParent  为空的时候先构建DecorView,同时初始化mContentParent
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
   //...省略一些代码
       mLayoutInflater.inflate(layoutResID, mContentParent);
 //...省略一些代码
}

此处代码的意思是
1.如果mContentParent 为空的时候,这个时候要对DecorView进行初始化。
2.通过LayoutInflate服务将该ID对应的资源文件解析成view,并且添加到它的父view(mContentParent)中。
关于LayoutInflate服务将该ID对应的资源文件解析成view 这个过程(也就是mLayoutInflater.inflate(layoutResID, mContentParent);这个过程),可以关注我的后续博文(最近公司活比较多,没太多时间写)
再看一下initDecor()

 private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();//这里初始化DecorView
     //...省略一些代码
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);//这里产生了contentParentView
   //...省略一些代码
}

在这里我们知道布局文件解析成的view必须添加到mContenParent这个view中,而而mContentParent是通过DecorView产生的,但是它们三者的关系是什么样的呢?
这里我们不从具体的代码分析了,分析过多容易晕,直接从应用显示的View视图进行分析。这里我们用了Hierarchy Viewer工具,
在层级图中,先找到我们自定义的布局,这是一个相对布局,其中包含了一个ImageView


此处输入图片的描述


继续向上,FrameLayout是什么?根据上文的理解,它直接包含了我们自定义的布局文件,应该就是mCotentParent这个view。


此处输入图片的描述


那和它并列的布局是什么呢?


此处输入图片的描述


这就是我们ActionBar所在的布局位置,里面的具体内容就不分析了。

继续向上分析,这个view是一个viewGroup,但还不是一个顶级view,我们查看它的Id,decor_content_parent,它应该是承载actionBar和mCotentParent 的父布局。 看来mContentParent并不是DecorView的子view,在其中间还有一个View,所以mContentParent应该是Decor的子view的子view。


此处输入图片的描述


继续向上,最后的FrameLayout才是顶级view,也就是DecorView所处的位置。


此处输入图片的描述

所以DecorView、mContentParent、布局文件View三者的关系是,
DecorView是顶级view,mContentParent是DecorView的子view的子view,布局文件view是mContentparent的子view,当我们要想让我们的布局view显示出来,只要让DecorView显示出来就可以了。
(此处也证明了我之前的观点,视图等级不包括Activit和Window。因为Activity只是包含Window对象,window里面包含DecorView,真正的视图只是DecorView)
(还有一点不知道大家有没有看出来,requestWindowFeature(Window.FEATURE_NO_TITLE)用来设置全屏显示一定要放在setContentView()方法前面才会生效,是因为actionbar等相关初始化工作是在setContentView进行)

让DecorView显示出来

Activity的显示(Dialog的就不分析了,大致过程一样,感兴趣的可以自行实践,是在show方法里)

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {

    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            // 1 这里获取了windows
            r.window = r.activity.getWindow();
            // 2 这里获取了decorview
            View decor = r.window.getDecorView();
            // 3 设置decorview为显示
            decor.setVisibility(View.INVISIBLE);
            // 4 获取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 (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                // 4 将decorview显示出来
                wm.addView(decor, l);
            }
        }
    }
    //省略了一些代码
}

关于第4点,正常解释是将decor添加到wm中,其实完成的功能的将decorview显示出来,为什么我这里没有说是添加呢?其实这不是一个添加的过程,只是WindowManger 对decorview进行管理,将decorview进行显示出来。
这里,我们明显可以看出WindowManger并没有对Window进行管理,而是对Window中的Decorview进行管理。
addView方法是哪里来的?
代码分析发现
WindowManger服务的具体实现类是WindowMangagerImpl

    public void addView(View view, ViewGroup.LayoutParams params) {
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

看来WindowMangagerImpl 也是一个冒牌货,真正是调用的是 mGlobal.addView(view, params, mDisplay, mParentWindow);
mGlobal是WinowMangerGolbal类,让我们看一下它的addView方法。

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    // 省略一些代码
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        // 省略一些代码
        // 1.初始化ViewRootImpl
        root = new ViewRootImpl(view.getContext(), display);
        // 2.给decorview设置参数
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    try {
        // 3 调用ViewRootImpl 的 setView方法,将decorview显示出来
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {

    }
}

可以看到并不是一个addview的过程,最后调用了ViewRootImpl 类的setView方法。

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
            // 省略了很多代码
            requestLayout();
            try {
                // 显示decorview
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel);
            } 
        }
}

requestLayout是请求布局,对要显示的view进行绘制

 @Override
     public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

这里会检测线程是不当前线程,如果不是,则会发生错误,这也是为什么不能再子线程更新UI的原因,但是可以在onCreate方法和onResume方法种的子线程中设置view(更新UI),是因为performResumeActivity 是在其之前执行的,还没有执行requestLayout。

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
    }
}
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
 void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        try {
            performTraversals();
        } 
    }
}

经过上述一系列过程最终会执行我们常见的performTraversals方法

private void performTraversals() {
    // 省略了一些代码
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // 省略了一些代码
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    // 省略了一些代码
    performDraw();
}

下面只列出performMeasure方法,其他同理,只是performDraw中的draw方法比较复杂一些,其中涉及通知surfaceFlinger更新,具体可自行查看。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

在performTraversals 方法中完成了view的测量、定位和绘制。
这也是我们通常在自定义view的时候只要修改onMeasure、onLayout和onDraw方法就能生效的原因。

内容绘制完毕之后,通过
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel);
通知与WindowManagerService(Native层),将绘制的视图显示出来。 这样Decorview就可以显示在手机界面中了。关于mWindowSession与WindowManagerService(Native层)之间的联系,是在viewRootImpl对象的构造函数完成的,感兴趣的可以自行分析。

总结一下,Activity通过setContentView,将布局文件加载成view,并且附在Decorview上,成为DecorView中子view的子子view。在handleResumeActivity中通过WindowManager将Decorview显示出来,其中view的绘制以及通知native层的WindowManagerService显示Decorview的过程,是通过WindowManagerGlobal中的ViewRootImpl来完成的。