美文网首页Android developerandroid干货源码分析
Android View源码解读:浅谈DecorView与Vie

Android View源码解读:浅谈DecorView与Vie

作者: 丶蓝天白云梦 | 来源:发表于2016-05-22 21:18 被阅读16328次

前言

对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制、测量、布局、绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解、研究。本系列文章将会为大家带来View的工作流程详细解析。在深入接触View的测量、布局、绘制这三个流程之前,我们从Activity入手,看看从Activity创建后到View的正式工作之前,所要经历的步骤。以下源码均取自Android API 21。

从setContentView说起

一般地,我们在Activity中,会在onCreate()方法中写下这样一句:

setContentView(R.layout.main);

显然,这是为activity设置一个我们定义好的main.xml布局,我们跟踪一下源码,看看这个方法是怎样做的,Activity#setContentView:

public void setContentView(@LayoutRes int layoutResID) {
     getWindow().setContentView(layoutResID);  //调用getWindow方法,返回mWindow
     initWindowDecorActionBar();
}
...
public Window getWindow() {   
     return mWindow;
}

从上面看出,里面调用了mWindow的setContentView方法,那么这个“mWindow”是何方神圣呢?尝试追踪一下源码,发现mWindow是Window类型的,但是它是一个抽象类,setContentView也是抽象方法,所以我们要找到Window类的实现类才行。我们在Activity中查找一下mWindow在哪里被赋值了,可以发现它在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) {
        ...
        mWindow = new PhoneWindow(this);
        ...
    }

我们只看关键部分,这里实例化了PhoneWindow类,由此得知,PhoneWindow是Window的实现类,那么我们在PhoneWindow类里面找到它的setContentView方法,看看它又实现了什么,PhoneWindow#setContentView:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) { // 1
        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); // 2
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

首先判断了mContentParent是否为null,如果为空则执行installDecor()方法,那么这个mContentParent又是什么呢?我们看一下它的注释:

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;

它是一个ViewGroup类型,结合②号代码处,可以得知,这个mContentParent是我们设置的布局(即main.xml)的父布局。注释还提到了,这个mContentParent是mDecor本身或者是mDecor的一个子元素,这句话什么意思呢?这里先留一个疑问,下面会解释。

这里先梳理一下以上的内容:Activity通过PhoneWindow的setContentView方法来设置布局,而设置布局之前,会先判断是否存在mContentParent,而我们设置的布局文件则是mContentParent的子元素。

创建DecorView

接着上面提到的installDecor()方法,我们看看它的源码,PhoneWindow#installDecor:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); // 1
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); // 2
        ...
        } 
    }
}

首先,会执行①号代码,调用PhoneWindow#generateDecor方法:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

可以看出,这里实例化了DecorView,而DecorView则是PhoneWindow类的一个内部类,继承于FrameLayout,由此可知它也是一个ViewGroup。
那么,DecroView到底充当了什么样的角色呢?
其实,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。在该布局下面,有标题view和内容view这两个子元素,而内容view则是上面提到的mContentParent。我们接着看②号代码,PhoneWindow#generateLayout方法

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        // 从主题文件中获取样式信息
        TypedArray a = getWindowStyle();

        ...

        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }

        if(...){
            ...
        }

        // Inflate the window decor.
        // 加载窗口布局
        int layoutResource;
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if(...){
            ...
        }

        View in = mLayoutInflater.inflate(layoutResource, null);    //加载layoutResource
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中添加子View,即mContentParent
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 这里获取的就是mContentParent
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // Remaining setup -- of background and title -- that only applies
        // to top-level windows.
        ...

        return contentParent;
    }

由以上代码可以看出,该方法还是做了相当多的工作的,首先根据设置的主题样式来设置DecorView的风格,比如说有没有titlebar之类的,接着为DecorView添加子View,而这里的子View则是上面提到的mContentParent,如果上面设置了FEATURE_NO_ACTIONBAR,那么DecorView就只有mContentParent一个子View,这也解释了上面的疑问:mContentParent是DecorView本身或者是DecorView的一个子元素
用一幅图来表示DecorView的结构如下:

DecorView结构

小结:DecorView是顶级View,内部有titlebar和contentParent两个子元素,contentParent的id是content,而我们设置的main.xml布局则是contentParent里面的一个子元素。

在DecorView创建完毕后,让我们回到PhoneWindow#setContentView方法,直接看②号代码: mLayoutInflater.inflate(layoutResID, mContentParent);这里加载了我们设置的main.xml布局文件,并且设置mContentParent为main.xml的父布局,至于它怎么加载的,这里就不展开来说了。

