Android系统源码分析–View绘制流程之-setContentView

作者 : 开心源码 本文共12892个字,预计阅读时间需要33分钟 发布时间: 2022-05-12 共103人阅读

上一篇分析了四大组件之ContentProvider,这也是四大组件最后一个。因而,从这篇开始我们分析新的篇章–View绘制流程,View绘制流程在Android开发中占有非常重要的位置,只需有视图的显示,都离不开View的绘制,所以理解View绘制原理对于应用开发以及系统的学习至关重要。因为View绘制流程比较复杂,并且涉及的知识非常多,所以后面我会按照下面几方面来详情View的绘制流程。每篇不是很长,但是尽量的详细,让每个人都看懂。

  • Android系统源码分析–View绘制流程之-setContentView
  • Android系统源码分析–View绘制流程之-inflate
  • Android系统源码分析–View绘制流程之-onMeasure
  • Android系统源码分析–View绘制流程之-onLayout
  • Android系统源码分析–View绘制流程之-onDraw
  • Android系统源码分析–View绘制流程之-硬件加速
  • Android系统源码分析–View绘制流程之-addView
  • Android系统源码分析–View绘制流程之-弹性效果

所以这篇我们先分析View绘制流程的setContentView方法,按照惯例,先贴一下流程图:

setContentView.jpg

1.PhoneWindow.setContentView

