dotnet 读 WPF 源代码笔记 渲染收集是如何触发

2021-09-18 16:01:35 浏览数 (1)

在 WPF 里面,渲染可以从架构上划分为两层。上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令。上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GFX 层,作用是根据收到的渲染的命令绘制出界面。本文所聊的是渲染上层部分,在 WPF 框架是如何做到界面刷新渲染,包括此调用的顺序以及框架逻辑

阅读本文之前,我期望读者有一定的 WPF 渲染基础,以及了解 WPF 的大架构。本文不会涉及到任何底层渲染相关的知识。阅读本文,你将了解到依赖属性和 WPF 渲染层之间的关系

在开始之前,必须明确一点的是,不是所有的 WPF 应用行为,如依赖属性变更,都会触发渲染变更。有渲染变更不代表立刻将会触发界面刷新,从触发渲染变更到界面刷新,还有以下步骤: 触发渲染,渲染上层收集应用层的绘制渲染的命令,触发渲染线程接收绘制渲染的命令,渲染的下层根据绘制渲染的命令进入 DirectX 渲染管线,由 DirectX 完成后续渲染步骤

本文所聊到的仅仅只是以上的触发渲染,渲染上层收集应用层的绘制渲染的命令这两个步骤。关于 WPF 渲染部分的大框架还请参阅 WPF 渲染原理

本篇博客基于 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客进行更深入 WPF 框架源代码探讨

为了能更好说明 WPF 框架的行为,本文开始先介绍一个测试代码用来测试 WPF 的行为

在本文实际开始之前,还请大家思考一个问题,在 WPF 中,调用 DrawingVisual 的 RenderOpen 方法返回的 DrawingContext 对象里面,传入的参数的属性值影响渲染结果,是一次性的,还是持续的?什么是一次性的,什么是持续的?换个问法是如果传入的值在 DrawingContext 关闭之后,变更属性,此时是否还会影响到渲染结果。答案的是或否就决定了 WPF 底层的实现行为,是否在 DrawingContext 关闭的时候,就直接触发渲染模块,或者就取出了传入的值的数据,断开和传入值之间的影响。如下面最简单的代码

代码语言:javascript复制
            var drawingVisual = new DrawingVisual();
            var translateTransform = new TranslateTransform();
            using (var drawingContext = drawingVisual.RenderOpen())
            {
                drawingContext.PushTransform(translateTransform);

                var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 10, 10));
                drawingContext.DrawGeometry(Brushes.Red, null, rectangleGeometry);

                drawingContext.Pop();
            }

            SetTranslateTransform(translateTransform);

        private async void SetTranslateTransform(TranslateTransform translateTransform)
        {
            while (true)
            {
                translateTransform.X  ;

                await Task.Delay(TimeSpan.FromMilliseconds(10));
            }
        }

以上代码在 SetTranslateTransform 函数里面,不断修改 TranslateTransform 的属性,是否还会影响到 DrawingVisual 的渲染效果?带着这个问题,进入到本文的开始

众所周知,只有在渲染收集触发的时候,才会收集应用层的渲染数据。以 TranslateTransform 为例,在更改 TranslateTransform 的 X 或 Y 属性的值的时候,如果没有给此 TranslateTransform 对象建立直接渲染关系,也就是 Freezable 的 AddSingletonContext 方法没有被传入渲染的直接元素联系的时候,对属性值的更改只是和更改 CLR 自动属性一样,不会有任何的通知和变更。也就是说在 TranslateTransform 对象想要影响到最终界面渲染,需要被动在渲染收集时,才会更新数据

代码语言:javascript复制
class Freezable
{
        private void AddSingletonContext(DependencyObject context, DependencyProperty property)
        {
            // 忽略建立联系代码,这里面比较绕。核心逻辑就是取出 context 里面的 SingletonHandler 委托
            // 以上的 context 是 RenderData 类型。此 SingletonHandler 委托将会在继承 Freezable 的类型的依赖属性变更的时候,支持被调用
            // 对于建立直接联系的对象,如存放在 UIElement 上的 TranslateTransform 属性,此时的 SingletonHandler 对象就是由 UIElement 发起的订阅
        }
}

如在 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客所聊到的实现方式,通过在 DrawingVisual 里面设置一个 TranslateTransform 对象,再将 DrawingVisual 放入到 UIElement 里面。如此行为将让 TranslateTransform 无法和 UIElement 建立直接的联系。以上进入 AddSingletonContext 函数将传入的是属于 DrawingVisual 的 RenderData 对象,这就意味着当 TranslateTransform 的属性变更时,仅仅只能通知到 DrawingVisual 对象,而不能通知到更上层的 UIElement 对象

