美文网首页
UI绘制源码分析_第一弹

UI绘制源码分析_第一弹

作者: Lypop | 来源:发表于2017-11-22 00:15 被阅读0次

对于UI加载过程我们会有很多的疑问

1.setContentView到底做了什么,为什么我们调用了之后就能显示相应的布局
2.PhoneWindow是什么,和window有什么关系
3.DecorView是什么,和我们的布局又有什么样的关系
4.RequestFeature为什么要在setContentView之前调用

好了,开始我们的摸索。

DecorView的创建过程

我们先从继承Activity进行探究

1.进入setContentView

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

通过attach方法可知getWindow得到的是PhoneWindow对象(具体可以查看ActivityThread类performLaunchActivity方法),PhoneWindow继承Window

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();//生成DecorView
    } 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);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

这一段代码最重要的是installDecor方法和inflate方法,那我们先进入第一个方法

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);//生成DecorView
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        //下面的就是设置title、转场
    }
}

DecorView继承于FrameLayout,下面进入generateLayout方法

protected ViewGroup generateLayout(DecorView decor) {
    ......//设置Flag和requestFeature        
    WindowManager.LayoutParams params = getAttributes();
    ......
    mDecor.startChanging();
    //根据layoutResource来生成DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //contentParent为内容布局
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ......
    return contentParent;
}

通过这段代码我们也就知道了为什么RequestFeature要在setContentView之前调用。为了更好的理解,我们可以研究一下布局文件,以screen_simple为例

<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>

这个布局生成的View就是DecorView,contentParent也就是FrameLayout,其实就是我们平时写的布局。

ok,回到PhoneWindow->setContentView的代码,接下来进入inflate方法,在这里面我们传入了我们写的布局和contentParent,可想而知inflate方法做的就是将我们xml写的布局解析出来依次添加到contentParent中

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

next

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        View result = root;

        try {
            //观察root节点
            int type;
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {//标签是否为merge
                //当root不为空则使用merge才有效
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                rInflateChildren(parser, temp, attrs, true);
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
            }

        }
        return result;
    }
}

进入rInflateChildren方法

void rInflate(XmlPullParser parser, View parent, Context context,
    AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        final String name = parser.getName();
        if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {//include不能是根元素
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);//如果深度不为0则进行解析include标签
        } else if (TAG_MERGE.equals(name)) {//merge标签必须是根元素
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);//递归调用
            viewGroup.addView(view, params);//将解析完的View添加到contentParent中
        }
    }
    if (finishInflate) {//当inflate完成则调用ViewGroup的onFinishInflate方法
        parent.onFinishInflate();
    }
}

不断的递归调用将View添加到上面定义的contentParent中,OK,这样就将试图显示出来了

上面我们是用Activity的源码来分析的,现在做兼容都使用了AppCompatActivity了,那它里面是怎么调用的呢?

我们进入AppCompatActivity的setContentView方法

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

这里调用了一个代理对象的setContentView方法,进入

private static AppCompatDelegate create(Context context, Window window,
    AppCompatCallback callback) {
    final int sdk = Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        return new AppCompatDelegateImplN(context, window, callback);
    } else if (sdk >= 23) {
        return new AppCompatDelegateImplV23(context, window, callback);
    } else if (sdk >= 14) {
        return new AppCompatDelegateImplV14(context, window, callback);
    } else if (sdk >= 11) {
        return new AppCompatDelegateImplV11(context, window, callback);
    } else {
        return new AppCompatDelegateImplV9(context, window, callback);
    }
}

擦,原来Google做了这么多的兼容,进入setContentView方法是进入的AppCompatDelegateImplV9类里面的

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

这代码和研究Activity的setContentView方法非常的相似,也是通过DecorView得到contentParent然后将自己写的布局添加到contentParent中,那ensureSubDecor里面是做了什么操作呢?

private ViewGroup createSubDecor() {
    //这里得到的是AppCompat的主题资源,所以要在继承AppCompatActivity的类使用AppCompat的主题
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    ......//设置title和Feature

    // 开始初始化PhoneWindow的DecorView
    mWindow.getDecorView();
    subDecor = (ViewGroup) LayoutInflater.from(themedContext)
            .inflate(R.layout.abc_screen_toolbar, null);
    mDecorContentParent = (DecorContentParent) subDecor
            .findViewById(R.id.decor_content_parent);
    mDecorContentParent.setWindowCallback(getWindowCallback());
    //为布局生成View并赋值给subDecorView
    //生成subDecor中的contentView
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);
    //得到PhoneWindow中contentParent
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        //将DecorView中的contentView设置为NO_ID,并将subDecorView的contentView设置为android.R.id.content
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
    }
    // Now set the Window's content view with the decor
    mWindow.setContentView(subDecor);
    return subDecor;
}

这里需要注意的是在AppCompatActivity里面,它做了一个替换,将DecorView的contentView设置为NO_ID,然后将subDecorView的contentView设置为android.R.id.content。只所以可以这样是因为在之前调用了mWindow.getDecorView()方法对DecorView进行了初始化操作,里面的布局就不多说和上面的布局类似,所以说我们要多多使用AppCompatActivity,毕竟Google做了这么多兼容

总结:每一个Activity都有一个关联的Window对象,用来描述应用程序窗口。每一个窗口内部又包含了一个DecorView对象,Decorview对象用来描述窗口的视图--xml布局

最后我们附上一张AppCompatActivity的视图结构

image

可以看到继承AppCompatActivity会将TextView自动转化为AppCompatTextView

DecorView添加到Window过程

首先我们直接进入ActivityThread的handleResumeActivity方法

final void handleResumeActivity(IBinder token,
    boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    ......
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        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) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
        }

        ......
}

进入WindowManagerImpl类addView方法(decor代表DecorView)

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);//生成ViewRootImpl对象
        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);
        }
    }
}

WindowManagerGlobal用来提供window显示和操作的全局方法,接下来进入ViewRootImpl的setView方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;

            ......

            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            requestLayout();(1)
            ......
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);(2)
            } 
            ......

            view.assignParent(this);(1)
            ......
    }
}

这里我们只需要看三个方法,一个是requestLayout()方法,然后一步一步调用了doTraversal方法进而调用了performTraversal方法

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

另一个则是addToDisplay方法,该过程使用了aidl的方式将视图显示到了window上

最后一个则是view.assignParent(this)方法,虽然它调用的是DecorView的方法,最后也是调用的View里面的assignParent方法

void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}

这个方法是将ViewRootImpl赋值给了mParent,这就是我们在代码中调用requestLayout方法最后会调用ViewRootImpl的原因,看到这里有一种豁然开朗的感觉

根据上面绘制DecorView添加到窗口的流程图

image

至此,就完了。如果不知道怎么使用的话我们可以去看snackBar的源码,这个控件更能教会我们怎么去使用android.R.id.content

private static ViewGroup findSuitableParent(View view) {
    ViewGroup fallback = null;
    do {
        if (view instanceof CoordinatorLayout) {
            return (ViewGroup) view;
        } else if (view instanceof FrameLayout) {
            if (view.getId() == android.R.id.content) {
                return (ViewGroup) view;
            } else {
                fallback = (ViewGroup) view;
            }
        }

        if (view != null) {
            final ViewParent parent = view.getParent();
            view = parent instanceof View ? (View) parent : null;
        }
    } while (view != null);
    return fallback;
}

不断的去getPartent直到当前View是contentView,使用这个特性我们可以去做非常炫酷的弹框效果应用在所有的界面。

每天的进步一点点,将来收获多一点

共勉~

相关文章

网友评论

      本文标题:UI绘制源码分析_第一弹

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