到目前为止,通过setContentView方法,创建了DecorView和加载了我们提供的布局,但是这时,我们的View还是不可见的,因为我们仅仅是加载了布局,并没有对View进行任何的测量、布局、绘制工作。在View进行测量流程之前,还要进行一个步骤,那就是把DecorView添加至window中,然后经过一系列过程触发ViewRootImpl#performTraversals方法,在该方法内部会正式开始测量、布局、绘制这三大流程。至于该一系列过程是怎样的,因为涉及到了很多机制,这里简单说明一下:

将DecorView添加至Window

每一个Activity组件都有一个关联的Window对象,用来描述一个应用程序窗口。每一个应用程序窗口内部又包含有一个View对象,用来描述应用程序窗口的视图。上文分析了创建DecorView的过程,现在则要把DecorView添加到Window对象中。而要了解这个过程,我们首先要简单先了解一下Activity的创建过程:
首先,在ActivityThread#handleLaunchActivity中启动Activity,在这里面会调用到Activity#onCreate方法,从而完成上面所述的DecorView创建动作,当onCreate()方法执行完毕,在handleLaunchActivity方法会继续调用到ActivityThread#handleResumeActivity方法,我们看看这个方法的源码:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { 
    //...
    ActivityClientRecord r = performResumeActivity(token, clearHide); // 这里会调用到onResume()方法

    if (r != null) {
        final Activity a = r.activity;

        //...
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow(); // 获得window对象
            View decor = r.window.getDecorView(); // 获得DecorView对象
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager(); // 获得windowManager对象
            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); // 调用addView方法
            }
            //...
        }
    }
}

在该方法内部,获取该activity所关联的window对象,DecorView对象,以及windowManager对象,而WindowManager是抽象类,它的实现类是WindowManagerImpl,所以后面调用的是WindowManagerImpl#addView方法,我们看看源码:

public final class WindowManagerImpl implements WindowManager {    
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}

接着调用了mGlobal的成员函数,而mGlobal则是WindowManagerGlobal的一个实例,那么我们接着看WindowManagerGlobal#addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            root = new ViewRootImpl(view.getContext(), display); // 1

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView); // 2
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

先看①号代码处,实例化了ViewRootImpl类,接着,在②号代码处,调用ViewRootImpl#setView方法,并把DecorView作为参数传递进去,在这个方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上,在这个过程中,ViewRootImpl、DecorView和WMS会彼此关联,至于详细过程这里不展开来说了。
最后通过WMS调用ViewRootImpl#performTraverals方法开始View的测量、布局、绘制流程,这三大流程在下文会详细讲述,谢谢大家的阅读。

相关文章

