Android 图形渲染【1】触发渲染指令的流程解析

429 阅读11分钟

Android 图形渲染的整体流程概述

为了实现流畅的用户体验,Android 图形渲染流程将应用程序的 UI 代码转换为屏幕上最终显示的像素,并确保 UI 更新与显示设备的刷新率保持同步,从而避免画面撕裂和卡顿现象。整体流程可以通过一个简化的流程图来概括:

应用程序 UI 代码 (Java/Kotlin)
    ↓
UI 线程 (主线程) 发出渲染指令 (例如 invalidate(), requestLayout(), 动画更新)
    ↓
View 树遍历 (Traversal)  - Measure, Layout, Draw (在 Choreographer 驱动下)
    ↓
生成绘制指令 (Display List / RenderNode) 并记录到 Canvas
    ↓
Surface (窗口的绘图表面) 获取 Canvas 对象
    ↓
BufferQueue (双缓冲/三缓冲机制) - 生产 Buffer (GraphicBuffer)
    ↓
SurfaceFlinger (系统服务) - 窗口合成 (Composition), 管理 BufferQueue
    ↓
Hardware Composer (HWC) / GPU - 硬件加速合成 (如果支持)
    ↓
Display Controller - 将帧缓冲区数据发送到显示设备
    ↓
显示设备 (屏幕) - 显示最终图像

invalidate 触发渲染流程

invalidate() 方法为例,invalidate() 方法的主要作用是标记一个 View 为 "无效 (dirty)",意味着该 View 的内容或外观已经发生了变化,需要进行重绘 (redraw)

当您修改了 View 的内部状态,例如文本内容、颜色、图片等,需要调用 invalidate() 来通知系统视图需要更新显示。

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
        // ...
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            // ...

            // 标记 View 废弃
            mPrivateFlags |= PFLAG_DIRTY;

            // ...
          
            // 当某个矩形区域出现损坏时,要把这个信息传播给包含它的父视图
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
            // ...
        }
    }

invalidate() 方法本身并不立即执行绘制操作, 而是 向 View 的层级结构向上冒泡 (bubble up) 传播 "invalidate" 请求。 这个请求最终会到达 View 树的根节点 ViewRootImpl

从代码中也可以看出,invalidate 最终走到了 invalidateInternal 函数,在这里去找父 View 并调用 invalidateChild 函数,通知父 View 该 View 的内容或外观已经发生了变化,需要进行重绘。

// in ViewGroup.java
@Override
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    // ...
    ViewParent parent = this;
    if (attachInfo != null) {
        final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
        Matrix childMatrix = child.getMatrix();

        // ...

        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }

            if (drawAnimation) {
                if (view != null) {
                    view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                } else if (parent instanceof ViewRootImpl) {
                    ((ViewRootImpl) parent).mIsAnimating = true;
                }
            }

            // ...

            parent = parent.invalidateChildInParent(location, dirty);
    
            // ...
        } while (parent != null);
    }
}

这个方法在每个父 View 中被调用。它的作用是:

  • 将子 View 相对于父 View 的坐标转换为父 View 坐标系中的坐标。
  • 根据父 View 的状态(是否需要重绘),决定是否继续向上层传递失效请求。
  • 返回父 View 的父 View,以便进行下一轮的 invalidateChildInParent() 调用。

当循环结束时,parentnull,此时 invalidateChildInParent() 方法返回 nullViewRootImpl 是 View 树的根节点,它负责整个 View 树的绘制和管理。当 ViewGroupinvalidateChildInParent() 方法最终返回 null 时,意味着失效事件已经传递到了 ViewRootImpl

这里为什么会从 View 传递到 ViewRootImpl 呢?

是因为:

class ViewRootImpl implements ViewParent

而 ViewParent:

public interface ViewParent {
  	// ...
  	public ViewParent invalidateChildInParent(int[] location, Rect r);
}

