[Android] View 的绘制流程不懂?来了解一下吧

前言

在 Android 开发中,View 的绘制是一个至关重要的环节,无论是简单的 TextView、Button 等,还是复杂的自定义控件,都需要经历一系列的绘制过程才能最终呈现在屏幕上。本文将从 View 的绘制整体流程到具体的绘制细节,由浅入深理解 View 的绘制流程。

1、View 显示到 Window 上的整体流程

View 添加到 Window 的起点是 ActivityThread 的 handleResumeActivity() 方法:

Java 复制代码
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
        
    ......
    ......
    
    if (!performResumeActivity(r, finalStateRequest, reason)) {
        return;
    }
    
    ......
    ......
    
    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 (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
    } else if (!willBeVisible) {
        if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
        r.hideForNow = true;
    }

    ......
    ......
}

这里先调用了 performResumeActivity() 方法,获取记录了 activity 信息的对象并保存到传入的引用变量 ActivityClientRecord 中,也就是下面代码用到的 r 变量,如果获取失败就直接返回;随后调用 wm.addView(decor, l) 方法添加 decorViewWindowManager.LayoutParams,这里的 wm 就是根据 r 中记录的 activity 拿到对应的 WindowManagerImpl 对象,让我们看看 WindowManagerImpl 中的 addView() 方法:

Java 复制代码
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyTokens(params);
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

继续看 WindowManagerGlobal 中的 addView() 方法:

Java 复制代码
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
        
        ......
        ......
        
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try {
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
            if (viewIndex >= 0) {
                removeViewLocked(viewIndex, true);
            }
            throw e;
        }
        
        ......
        ......
}

这里先把 view,root,wparams 保存到 WindowManagerGlobal 内部的数组中,也就是:

Java 复制代码
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();

WindowManagerGlobal 是用来全局管理 View 的类,因此这里要保存起来,随后调用 root.setView(view, wparams, panelParentView, userId) 方法将 View 添加到 root 中,rootViewRootImpl,继续点进 setView() 中,其中调用了 request() 方法来请求布局:

Java 复制代码
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

然后是 scheduleTraversals() 方法:

Java 复制代码
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

其中的 mTraversalRunnable 是:

Java 复制代码
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

也就是这里又调用到了 doTraversal() 方法:

Java 复制代码
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

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

        performTraversals();

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

最后通过 performTraversals() 方法执行 View 的具体绘制方法 。

流程总结: ActivityThread.handleResumeActivity() -> WindowManagerImpl.addView() -> WindowManagerGlobal.addView() -> ViewRootImpl.setView() -> ViewRootImpl.requestLayout() -> ViewRootImpl.scheduleTraversals() -> ViewRootImpl.doTraversal() -> ViewRootImpl.performTraversals()

2、ViewRootImpl 的 performTraversals() 方法中做了什么

在上面的流程介绍中,我们知道,最终调用 View 的方法进行真正的绘制是在 ViewRootImpl.performTraversals() 方法中,这个方法有多重要,从它的代码行数就能看出来,在 Android 34 的源码中,这个方法在 ViewRootImpl.java2907 - 3845 行,整整 939 行。

由于代码量太大,这里就不把源码粘贴过来,有需要或者感兴趣的可以自己去看,接下来,我们将介绍 performTraversals() 方法里 5 个重要的步骤:

  1. 预测量:windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight, shouldOptimizeMeasure);
  2. 布局窗口:relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
  3. 控件树测量:performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  4. 布局:performLayout(lp, mWidth, mHeight);
  5. 绘制:if (!performDraw() && mActiveSurfaceSyncGroup != null) { mActiveSurfaceSyncGroup.markSyncReady(); }

下面,让我们看看这些方法里面做了些什么。

1) 预测量 measureHierarchy()

Java 复制代码
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight,
        boolean forRootSizeOnly) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;

    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(mTag,
            "Measuring " + host + " in display " + desiredWindowWidth
            + "x" + desiredWindowHeight + "...");

    boolean goodMeasure = false;
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { // 1.
        final DisplayMetrics packageMetrics = res.getDisplayMetrics();
        res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); // 2.
        int baseSize = 0;
        if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
            baseSize = (int)mTmpValue.getDimension(packageMetrics); // 3.
        }
        if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": baseSize=" + baseSize
                + ", desiredWindowWidth=" + desiredWindowWidth);
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
                    lp.privateFlags);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                    + host.getMeasuredWidth() + "," + host.getMeasuredHeight()
                    + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec)
                    + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { // 4.
                goodMeasure = true;
            } else {
                baseSize = (baseSize+desiredWindowWidth)/2; // 5.
                if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": next baseSize="
                        + baseSize);
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(mTag, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { // 6.
                    if (DEBUG_DIALOG) Log.v(mTag, "Good!");
                    goodMeasure = true;
                }
            }
        }
    }

    if (!goodMeasure) { // 7.
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width,
                lp.privateFlags);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
                lp.privateFlags);
        if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec(
                childWidthMeasureSpec, childHeightMeasureSpec)) {
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        } else {
            mViewMeasureDeferred = true;
        }
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }

    if (DBG) {
        System.out.println("======================================");
        System.out.println("performTraversals -- after measure");
        host.debug();
    }

    return windowSizeMayChange;
}