调用setContentView最开始的地方是在我们继承Activity的子类中的onCreate方法中,这个方法其实是调用的Activity中的setContentView方法:

    public void setContentView(@LayoutRes int layoutResID) {        // getWindow获取的是PhoneWindow,所以这里是调用的PhoneWindow的setContentView方法        getWindow().setContentView(layoutResID);        initWindowDecorActionBar();    }

其实这个getWindow获取的是继承Window的PhoneWindow,所以这里getWindow.setContentView是调用的PhoneWindow.setContentView方法,具体的自己可以看看代码哪里赋值的就知道了。另外这个方法还有两个相似的方法:

    public void setContentView(View view) {        getWindow().setContentView(view);        initWindowDecorActionBar();    }    public void setContentView(View view, ViewGroup.LayoutParams params) {        getWindow().setContentView(view, params);        initWindowDecorActionBar();    }

这三个方法差不多,只不过下面的两个直接传递了view对象,而第一个是传递了view的id。我们接着看PhoneWindow.setContentView方法。

    public void setContentView(int layoutResID) {        // 根据layout的id加载一个布局,而后通过findViewById(R.id.content)加载出布局中id为content        // 的FrameLayout赋值给mContentParent,并且将该view增加到mDecor(DecorView)中        if (mContentParent == null) {// 第一次是空            installDecor();        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {            // 没有过度效果,并且不是第一次setContentView,那么要先移除盛放setContentView传递进来            // 的View的父容器中的所有子view            mContentParent.removeAllViews();        }        // 窗口能否需要过度显示        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {            ...        } else {// 不需要过度,加载id为layoutResID的视图并且增加到mContentParent中            mLayoutInflater.inflate(layoutResID, mContentParent);        }        // 绘制视图        mContentParent.requestApplyInsets();        ...        mContentParentExplicitlySet = true;    }

上面注释很详细,但是还是需要解释一下mContentParent,这个mContentParent是一个FrameLayout,这里的Content是指你setContentView传递进来的id指向的视图,所以mContentParent也就是指放置传递进来的视图的父视图。看下面的图:

ContentView.png

上面的ActionBarContextView是标题,不过有些设置是不会显示整个标题的,所以这里只是一种情况,下面的id为content的FrameLayout就是这个mContentParent,你通过setContentView方法传递的视图会放到这个id为content的FrameLayout上面,这样你的Activity就显示了你写的布局视图了,这里先解释一下,我们下面看看是不是真的这样。因为第一次创立Activity时mContentParent是空的,所以会走PhoneWindow.installDecor方法。

2.PhoneWindow.installDecor

    private void installDecor() {        mForceDecorInstall = false;        // 继承FrameLayout,是窗口顶级视图,也就是Activity显示View的根View,包含一个TitleView和一个ContentView        if (mDecor == null) {// 初次为空            // 创立DecorView(FrameLayout)            mDecor = generateDecor(-1);            ...        } else {            mDecor.setWindow(this);        }        if (mContentParent == null) {// 第一次setContentView时为空            // 这个mContentParent就是后面从系统的frameworks\base\core\res\res\layout\目录下加载出来            // 的layout布局(这个Layout布局加载完成后会增加到mDecor(DecorView)中)中的一个id为content的            // FrameLayout控件,这个FrameLayout控件用来盛放setContentView传递进来的View            mContentParent = generateLayout(mDecor);            ...            // 判断能否存在id为decor_content_parent的view(我只看到screen_action_bar.xml这个里面有这个id)            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(                    R.id.decor_content_parent);            if (decorContentParent != null) {                ...                if (mDecorContentParent.getTitle() == null) {                    // 设置标题                    mDecorContentParent.setWindowTitle(mTitle);                }                ...            } else {                // 标题视图                mTitleView = (TextView) findViewById(R.id.title);                // 有的布局中是没有id为title的控件的,也就是不显示标题                if (mTitleView != null) {                    // 判断能否有不显示标题的特性                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {                        final View titleContainer = findViewById(R.id.title_container);                        if (titleContainer != null) {                            titleContainer.setVisibility(View.GONE);                        } else {                            mTitleView.setVisibility(View.GONE);                        }                        mContentParent.setForeground(null);                    } else {// 显示标题                        mTitleView.setText(mTitle);                    }                }            }            // 背景            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {                mDecor.setBackgroundFallback(mBackgroundFallbackResource);            }            // 过度效果            ...        }    }

这里出现了一个mDecor,这个mDecor是DecorView,继承FrameLayout,是窗口顶级视图,也就是Activity显示View的根View,包含一个TitleView和一个ContentView,也就是上面图形中的最外层蓝色的边框所指代的视图,当然,这里第一加载时也是空的,那么会调用generateDecor函数来创立mDecor,而后通过generateLayout方法创立mContentParent视图,创立完成后会设置标题,设置标题的就不分析了,比较简单,下面先看创立mDecor的方法。

3.PhoneWindow.generateDecor

    protected DecorView generateDecor(int featureId) {        ...        // activity.        Context context;        if (mUseDecorContext) {// 从Activity的setContentView方法调用则为true            Context applicationContext = getContext().getApplicationContext();            if (applicationContext == null) {// 系统进程时没有Application的context,所以就用现有的context                context = getContext();            } else {// 应用会有application的Context                ...            }        } else {            context = getContext();        }        return new DecorView(context, featureId, this, getAttributes());    }

这里判断了一个applicationContext能否存在,主要是区分这个是系统调用还是应用,系统是没有applicationContext的,最后通过new关键字创立对象DecorView,这里就获取到了DecorView。

5.PhoneWindow.generateLayout

    protected ViewGroup generateLayout(DecorView decor) {        ...        // 根据Window的属性调用相应的requestFeature        ...        // 获取Window的各种属性来设置flag和参数        ...        // 根据之前的flag和feature来加载一个layout资源到DecorView中,并把可以作为容器的View返回        // 这个layout布局文件在frameworks\base\core\res\res\layout\目录下        int layoutResource;        int features = getLocalFeatures();        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {            layoutResource = R.layout.screen_swipe_dismiss;        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {            if (mIsFloating) {                ...            } else {                layoutResource = R.layout.screen_title_icons;            }            removeFeature(FEATURE_ACTION_BAR);            // System.out.println("Title Icons!");        } 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) {                ...            } else {                layoutResource = R.layout.screen_custom_title;            }            ...        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {            if (mIsFloating) {                ...            } 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;            }            // System.out.println("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.startChanging();        // 根据layoutResource(布局id)加载系统中布局文件(Layout)并增加到DecorView中        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);        // contentParent是用来增加Activity中布局的父布局(FrameLayout),并带有相关主题样式,就是上面        // 提到的id为content的FrameLayout,返回后会赋值给PhoneWindow中的mContentParent        ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);        if (contentParent == null) {            throw new RuntimeException("Window couldn't find content container view");        }        ...        // 设置mDecor背景之类        ...        mDecor.finishChanging();        return contentParent;    }

前面一大段if-else语句是根据属性值获取系统中的layout的id,主要有下面几种:
* R.layout.screen_swipe_dismiss
* R.layout.screen_title_icons
* R.layout.screen_progress
* R.layout.screen_custom_title
* R.layout.screen_title
* R.layout.screen_simple_overlay_action_mode
* R.layout.screen_simple
这些布局文件都在系统frameworks\base\core\res\res\layout\目录下,我们看其中一个screen_simple.xml布局代码:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:fitsSystemWindows="true"    android:orientation="vertical">    <ViewStub android:id="@+id/action_mode_bar_stub"              android:inflatedId="@+id/action_mode_bar"              android:layout="@layout/action_mode_bar"              android:layout_width="match_parent"              android:layout_height="wrap_content"              android:theme="?attr/actionBarTheme" />    <FrameLayout         android:id="@android:id/content"         android:layout_width="match_parent"         android:layout_height="match_parent"         android:foregroundInsidePadding="false"         android:foregroundGravity="fill_horizontal|top"         android:foreground="?android:attr/windowContentOverlay" /></LinearLayout>

其中我们需要获取的contentParent就是xml布局中id为content的FrameLayout,为什么是这个,我们通过上面代码分析,上面我们看到了mDecor.onResourcesLoaded方法,这里的第二个参数layoutResource就是上面的xml布局,所以这里就是加载这个布局的我们看看是不是

6.DecorView.onResourcesLoaded

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {        ...        // DecorView中的标题视图,可能是空,也就是没有标题        mDecorCaptionView = createDecorCaptionView(inflater);        // 加载Layout作为根布局(frameworks\base\core\res\res\layout\目录下layout布局文件)        // 这里获取到的root是没有宽高的        final View root = inflater.inflate(layoutResource, null);        if (mDecorCaptionView != null) {// 有标题            // 这里可以看到mDecorCaptionView不为空时,将mDecorCaptionView增加到DecorView,而后再将            // Layout增加到mDecorCaptionView            if (mDecorCaptionView.getParent() == null) {                addView(mDecorCaptionView,                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));            }            mDecorCaptionView.addView(root,                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));        } else {// 没有标题            // 假如mDecorCaptionView为空,则直接将跟布局Layout增加到DecorView            // Put it below the color views.            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));        }        mContentRoot = (ViewGroup) root;        initializeElevation();    }

这里首先创立了标题视图,而后通过LayoutInflater.inflate加载了id为layoutResource的布局文件并赋值给root引用,最终返回的也是这个root,所以上面方法5中加载了这个布局文件,加载完成后,假如标题视图文件存在,则将root增加到标题视图中,再将标题视图增加到DecorView上,假如没有标题视图,则直接将root布局增加到DecorView上面,宽高是MATCH_PARENT。再回到5中,在调用完mDecor.onResourcesLoaded方法后通过id为ID_ANDROID_CONTENT获取了一个ViewGroup,那么这个ID_ANDROID_CONTENT是什么,通过查找我们发现是:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

这里即可以知道获取的contentParent就是上面xml布局中的id为content的FrameLayout布局,所以到这里整体结构基本明白了。

另外上面调用了一个createDecorCaptionView方法并且传入了LayoutInflater,那么看看这个方法做了哪些操作。

7.DecorView.createDecorCaptionView

    private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {        ...        if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) {            if (decorCaptionView == null) {                decorCaptionView = inflateDecorCaptionView(inflater);            }            decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);        } else {            decorCaptionView = null;        }        ...        return decorCaptionView;    }

这里其实就一个方法需要再看看那就是inflateDecorCaptionView方法。

8.DecorView.inflateDecorCaptionView

    private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {        final Context context = getContext();        inflater = inflater.from(context);        // 从frameworks\base\core\res\res\layout\中加载decor_caption.xml布局        final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,                null);        ...        return view;    }

这里其实就是通过LayoutInflater.inflate方法加载frameworks\base\core\res\res\layout\下的decor_caption.xml布局,这个LayoutInflater.inflate因为比较重要,所以我们放到下一章单独讲解。

10.LayoutInflater.inflate

我们在第二步初始化完DecorView和mContentParent视图后开始调用mLayoutInflater.inflate(layoutResID, mContentParent)方法,加载我们setContentView方法传递进来的视图,也就是我们自己写的Activity布局,之前的都是系统的布局。我们知道mContentParent是放置我们自己写的Activity视图的容器,所以后面就简单了。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {        return inflate(resource, root, root != null);    }

上面我们说了这个方法具体分析我们下一章单独分析。所以我们接着前面分析。

11.View.requestApplyInsets

    public void requestApplyInsets() {        requestFitSystemWindows();    }

12.View.requestFitSystemWindows

    public void requestFitSystemWindows() {        if (mParent != null) {            mParent.requestFitSystemWindows();        }    }

这里的mParent是ViewParent的具体实现ViewRootImpl,所以调用的是ViewRootImpl里的requestFitSystemWindows方法。

13.ViewRootImpl.requestFitSystemWindows

    public void requestFitSystemWindows() {        checkThread();        mApplyInsetsRequested = true;        scheduleTraversals();// 绘制视图    }

checkThread这个是检测线程的方法,也就是检测当前线程是不是主线程,也就是setContentView方法要在UI线程调用。而后调用scheduleTraversals方法开始绘制视图。

15.ViewRootImpl.scheduleTraversals

    void scheduleTraversals() {        // 当mTraversalScheduled为false,也就是没有重绘请求或者者没有未执行完的重绘时才开始重绘        if (!mTraversalScheduled) {            // 一旦开始重回此处设置为True,当执行完毕后调用unscheduleTraversals函数,            // 重新设置为false,避免同时存在屡次绘制            mTraversalScheduled = true;            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();            // 将消息放入消息解决器中,最终调用doTraversal方法            mChoreographer.postCallback(                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);            ...        }    }

mTraversalScheduled只有调用这个方法后才设置为true,所以在开始调用这个方法的时候是false,后面会将mTraversalRunnable放到消息解决器中,这个mTraversalRunnable是一个实现了Runnable接口的对象,所以从这里调用了TraversalRunnable中的run方法。

16.TraversalRunnable.run

        public void run() {            doTraversal();        }

这里很简单就是调用了doTraversal方法。

17.ViewRootImpl.doTraversal

    void doTraversal() {        if (mTraversalScheduled) {            mTraversalScheduled = false;            ...            // 执行View绘制流程            performTraversals();           ...        }    }

这里主要是调用performTraversals方法,开始View的真正绘制。

18.ViewRootImpl.performTraversals

    private void performTraversals() {                ...        // 这里是需要测量的条件:第一次加载View,需要调整窗口大小,需要适应系统窗口,视图显示状态改变,        // 视图布局参数不为空,强制窗口重新布局。首先要满足这个几个条件才可能执行测量        if (mFirst || windowShouldResize || insetsChanged ||                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {                       ...            // 窗口没有中止,或者者通知需要绘制            if (!mStopped || mReportNextDraw) {                                ...                                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||                        updatedConfiguration) {                    ...                    // 1.第一步:测量                    // Ask host how big it wants to be                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                    ...                }            }        } else {            ...        }        ...                if (didLayout) {// 执行布局            // 2.第二步:布局            performLayout(lp, mWidth, mHeight);            ...        }                ...                // 假如没有取消绘制,并且不是新的Surface,那么执行绘制        if (!cancelDraw && !newSurface) {            ...            // 3.第三步:绘制            performDraw();        } else {// 假如取消了绘制或者者是新的Surface,那么要重新测量、布局和绘制            ...        }        mIsInTraversal = false;    }

这里开始进入测量,布局,绘制的过程,里面通过各个条件来判断需要执行哪一步或者者哪几部,由于这一段主要是设计测量、布局、绘制,所以这章就不分析了,这个方法放到《Android系统源码分析–View绘制流程之-onMeasure》一章讲解。

我们下一章开始分析《Android系统源码分析–View绘制流程之-inflate》。

参考文章:

  • https://www.songma.com/p/82b467be18b7
  • https://www.songma.com/p/f76a6f6f75aa
  • https://www.songma.com/p/3872219cc07a

代码地址:

直接拉取导入开发工具(Intellij idea或者者Android studio)

因为coding与腾讯云合作,改变很多,所以后续代码切换到Gitlab。

https://gitlab.com/yuchuangu85/android-25

首发地址:http://www.codemx.cn

Android开发群:192508518

微信公众账号:Code-MX

注:本文原创,转载请注明出处,多谢。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android系统源码分析–View绘制流程之-setContentView

发表回复