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


    在 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 关闭的时候,就直接触发渲染模块,或者就取出了传入的值的数据,断开和传入值之间的影响。如下面最简单的代码

                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 对象想要影响到最终界面渲染,需要被动在渲染收集时,才会更新数据

    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 类

        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 代码

        <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 对象的代码

    MediaContext mctx = MediaContext.From(dispatcher);
    

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

            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 就是具体的执行渲染消息,定义如下

            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 队列

                private object RenderMessageHandler(
                  object resizedCompositionTarget /* can be null if we are not resizing*/
                )
                {
                     // 忽略调试用的逻辑 
                     RenderMessageHandlerCore(resizedCompositionTarget);
                }
    

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

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

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

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

            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 方法注册,代码如下

            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 的实现代码

            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/

    知识共享许可协议
    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我[联系](mailto:lindexi_gd@163.com)。
  • 相关阅读:
    小程序(1)
    手机端放在线条中间的标题
    不定长度导航的两端对齐
    扇形导航菜单
    个性搜索框
    javascript数组原型方法
    jquery插件开发的demo
    监听表单中的内容变化
    mui中的关闭页面的几种方法
    css之伪类选择器:before :after(::before ::after)
  • 原文地址:https://www.cnblogs.com/lindexi/p/15264361.html
Copyright © 2020-2023  润新知