也就是最终会调用到 ViewRootImpl 的 invalidateChildInParent :

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }

在 ViewRootImpl 的 invalidateChildInParent 中,最终可能会走到 invalidate();invalidateRectOnScreen(dirty); ,不管两者的哪一个,其最终都会走到 scheduleTraversals();

    private void invalidateRectOnScreen(Rect dirty) {
        if (DEBUG_DRAW) Log.v(mTag, "invalidateRectOnScreen: " + dirty);
        final Rect localDirty = mDirty;

        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        // Intersect with the bounds of the window to skip
        // updates that lie outside of the visible region
        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }
    @UnsupportedAppUsage
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }

最终进入 ViewRootImpl 的 scheduleTraversals();

requestLayout 触发渲染流程

requestLayout() 方法用于通知系统一个 View 的布局 (layout) 可能已经失效,需要重新进行布局计算。 当您动态修改了 View 的尺寸、位置、或者 View 的内部结构(例如,添加或移除子 View)时,需要调用 requestLayout() 来触发重新布局。 重新布局通常会伴随重绘。

如何发出渲染指令:invalidate() 类似, requestLayout() 也不会立即执行布局操作, 而是 向上冒泡 "layout request" 请求到 ViewRootImpl

    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

也是最终调用到 ViewRootImpl 中的 requestLayout()

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

最终也是走进了 ViewRootImpl 的 scheduleTraversals();

postInvalidate 触发渲染流程

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

这里很明显地证明进入了 ViewRootImpl:

		final ViewRootHandler mHandler = new ViewRootHandler();

    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

在 ViewRootImpl 中使用 Handler 发送了一个 MSG_INVALIDATE 消息。

// in ViewRootHandler
				private void handleMessageImpl(Message msg) {
            switch (msg.what) {
                case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                    break;
                ...     
            }
        }

最终是通过 View 的 invalidate 函数触发刷新的。

forceLayout 触发渲染流程

    public void forceLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
    }

mPrivateFlags |= PFLAG_FORCE_LAYOUT;: 这一行代码设置了 View 的私有标志 PFLAG_FORCE_LAYOUT。这个标志表示 View 需要强制重新布局。

mPrivateFlags |= PFLAG_INVALIDATED;: 这一行代码设置了 View 的私有标志 PFLAG_INVALIDATED。这个标志表示 View 需要重绘。

if (mMeasureCache != null) mMeasureCache.clear();: 这一行代码清除了 View 的测量缓存。测量缓存用于存储 View 的测量结果,如果清除了测量缓存,View 在下一次布局时就需要重新测量。

forceLayout() 本身并不会直接触发 ViewRootImplperformTraversals() 方法。但是,它通过设置 PFLAG_FORCE_LAYOUTPFLAG_INVALIDATED 标志,间接影响了 View 树的布局和绘制流程,从而在后续的某个时刻触发 traversal。

具体来说,forceLayout() 的作用是:

  1. 强制重新布局: 设置 PFLAG_FORCE_LAYOUT 标志后,View 在下一次布局过程中会被强制重新测量和布局,即使其大小或位置没有发生变化。这意味着 ViewonMeasure()onLayout() 方法会被调用。
  2. 触发重绘: 设置 PFLAG_INVALIDATED 标志后,View 会被标记为 "dirty",表示需要重绘。这将导致 View 的 draw(Canvas canvas) 方法被调用。
  3. 间接触发 traversal: 当 View 被标记为 "dirty" 或需要重新布局时,View 的父 View 会接收到相应的事件,并最终传递到 ViewRootImplViewRootImpl 会在合适的时机调用 performTraversals() 方法,执行 View 树的遍历、测量、布局和绘制。

使用 WindowManager addView 触发渲染流程

WindowManager 的 addView 方法来自于其实现的接口:

public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