这完全取决于此应用代码的实现,为了让大家不需要在两篇博客之间来回跳,以下给出用到 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客的核心代码

以下是一个继承 UIElement 的 Foo 类

代码语言:javascript复制
    class Foo : UIElement
    {
        public Foo()
        {
            var drawingVisual = new DrawingVisual();
            var translateTransform = new TranslateTransform();
            using (var drawingContext = drawingVisual.RenderOpen())
            {
                var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 10, 10));

                drawingContext.PushTransform(translateTransform);

                drawingContext.DrawGeometry(Brushes.Red, null, rectangleGeometry);

                drawingContext.Pop();
            }

            Visual = drawingVisual;

            SetTranslateTransform(translateTransform);
        }

        private async void SetTranslateTransform(TranslateTransform translateTransform)
        {
            while (true)
            {
                translateTransform.X  ;

                if (translateTransform.X > 700)
                {
                    translateTransform.X = 0;
                }

                await Task.Delay(TimeSpan.FromMilliseconds(10));
            }
        }

        protected override Visual GetVisualChild(int index) => Visual;
        protected override int VisualChildrenCount => 1;

        private Visual Visual { get; }
    }

以下是使用此 Foo 的 xaml 代码

代码语言:javascript复制
    <Grid>
        <local:Foo x:Name="Foo"></local:Foo>
    </Grid>

以上就是本文所有用到的测试辅助的代码

为了更好了解 WPF 框架的底层行为,以上代码被我放入到我私有的 WPF 仓库中,作为 WPF 仓库里面的 demo 的代码。可以从 github 获取本文以上测试代码,获取代码之后,请将 WPFDemo 作为启动项目

以上就是本文构建的测试逻辑。下面将回到主题部分

从 TranslateTransform 属性影响界面逻辑渲染入手,在变更 TranslateTransform 属性时,将因为没有和 Foo 此 UIElement 建立直接的逻辑关系,同时所在的 DrawingVisual 也没有在 Foo 里面被调用 AddVisualChild 方法而加入到可视化树(视觉树)上,因此 TranslateTransform 属性的变更无法通知到 WPF 布局层

更好利用此特性来测试 WPF 框架层的行为。在此先回答一个问题,为什么不通过静态代码阅读了解框架的行为?原因是 WPF 框架太过庞大,我在静态代码阅读过程将受限于记忆而无法从全局把握 WPF 框架逻辑。因此更多的是需要靠测试代码来了解 WPF 框架的逻辑

在 Dispatcher 对象里面,从 VisualStudio 的调试窗口可以看到有没有开放的几个 Reserved 属性,其中一项就是专门给 MediaContext 所使用。如命名,此 MediaContext 类型就是 WPF 渲染上层的渲染上下文,依靠此渲染上下文可以用来控制 WPF 的多媒体(渲染)层的行为

在 WPF 框架里面可以随处见到从 Dispatcher 里面获取 MediaContext 对象的代码

代码语言:javascript复制
MediaContext mctx = MediaContext.From(dispatcher);

从众多的(不包括动画)触发渲染进入之后,都会汇总到 MediaContext 的 PostRender 方法。此方法是给 Dispatcher 传递一个渲染消息,也就是优先级为 Render 的 RenderMessage 任务。以下是有删减的 PostRender 方法代码

代码语言:javascript复制
        internal void PostRender()
        {
        	// 如果当前没有在进入渲染状态,那么开始触发渲染消息
            if (!_isRendering)
            {
                if (_currentRenderOp != null)
                {
                    // 如果已有渲染消息在消息队列里,那么更改优先级确保是 Render 优先级。此渲染消息将会很快被调度
                    // If we already have a render operation in the queue, we should
                    // change its priority to render priority so it happens sooner.
                    _currentRenderOp.Priority = DispatcherPriority.Render;
                }
                else
                {
                    // 如果还没有渲染消息,那么给 Dispatcher 传入优先级为 Render 的渲染消息
                    // If we don't have a render operation in the queue, add one at
                    // render priority.
                    _currentRenderOp = Dispatcher.BeginInvoke(DispatcherPriority.Render, _renderMessage, null);
                }
            }
        }

以上代码的 _renderMessage 就是具体的执行渲染消息,定义如下

代码语言:javascript复制
        internal MediaContext(Dispatcher dispatcher)
        {
             _renderMessage = new DispatcherOperationCallback(RenderMessageHandler);
        }