网友评论

  • 小郑太:WindowManager是接口,不是抽象类哦
  • 174232d9bdc9:大神,求助 自定义 toast 绕过通知权限,这一块的问题。刚学android 没多久,问题链接
    https://github.com/Blincheng/EToast2/issues/3
  • IT枫:这个mContentParent是mDecor本身或者是mDecor的一个子元素//验证这一句真假
    mDecor 对象初始化
    调用函数 mDecor = generateDecor();
    ```
    protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
    }
    ```
    mContentParent 对象初始化
    调用函数
    mContentParent = generateLayout(DecorView decor);
    ```
    ViewGroup generateLayout(DecorView decor) {
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    return contentParent;
    }
    ```
    无论如何都是两个对象,怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    怎么能扯到mContentParent是mDecor本身这样的结论呢!!!
    蓄_:博主写的没问题的。
    在generateLayout(DecorView decor)方法中,有一处代码是这样的:
    // Inflate the window decor.
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
    layoutResource = R.layout.screen_swipe_dismiss;
    }
    ....
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    (你到DecorView里面去看onResourcesLoaded方法就知道是给DecorView设置布局文件,每一个布局文件都要一个R.id.content的子布局)。
    然后有一段代码:
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    这个在PhoneWindow里面的源码是:
    public View findViewById(@IdRes int id) {
    return getDecorView().findViewById(id);
    }
    所以说mContentParent是mDecor 的一部分。为什么可能会是mDecor 本身啦?是想,假设上面的布局(layoutResource)里面只有一个R.id.content,那么是不是它本身?
  • b707c3937ef8:受教,作者的文章我会一一看完
  • 577cda791ed6:如果上面设置了FEATURE_NO_ACTIONBAR,那么DecorView就只有mContentParent一个子View,这也解释了上面的疑问:mContentParent是DecorView本身或者是DecorView的一个子元素。请问作者是decor view本身如何解释的?
    临窗听雨:我对这里也有点不懂,只有一个子view也不是本身吧
  • 冷师傅_:楼主你好,但是我测了一下DecorView的宽高和手机屏幕是一致的,那么状态栏到底算不算在DecorView里呢?
    丶蓝天白云梦:状态栏属于系统级别的UI,不包括在当前Activity的UI上,DecorView的宽高和手机屏幕一样是正常的,你可以查看下DecorView的层级关系,会发现DecorView的ActionBar的位置是处于某一个值之下的,而这个值正好是StatusBar的高度,所以推测DecorView在绘制子View的时候会预留一部分空间给StatusBar,而StatusBar则是有系统自身进行绘制。
  • 608924568ff8:你好 我看的android24的源码 PhoneWindow的generateLayout()方法中并没有decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 我也没有找到关联的相关代码 楼主知道吗
    yyhat2:在函数onResourcesLoaded中
    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 {

    // Put it below the color views.
    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
  • f0c819a54656:DecorView确实不包括状态栏,但是DecorView的唯一子类是一个LinearLayout,并且在LinearLayout中除了contentParent,还有另一个ViewStub。所以“mContentParent是DecorView本身”这个结论有待考察。以上是我通过Debug真机查看的,测试机小米4C,Rom是MIUI 7.0
    丶蓝天白云梦:@刺猬Spiny 嗯嗯,多谢你的指出,学习了:smile:
    f0c819a54656:@陈育 刚刚做完实验,是会有ViewStub的。
    具体代码

    ```
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    View contentParent = findViewById(android.R.id.content);
    if(contentParent!=null){
    LinearLayout vg1 = (ViewGroup) contentParent.getParent();
    DecorView vg2 = (ViewGroup)vg1.getParent();
    ViewRootImpl viewRoot = (ViewRootImpl)vg2.getParent();
    }
    }
    ```
    但是这些分析不适用AppCompatActivity。AppCompatActivity在contentParent和DecorView之间还嵌套了其他的Layout。
    丶蓝天白云梦:@刺猬Spiny ViewStub估计是为了延迟加载titlebar而存在的,如果主题是noActionbar,是不是就没有ViewStub了呢?
  • 擒贼先擒王:DecorView不包括状态栏???
    丶蓝天白云梦:@fewwind 状态栏属于系统级别的UI,不包括在当前Activity的UI上,DecorView的宽高和手机屏幕一样是正常的,你可以查看下DecorView的层级关系,会发现DecorView的ActionBar的位置是处于某一个值之下的,而这个值正好是StatusBar的高度,所以推测DecorView在绘制子View的时候会预留一部分空间给StatusBar,而StatusBar则是有系统自身进行绘制。
    2eb56199844d:@陈育 我测试是包括的。720p的手机得出来1280?
    丶蓝天白云梦:@擒贼先擒王 不包括不包括
  • acjiji:在②号代码处,调用ViewRootImpl#setView方法,并把DecorView作为参数传递进去,在这个方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上
    =========================
    想看最终是如何添加到Window上的,这里确没有讲,最终虽然知道是被添加到Window上面,但是感觉这里还是没有讲明白
    itcayman:@acjiji 详细绘制过程是交给WindowManagerService与Surfaceflinger来进行的,具体可以研究这两个类源码。
  • d895bee483f8:目前看到有关DecorView RootView写的最清晰一篇文章
    丶蓝天白云梦:@1千山鸟飞绝1 谢谢!
  • 捡淑:马克
  • 楚云之南: View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
    throw new RuntimeException("Window couldn't find content container view");
    }

    这里什么时候decorview == contentParent?并没有看到decorview什么时候被设置一个ID=ID_ANDROID_CONTENT
    kerwin92:@楚云之南 docerView为什么会等于contentParent?DecorView是在PhoneWindow中定义的一个内部类,继承了FrameLayout。在Activity中,DecorView就是根View,我们设置的Theme以及setContentView的内容都是呈现到DecorView之上的
    楚云之南:@kerwin92 那什么时候decorview == contentParent
    kerwin92:@楚云之南 View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));in添加到decor中,而in的布局id layoutResource是根据Theme等信息去加载的系统布局。系统布局里面有提供一个ID_ANDROID_CONTENT的FrameLayout。所以可以直接findViewById了
  • 南梁陈庆之:楼主,我有一个问题:ContentParent是在什么时候被添加到DecorView上的?是“decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))”这句代码吗?但是in和ContentParent有什么联系呢?求楼主解惑。
    丶蓝天白云梦:@陈旸 嗯 可以这样理解
    南梁陈庆之:@陈育 可以理解为in是DecorView的实际布局,contentParent是in布局中一个id为R.id.content的FrameLayout吗?
    丶蓝天白云梦:@陈旸 对的,其实in是加载后的窗口布局,而contenParent是其中的控件实例(通过findviewbyid)获得的

本文标题:Android View源码解读:浅谈DecorView与Vie

本文链接:https://www.haomeiwen.com/subject/hzmorttx.html