WindowManager 的实现在 WindowManagerGlobal:


    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
        // ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
  					// ...
            if (windowlessSession == null) {
                root = new ViewRootImpl(view.getContext(), display);
            } else {
                root = new ViewRootImpl(view.getContext(), display,
                        windowlessSession, new WindowlessWindowLayout());
            }

            view.setLayoutParams(wparams);

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

这里创建了 ViewRootImpl 并调用了它的 setView 方法:

// ViewRootImpl.java
public void setView(View view, ViewGroup.LayoutParams params) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            mAttachInfo.mRootView = view;

            // ... (一些初始化操作)

            requestLayout(); // 请求重新布局
        }
    }
}

最终是通过 requestLayout 函数来请求布局的。

Activity 创建时触发渲染流程

Activity 通过 onCreate 里面的 setContentView 加载 UI:

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

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

setContentView 有多种重载形式,一种是直接设置一个 View 的,另一种是设置布局文件并将其解析成 View 的。

无论哪一种这里都是通过 getWindow 的 setContentView 进行下一步的调用,这里以 View 为例。

通过 getWindow 函数获取到当前的 PhoneWindow 对象:

    public Window getWindow() {
        return mWindow;
    }

这里 mWindow 的初始化在 attach 方法中:

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {
  			// ... 
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
      	// ... 
    }

mWindow 是一个 PhoneWindow,它的 setContentView:

// in PhoneWindow.java
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor(); // 创建 DecorView
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

检查 mContentParent 是否为空。mContentParent 是一个 ViewGroup,通常是一个 FrameLayout,用于容纳 Activity 的内容视图。

  • 如果 mContentParent 为空,说明这是第一次调用 setContentView(),需要调用 installDecor() 创建 DecorView 并初始化布局。

  • 如果 mContentParent 不为空,且不支持内容过渡动画(FEATURE_CONTENT_TRANSITIONS),则执行 mContentParent.removeAllViews();,移除 mContentParent 中所有的子 View。这是为了在多次调用 setContentView() 时,先清空之前的视图

installDecor() 负责创建 DecorView,DecorView 是 Android 窗口视图层级的根视图,它包含了状态栏、标题栏和内容区域。

installDecor() 还会创建 mContentParent,并将其添加到 DecorView 的内容区域。

		private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor); // 在这里赋值
            // ...
        }
    }

如果 mContentParent 不为空,且不支持内容过渡动画(FEATURE_CONTENT_TRANSITIONS),则执行

在确保 mContentParent 创建完成后,检查是否支持内容过渡动画,并为 view 设置 setLayoutParams,执行动画,否则通过 mContentParent 的 addView 添加 Content View:

				if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }

mContentParent 是一个 ViewGroup,也就是通过 ViewGroup 的 addView 将 Activity 展示的 View 内容添加进去的。

View/ViewGroup 的 addView 触发渲染流程

ViewGroup 的 addView 函数:

    @Override
    public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }

		public void addView(View child, int index, LayoutParams params) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }

      	// 通知系统,当前 ViewGroup 的布局需要重新计算
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

在添加子 View 之前调用 requestLayout(),可以确保在后续的布局计算过程中,ViewGroup 能够正确地处理新添加的子 View 的布局参数。

addViewInner() 方法在设置子 View 的布局参数 (LayoutParams) 时,会调用子 View 的 requestLayout() 方法。这意味着子 View 可能会请求重新布局。

为了防止子 View 在添加到 ViewGroup 之前就过早地请求布局,ViewGroup 会在调用 addViewInner() 之前,先调用自身的 requestLayout() 方法。

这样,子 View 的 requestLayout() 请求会被“阻塞”在 ViewGroup 这一层。也就是说,子 View 的布局请求不会立即被执行,而是会等到 ViewGroup 自身的布局计算完成后,再统一处理。

第二步是调用 ViewGroupinvalidate(true) 方法。这个方法会使整个 ViewGroup 及其子 View 失效,从而触发重绘。参数 true 表示强制重绘,即使 View 的内容没有发生变化。