上述代码按序号标记了主要的执行步骤,下面我们来一一分析:

  1. 判断父容器宽度是否设置为 WRAP_CONTENT ,如果是,则进入 if 继续运行,否则直接跳到第 7 步;
  2. 获取一个系统默认的最小宽度,并保存到 mTmpValue 中,这个默认值为 320 dp ,根据变量名 R.dimen.config_prefDialogWidth 就知道这是系统给 dialog 设置的默认最优宽度;
  3. 将第 2 步获取到的值赋给 baseSize,下面调用 performMeasure() 方法测量子 View 是否适合这个宽度,如果当前子 View 不适合 320 dp,则运行到第 4 步;
  4. 如果上面赋予的 320 dp 不合适,则进入到 else 执行第 5 步;
  5. baseSize 根据当前屏幕宽度增大一定值,下面再次调用 performMeasure() 测量子 View,如果这步合适,则进行第 6 步,否则运行到第 7 步;
  6. 设置 goodMeasure 标志位为 true,则不会运行第 7 步;
  7. 运行到这里就直接将父容器能给的最大值赋给子 View,并判断父容器给子 View 的大小是否合适,不合适则 windowSizeMayChange = true,之后还将测量,若合适,则之后不再测量。

综上所述:如果如容器设置的宽度是 WRAP_CONTENT ,则会至多进行三次 performMeasure(),如果其中有满足的则不会进入后面的 if 判断;设置的宽度不是 WRAP_CONTENT,则直接进入第 7 步,赋予父容器能给的最大值。

2) 布局窗口 relayoutWindow()

根据预测阶段量第 7 步中 windowSizeMayChange 的值,如果满足条件则会运行到 relayoutWindow() 方法,这个方法的主要工作包括:

  1. 计算窗口的尺寸和位置:根据窗口的布局参数和屏幕的尺寸,计算出窗口应该显示的位置和大小。
  2. 更新视图的布局参数:遍历窗口中的所有视图,根据它们的布局参数,更新它们的位置和大小。
  3. 执行视图的绘制:根据更新后的布局参数,重新绘制窗口中的所有视图。
  4. 更新窗口的显示状态:根据需要,更新窗口的显示状态,比如显示、隐藏或者改变透明度等。

因此我们知道:relayoutWindow() 方法是重新布局窗口的重要方法,它确保窗口中的所有视图都能按照最新的布局规则正确显示。

3) 控件树测量 performMeasure()

同样也是根据上文 windowSizeMayChange 的值,为 true 则会运行到 performMeasure() 方法再次进行测量。

这个方法就是进行了 View 的测量,调用链:ViewRootImpl.performMeasure() -> View.measure() -> View.onMeasure()

其中需要注意的点是,如果 View 重写了 onMeasure() 方法,则必须调用 setMeasureDimension() 方法( super.onMeasure 也包括 ),因为在 View 的 measure() 方法的末尾有判断语句,若没调用 setMeasureDimension() 方法,则会报错,如下:

Java 复制代码
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
    throw new IllegalStateException("View with id " + getId() + ": "
            + getClass().getName() + "#onMeasure() did not set the"
            + " measured dimension by calling"
            + " setMeasuredDimension()");
}

4) 布局控件树 performLayout()

这个方法比较简单,调用链:ViewRootImpl.performLayout() -> View.layout() -> View.onLayout()

而源码中的 View.onLayout() 方法为空,因为只有容器才需要布局子 View,我们知道 ViewGroup 是继承 View 的,而 ViewGroup.onLayout() 是抽象方法,因此继承 ViewGroup 自定义容器时,要重写 onLayout() 方法。

5) 绘制 performDraw()

这个方法也没什么好说的,调用链:ViewRootImpl.performDraw() -> ViewRootImpl.draw() -> 硬件加速软件绘制

其中在 ViewRootImpl.draw() 中会判断是 硬件加速 还是 软件绘制,源码如下:

Java 复制代码
if (isHardwareEnabled()) {
    ......
    ......
    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
    ......
    ......
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) {
        return false;
    }
}

这里的 硬件加速软件绘制,感兴趣的读者可以自行搜索了解。

3、requestLayout()invalidate() 的区别

我们知道调用 requestLayout()invalidate() 方法都会重新绘制 View,那么这两者有什么区别呢?答案是 requestLayout() 方法会重新测量、布局并绘制,而 invalidate() 方法则是直接到绘制阶段。具体流程如下图所示:

👉 以上就是对 View 绘制流程的简单梳理。

相关推荐
枯骨成佛29 分钟前
Android中Crash Debug技巧
android
工业互联网专业5 小时前
Python毕业设计选题:基于Django+uniapp的公司订餐系统小程序
vue.js·python·小程序·django·uni-app·源码·课程设计
程序员小海绵【vincewm】5 小时前
【设计模式】结合Tomcat源码,分析外观模式/门面模式的特性和应用场景
设计模式·tomcat·源码·外观模式·1024程序员节·门面模式
kim56596 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼6 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ6 小时前
Android Studio使用c++编写
android·c++
csucoderlee6 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim56596 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式6 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。7 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio