美文网首页Android技术知识Android开发Android开发经验谈
Android面试高频问题:UI绘制流程解析

Android面试高频问题:UI绘制流程解析

作者: 愿天堂没Android | 来源:发表于2022-03-28 20:53 被阅读0次

    一、View如何被添加到屏幕窗口

    了解View如何被添加到屏幕窗口之前,先理解几个概念

    • Window:是一个抽象类,提供了绘制窗口的一组通过API
    • PhoneWindow: 是Window的唯一继承实现类,该类内部包含一个DecorView的对象,该DecorView对象是所有窗口(Actvivty)的根View
    • DecorView: 是PhoneWindow的内部类,是FrameLayout的子类,是对Framelayout进行功能的修饰(所以叫Decorxxx),是所有应用窗口的根View

    以Activity为例(AppCompatActivity略有不同),我们的布局要被加载到窗口中,是通过onCrete方法调用setContentView(layoutResId)传入布局资源id,并经过一下三个过程

    1. 创建顶层布局容器DecorView
    2. 在顶层布局中加载基础布局ViewGroup
    3. 将setContentView(layoutResId)添加到基础布局中的FragmeLayout

    涉及的主要实现类为ActivityPhoneWindow,主要代码实现过程如下

    Activity.class

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

    PhoneWindow.class

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            // step1: 初始化mContentParent
            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 {
            //step6:把我们传入的layoutResID绘制成view,并作为mContentParent的子view
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }
    
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //step1: 创建一个DecorView作为Activity的跟布局
            mDecor = generateDecor(-1);
            ...
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //step2: 调用generateLayout创建系统给的基础布局
            mContentParent = generateLayout(mDecor);
        }
        ...
    }
    
    protected ViewGroup generateLayout(DecorView decor) {
        ...
        int layoutResource;
        int features = getLocalFeatures();
        ...
        else {
            //step3:根据设置的主题指定一个基础布局,这里以R.layout.screen_simple为例
            layoutResource = R.layout.screen_simple;
        }
    
        mDecor.startChanging();
        //step4: 把screen_simple绘制成view并add到DecorView中
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //step5: creen_simple为LinearLayout,其中包含一个id=content的FrameLayout的子view
        //作为后续用来承载我们设置的xml布局的父view
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        return contentParent;
    }
    
    

    关注其中注释step1~6关键代码实现,完成了我们的资源布局加载到了窗口(DecorView)中,如上完成了View的加载过程,但是并没有确定View的坐标宽高等信息,下面就要对我们的View进行绘制以确定View的各类属性

    二、View的绘制流程

    2.1、绘制入口

    ActivityThread.handlerResumeActivity()中调用wm.addView(),而这个wm是WindowManager的实现类为WindowManagerImpl

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //step1: 这里wm的实现类是WindowMamangerImpl
            ViewManager wm = a.getWindowManager();
              ...
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //step2: 执行WindowManagerImpl.addView()
                    wm.addView(decor, l);
                } else {
                    a.onWindowAttributesChanged(l);
                }
            }
            ...
        } 
    }
    
    //关注a.getWindowManager(),调用的是Activity的getWindowManager(),而实际又是Window类个mWindowManager
    WindowManager = mWindow.getWindowManager();
    
    //而Window类的MWindowManager创建过程如下
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //返回的实现类型为WindowManagerImpl
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
    
    

    确定了WindowManager实际类型为WindowManagerImpl后,继续跟进addView方法,调用过程如下 WindowManagerImpl.addView(decroView,layoutParams)进而通过调用WindowManagerGlobal.addView()方法,并创建ViewRootImpl,调用ViewRootImpl.setView(decorView,layoutParams,parentView),进而调用ViewRootImpl的requestLayout()->sheduleTraversals()->doTraversal()->并最终调用performTraversals(),调用顺序用图形表示如下

    重点关注最后的performTraversals()方法,在performTraversals()中会依次调用如下三个方法去完成View绘制的关键三步,测量,摆放和绘制

    • performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    • performLayout(lp, mWidth, mHeight)、
    • performDraw();

    2.2、MeasureSpec

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
    
    

    performMeasure()方法调用view的measure()方法。并传入父容器的宽高MeasureSpec作为入参

    MeasureSpace是View的一个静态内部类,代表一个 32 位 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表规格大小 SpecSize,MeasureSpec 通过把 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配

    主要实现如下,用来保存View的测量模式(SpecMode)和大小(SpecSize),并定义了三种测量模式

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//11000000000000000000000000000000
    
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;//00000000000000000000000000000000
    
        public static final int EXACTLY     = 1 << MODE_SHIFT;//01000000000000000000000000000000
    
        public static final int AT_MOST     = 2 << MODE_SHIFT;//10000000000000000000000000000000
    
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
    
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }
    
    

    MeasureSpec定义的三种模式

    1. UNSPECIFIED:父容器对子View的大小不做约束,它的值为0左移30位(00000000000000000000000000000000)
    2. EXACTLY:父容器计算好了子View的具体宽高,子View的大小就是SpecSize,它的值为1左移30位(01000000000000000000000000000000)
    3. AT_MOST:父容器指定了一个可用大小,子View的大小不能超过这个大小,它的值为2左移30位(10000000000000000000000000000000)

    通过makeMeasureSpec(int size,int mode)方法把size和mode组装到一个32位的int里面

    (size & ~MODE_MASK) | (mode & MODE_MASK)
    /*
    其中MODE_MASK是0x3左移30位=11000000000000000000000000000000
    (size & 00111111111111111111111111111111) | (mode & 11000000000000000000000000000000) 
    size & 00111111111111111111111111111111 得到低30位
    mode & 11000000000000000000000000000000 得到高2位
    再把低30位和高两位取`与`操作,就完成了高 2 位代表测量模式 `SpecMode`,低 30 位代表规格大小 `SpecSize`
    */
    
    

    2.3、绘制三大步骤

    2.3.1、performMeasure - 测量

    再来看View的测量过程performMeasure

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
    
        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    
        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
            resolveRtlPropertiesIfNeeded();
    
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            ...
        }
    
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
    
    

    measure方法中会调用onMeasure(widthMeasureSpec, heightMeasureSpec)方法,

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
    
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
    
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    
    

    最终调用的setMeasuredDimensionRaw方法并确定mMeasuredWidthmMeasureHeight的值,也就是测量的过程目的就是为了确定宽高的值

    再回到onMeasure方法,如果此时是ViewGroup,我们一般需要重写onMeasure方法,以FrameLayout为例

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
    
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
    
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        //step 1: 遍历子View 
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //step 2: 测量子View 
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
    
        //step 3:根据子view的测量结果,计算当前Framelayout的最终宽高 
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    
        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
    
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
          ...
    }
    
    

    在FrameLayout的onMeasure中,首选要遍历子View,通过measureChildWithMargins方法中再调用getChildMeasureSpec确定View的SpecMode个SpecSize

    public abstract class ViewGroup extends View implements ViewParent, ViewManager {
        protected void measureChildWithMargins(View child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) {
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
               //获取子view的MeasureSpec
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                            + widthUsed, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                            + heightUsed, lp.height);
            //step 4:把当前测量的子view的MeasureSpec作为入参,调用子View的measure方法,
            //递归调用,使得View树进入下一层级的测量
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
        
        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    }
    
    

    getChildMeasureSpec(int spec, int padding, int childDimension)实现如上

    确定SpecMode和SpecSize的影响因素有父容器的MeasureSpec自身的LayoutParams,规则入下表所示

    [图片上传失败...(image-112dee-1648471932941)]

    当获取到了子View的MeasureSpec后,把MeasureSpec作为入参继续调用子View的measure方法, 继续测量View树的下一层,进而完成整个View树的测量过程

    2.3.2、performLayout - 摆放

    performLayout方法会调用view.layout方法

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //step 1: 通过setFrame方法确定View的摆放位置
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //step 2: 然后再调用layout方法,实现子view的摆放
            onLayout(changed, l, t, r, b);
            ...
        }
    
    

    layout方法中首先会调用setFrame()方法设定View的位置,也就是左上右下,确定了自身位置后再通过onMeasure确定子view的位置,我们在自定义ViewGrope时一般需要重写onLayout方法,根据我们ViewGroup的特性以确定子View改最终的摆放位置并调用子view.layout(l,t,r,b)进行摆放

    2.3.3、performDraw - 绘制

    performDraw ->draw(Canves fullRedrawNeeded) -> drawSoftware -> view.draw(canves) 调用流程如上,最终调用view的draw方法

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
         * 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)
         */
    
        // Step 1, draw the background, if needed
        int saveCount;
    
        drawBackground(canvas);
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
            return;
        }
       ...
    }
    
    

    onDraw(canves)绘制方法共有6步

    1. 绘制背景
    2. 保存 Canvas 图层
    3. 绘制自身内容的内容
    4. 绘制子View (dispatchDraw)
    5. 绘制 Canvas 图层
    6. 绘制装饰(比如 foreground 和 scrollbar)

    重点关注绘制的第三步和第四步

    • 第三步:调用了onDraw(canvas),如果我们是自定义View的话一般需要复写onDraw方法,在里面进行Canves自身内容的绘制
    • 第四步:调用了dispatchDraw(canvas),如果当前View是ViewGroup那么就会调用ViewGroup的dispatchDraw方法,遍历所有子View并调用子View的draw方法,完成绘制方法在View树的逐层执行
    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                        //
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
        }
        ...
    }
    
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

    作者:只玩鲁班
    转载来源于:https://juejin.cn/post/7079629919705104398
    如有侵权,请联系删除!

    相关文章

      网友评论

        本文标题:Android面试高频问题:UI绘制流程解析

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