最后调用 addViewInner(child, index, params, false) ,这是实际执行添加子 View 操作的方法。

addViewInner() 方法内部会处理子 View 的添加、布局参数的设置等细节。

private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {
    // 1. 设置或生成 LayoutParams
    if (!checkLayoutParams(params)) {
        params = generateLayoutParams(params);
    }
  
  	// 设置 LayoutParams
		if (preventRequestLayout) {
				child.mLayoutParams = params;
		} else {
				child.setLayoutParams(params);
		}

    // 2. 添加子 View 到数组
    if (index < 0) {
        index = mChildrenCount; // 默认添加到末尾
    }
    addInArray(child, index); // 添加到子 View 数组

    // 3. 设置子 View 的父 View
    child.mParent = this; // 设置父 View

    // 4. 处理子 View 的 attachToWindow 事件
    AttachInfo ai = mAttachInfo;
    if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags & VISIBILITY_MASK));
    }

    // 5. 分发 View 添加事件
    dispatchViewAdded(child);

    // 6. 处理与焦点相关的逻辑
    if (child.hasDefaultFocus()) {
        setDefaultFocus(child);
    }
}

addViewInner 内部主要做了以下事情:

  1. 为子 View 设置布局参数。
  2. 将子 View 添加到 ViewGroup 的子 View 数组中。
  3. 建立子 View 和父 View 之间的父子关系。
  4. 处理子 View 添加到窗口时的一些事件。

在为子 View 设置布局参数时,调用了 setLayoutParams :

    public void setLayoutParams(ViewGroup.LayoutParams params) {
        if (params == null) {
            throw new NullPointerException("Layout parameters cannot be null");
        }
        mLayoutParams = params;
        resolveLayoutParams();
        if (mParent instanceof ViewGroup) {
            ((ViewGroup) mParent).onSetLayoutParams(this, params);
        }
        requestLayout();
    }

在 View 设置布局参数的过程中,也会调用 requestLayout

子 View 的 dispatchAttachedToWindow 方法,主要处理了以下事情:

  1. 建立 View 与窗口的联系: 设置 mAttachInfo,关联 View 和窗口信息。
  2. 触发依附窗口事件: 调用 onAttachedToWindow(),允许 View 执行自定义操作。
  3. 通知监听器: 分发 OnAttachStateChangeListener 事件,通知监听器 View 已被附加到窗口。
  4. 处理可见性: 处理窗口和 View 的可见性变化,调用 onWindowVisibilityChanged()onVisibilityChanged()

总结

invalidate() 主要用于标记 View 为 “无效”,当 View 内部状态发生改变,如内容更新、大小变化等,就需要调用它来提示系统该 View 需要重绘。这个请求会沿着 View 树向上冒泡传播,直至到达 ViewRootImpl,经过一系列复杂的调用,最终走进 scheduleTraversals(),开启后续的重绘流程。

requestLayout() 则是用于通知系统 View 的布局可能失效,需要重新计算布局,常见于 View 的尺寸、位置等布局相关属性发生变化时。它同样会将 “layout request” 请求向上传递到 ViewRootImpl,并最终触发scheduleTraversals(),不仅会重新布局,往往还会伴随重绘操作。

postInvalidate() 方法稍有不同,它通过 postInvalidateDelayed() 进入 ViewRootImpl,借助 Handler 发送 MSG_INVALIDATE 消息,绕了一个 “小弯”,但最终还是通过 View 的 invalidate 函数来触发刷新。

最后,forceLayout() 方法比较直接,它通过设置 View 的私有标志 PFLAG_FORCE_LAYOUTPFLAG_INVALIDATED,强制进行重新布局和重绘,同时清除测量缓存,以确保布局和绘制的准确性。

在实际的场景中,无论是通过 WindowManager,还是 Activity 呈现 Content View,本质上也都是通过 View/ViewGroup 到 ViewRootImpl 来发出 UI 的图形渲染。