[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 绘制流程的简单梳理。

相关推荐
我命由我123453 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2334 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759624 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马4 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone4 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone4 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子8 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker8 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone8 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常
雾屿_Mistisle8 小时前
文件包含漏洞(File Inclusion)
android