private DispatcherOperationCallback _renderMessage;

歪楼一下,在 WPF 里面,通用的调度使用的委托都是 DispatcherOperationCallback 类型,使用此类型是为了性能考虑。在 Dispatcher 的 WrappedInvoke 方法里面,将会通过 as 判断当前传入的 Delegate 委托类型。使用框架内置的 Action 和 DispatcherOperationCallback 等类型,可以使用明确类型的委托调用,而不需要使用 DynamicInvoke 调用委托来提升性能。详细请看 github 上大佬的更改 内容

通过以上代码可以了解到渲染消息的在于 MediaContext 的 RenderMessageHandler 方法里面。此方法将会被 Dispatcher 使用 Render 优先级进行调用,也会被各个模块触发渲染时加入 Dispatcher 队列

代码语言:javascript复制
            private object RenderMessageHandler(
              object resizedCompositionTarget /* can be null if we are not resizing*/
            )
            {
                 // 忽略调试用的逻辑 
                 RenderMessageHandlerCore(resizedCompositionTarget);
            }

接着在 RenderMessageHandlerCore 里面将会层层调用,调用到 Render 方法。此方法实现以下功能

  • 渲染每个注册的 ICompositionTarget 以完成批处理。 渲染都是一批批处理的
  • 更新收集的渲染数据
  • 将收集到的数据提交给下层渲染

核心的步骤就是在 更新收集的渲染数据 这一步。这里也就能解答 WPF 的渲染收集是如何触发的

在 更新收集的渲染数据 里面的实现代码如下

代码语言:javascript复制
        private void RaiseResourcesUpdated()
        {
            if (_resourcesUpdatedHandlers != null)
            {
                DUCE.ChannelSet channelSet = GetChannels();
                _resourcesUpdatedHandlers(channelSet.Channel, false /* do not skip the "on channel" check */);
                _resourcesUpdatedHandlers = null;
            }
        }

这里的 _resourcesUpdatedHandlers 是委托,在各个资源,如 TranslateTransform 都会注册到 MediaContext 里,也就是在这一层可以让资源可以收到渲染更新的消息

如在 TranslateTransform 的基类 Animatable 里面,就在 RegisterForAsyncUpdateResource 方法注册,代码如下

代码语言:javascript复制
        internal void RegisterForAsyncUpdateResource()
        {
            MediaContext mediaContext = MediaContext.From(Dispatcher);

            if (!resource.GetHandle(mediaContext.Channel).IsNull)
            {
                mediaContext.ResourcesUpdated  = new MediaContext.ResourcesUpdatedHandler(UpdateResource);
            }
        }

如上文,在 WPF 框架里面,可以非常方便从 Dispatcher 拿到 MediaContext 对象,从而也很方便加上 ResourcesUpdated 委托

在此 ResourcesUpdated 事件触发的时候,就需要各个资源向 DUCE.Channel 写入资源的数据,让下层渲染使用。如 TranslateTransform 的实现代码

代码语言:javascript复制
        internal override void UpdateResource(DUCE.Channel channel, bool skipOnChannelCheck)
        {
            if (skipOnChannelCheck || _duceResource.IsOnChannel(channel))
            {
                base.UpdateResource(channel, skipOnChannelCheck);
                DUCE.MILCMD_TRANSLATETRANSFORM data;
                unsafe
                {
                    data.Type = MILCMD.MilCmdTranslateTransform;
                    data.Handle = _duceResource.GetHandle(channel);
                    data.X = X;
                    data.Y = Y;

                    channel.SendCommand(
                        (byte*)&data,
                        sizeof(DUCE.MILCMD_TRANSLATETRANSFORM));
                }
            }
        }

回到本文开始的问题,在 WPF 调用 DrawingContext 的关闭时,此时不会立刻执行界面渲染逻辑。此时离实际的界面渲染还很远,需要先通知到 MediaContext 将渲染消息加入到 Dispatcher 队列。等待 Dispatcher 的调度,接着进入 MediaContext 的层层 Render 方法,再由 Render 方法触发资源收集更新的事件,依靠监听事件让各个资源向 Channel 写入资源的当前状态信息。最后告诉下层渲染,批量收集渲染数据完成,可以开始执行下层渲染逻辑

更多渲染相关博客请看 渲染相关


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-读-WPF-源代码笔记-渲染收集是如何触发.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者前往 CSDN 关注我的主页

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。

无盈利,不卖课,做纯粹的技术博客

0 人点赞