在 WPF 中,常用的画刷里面有纯色画刷 SolidColorBrush 类。因为画刷会对应到 DirectX 的资源,因此之前我以为纯色画刷其实会比 Color 会占用更多的资源。在 WPF 中 Color 其实是结构体,创建速度快。而 SolidColorBrush 是画刷,会对应 DirectX 资源,相对来说性能会比较差。但在通过阅读 WPF 的源代码,发现其实 SolidColorBrush 的创建的性能其实是特别好的,因此请不要担心创建了太多的纯色画刷类
在 WPF 中,画刷 Brush 有很多实现,本文的内容是纯色画刷的实现。在 WPF 的纯色画刷是继承 Brush 的类,这个类自己定义的只有一个字段 _duceResource
和 Color 一个属性,而 Color 属性是一个依赖属性。从这里可以看到 SolidColorBrush 类占用的托管内存空间其实很小
那在日常调试内存的时候,遇到的 SolidColorBrush 类占用非托管内存,这里的非托管内存是在什么时候申请的?其实非托管内存的申请是在 SolidColorBrush 被使用的时候,准确来说是被调用到 AddRefOnChannelCore 方法的时候,才会申请非托管内存。而如果只是构建出来,那么纯色画刷不会申请任何的非托管内存。也就是说此时创建纯色画刷仅仅只会用到很少量的托管内存
在 WPF 设计里面,所有继承 System.Windows.Media.Composition.DUCE.IResource 接口的类型,都可以表示这是一个 DirectX 资源类,将会在渲染过程中,申请或使用 DirectX 资源。而 DirectX 资源就是非托管资源。在 WPF 的机制,将会在 WPF 资源被使用的时候,如画刷被附加到某个元素上,在此元素渲染的时候(准确来说是之前)将会通过 IResource 接口的 AddRefOnChannel 方法让资源通过 System.Windows.Media.Composition.DUCE.Channel 申请到 DirectX 资源。以下是 IResource 接口代码
///<summary>
/// DUCE.IResource
///</summary>
internal interface IResource
{
DUCE.ResourceHandle AddRefOnChannel(Channel channel);
int GetChannelCount();
DUCE.Channel GetChannel(int index);
void ReleaseOnChannel(Channel channel);
DUCE.ResourceHandle GetHandle(Channel channel);
/// <summary>
/// Only Vieport3DVisual and Visual3D implement this.
/// Vieport3DVisual has two handles. One stored in _proxy
/// and the other one stored in _proxy3D. This function returns
/// the handle stored in _proxy3D.
/// </summary>
DUCE.ResourceHandle Get3DHandle(Channel channel);
/// <summary>
/// Sends a command to compositor to remove the child
/// from its parent on the channel.
/// </summary>
void RemoveChildFromParent(IResource parent, DUCE.Channel channel);
}
在 Brush 类将会重写 AddRefOnChannel 方法,如下面代码
internal abstract DUCE.ResourceHandle AddRefOnChannelCore(DUCE.Channel channel);
/// <summary>
/// AddRefOnChannel
/// </summary>
DUCE.ResourceHandle DUCE.IResource.AddRefOnChannel(DUCE.Channel channel)
{
// Reconsider the need for this lock when removing the MultiChannelResource.
using (CompositionEngineLock.Acquire())
{
return AddRefOnChannelCore(channel);
}
}
可以看到在 Brush 类中,其实是调用了 AddRefOnChannelCore 抽象方法,在 SolidColorBrush 里面实现了 AddRefOnChannelCore 申请非托管资源
internal override DUCE.ResourceHandle AddRefOnChannelCore(DUCE.Channel channel)
{
if (_duceResource.CreateOrAddRefOnChannel(this, channel, System.Windows.Media.Composition.DUCE.ResourceType.TYPE_SOLIDCOLORBRUSH))
{
Transform vTransform = Transform;
if (vTransform != null) ((DUCE.IResource)vTransform).AddRefOnChannel(channel);
Transform vRelativeTransform = RelativeTransform;
if (vRelativeTransform != null) ((DUCE.IResource)vRelativeTransform).AddRefOnChannel(channel);
AddRefOnChannelAnimations(channel);
UpdateResource(channel, true /* skip "on channel" check - we already know that we're on channel */ );
}
return _duceResource.GetHandle(channel);
} // 这个花括号在 WPF 代码里面就没对齐
上面代码核心就是 _duceResource.CreateOrAddRefOnChannel
创建 ResourceHandle 以及通过 UpdateResource 将颜色更新到 DirectX 资源
在 UpdateResource 里面,将会通过如下代码在非托管层注册纯色画刷
internal override void UpdateResource(DUCE.Channel channel, bool skipOnChannelCheck)
{
// 忽略代码
DUCE.MILCMD_SOLIDCOLORBRUSH data;
unsafe
{
data.Type = MILCMD.MilCmdSolidColorBrush;
data.Handle = _duceResource.GetHandle(channel);
if (hOpacityAnimations.IsNull)
{
data.Opacity = Opacity;
}
data.hOpacityAnimations = hOpacityAnimations;
data.hTransform = hTransform;
data.hRelativeTransform = hRelativeTransform;
if (hColorAnimations.IsNull)
{
// 将颜色给到非托管层
data.Color = CompositionResourceManager.ColorToMilColorF(Color);
}
// 如果有动画,那么设置动画的颜色
data.hColorAnimations = hColorAnimations;
// Send packed command structure
channel.SendCommand(
(byte*)&data,
sizeof(DUCE.MILCMD_SOLIDCOLORBRUSH));
}
}
}
回到主题,在创建 SolidColorBrush 时,在 WPF 框架里面做了什么?通过上文可以看到申请非托管资源是在使用到画刷的时候,如果我创建的纯色画刷只是存放而已,而不会使用他去参加渲染,那么纯色画刷将不会占用任何非托管资源,也不需要有任何逻辑调用到非托管的 DirectX 层
在 SolidColorBrush 的构造函数里面,可以选择传入或不传入 Color 参数。如上文可以了解到在 SolidColorBrush 的颜色属性是依赖属性,假定没有传入构造参数,那么将会使用依赖属性默认值,也就是说此实例仅仅只使用到字段 _duceResource
的内存。从性能角度上,如果没有传入构造参数,那么如下面代码,这是一个空白的构造函数,啥都没有做
public SolidColorBrush()
{
}
当然了 SolidColorBrush 继承了 Brush 类,咱也需要看一下 Brush 类的构造函数的定义
protected Brush()
{
}
可以看到 Brush 也是空白。但 Brush 继承了 Animatable 类,咱继续看接下来的继承的类的构造
public abstract partial class Animatable : Freezable, IAnimatable, DUCE.IResource
{
protected Animatable()
{
}
// 忽略代码
}
public abstract class Freezable : DependencyObject, ISealable
{
protected Freezable()
{
Debug.Assert(!Freezable_Frozen
&& !Freezable_HasMultipleInheritanceContexts
&& !(HasHandlers || HasContextInformation),
"Initial state is incorrect");
}
}
public class DependencyObject : DispatcherObject
{
public DependencyObject()
{
Initialize();
}
private void Initialize()
{
CanBeInheritanceContext = true;
CanModifyEffectiveValues = true;
}
internal bool CanBeInheritanceContext
{
[FriendAccessAllowed] // Built into Base, also used by Framework.
get { return (_packedData & 0x00200000) != 0; }
[FriendAccessAllowed] // Built into Base, also used by Framework.
set
{
if (value)
{
_packedData |= 0x00200000;
}
else
{
_packedData &= 0xFFDFFFFF;
}
}
}
private bool CanModifyEffectiveValues
{
get { return (_packedData & 0x00080000) != 0; }
set
{
Debug.Assert(!DO_Sealed, "A Sealed DO cannot be modified");
if (value)
{
_packedData |= 0x00080000;
}
else
{
_packedData &= 0xFFF7FFFF;
}
}
}
}
public abstract class DispatcherObject
{
protected DispatcherObject()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
}
可以看到性能层面上,几乎构造函数是啥都没有做。通过上面的代码也可以看到,如果一个类的继承很长,那么构造函数的调用性能,也许需要关注。在另一个仓库,也算是跨平台版本的 WPF 仓库 https://github.com/AvaloniaUI/Avalonia 这里面的元素定义,元素的类型继承十分长,这是设计上的缺点
那如果在 SolidColorBrush 的构造加上参数,传入颜色,此时发生了什么?在 SolidColorBrush 的构造函数将会给依赖属性设置值,如下面代码
public SolidColorBrush(Color color)
{
Color = color;
}
public Color Color
{
get
{
return (Color) GetValue(ColorProperty);
}
set
{
SetValueInternal(ColorProperty, value);
}
}
public static readonly DependencyProperty ColorProperty;
static SolidColorBrush()
{
// We check our static default fields which are of type Freezable
// to make sure that they are not mutable, otherwise we will throw
// if these get touched by more than one thread in the lifetime
// of your app.
// Initializations
Type typeofThis = typeof(SolidColorBrush);
ColorProperty =
RegisterProperty("Color",
typeof(Color),
typeofThis,
Colors.Transparent,
new PropertyChangedCallback(ColorPropertyChanged),
null,
/* isIndependentlyAnimated = */ true,
/* coerceValueCallback */ null);
}
private static void ColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SolidColorBrush target = ((SolidColorBrush) d);
target.PropertyChanged(ColorProperty);
}
从上面代码可以看到给依赖属性设置值将会触发 ColorPropertyChanged 函数。在这个函数调用了 PropertyChanged 方法。这是定义在 Animatable 的方法,代码如下
internal void PropertyChanged(DependencyProperty dp)
{
AnimationStorage animationStorage = AnimationStorage.GetStorage(this, dp);
IndependentAnimationStorage independentAnimationStorage = animationStorage as IndependentAnimationStorage;
if (independentAnimationStorage != null)
{
independentAnimationStorage.InvalidateResource();
}
else
{
RegisterForAsyncUpdateResource();
}
}
刚创建的 SolidColorBrush 是不存在 AnimationStorage 的,因此 independentAnimationStorage 一定是空,将会调用 RegisterForAsyncUpdateResource 方法
internal void RegisterForAsyncUpdateResource()
{
DUCE.IResource resource = this as DUCE.IResource;
if (resource != null)
{
if ((Dispatcher != null) && Animatable_IsResourceInvalidationNecessary)
{
MediaContext mediaContext = MediaContext.From(Dispatcher);
//
// Only register for a deferred resource update if this
// is actually on the channel.
//
if (!resource.GetHandle(mediaContext.Channel).IsNull)
{
// Add this handler to this event means that the handler will be
// called on the next UIThread render for this Dispatcher.
mediaContext.ResourcesUpdated += new MediaContext.ResourcesUpdatedHandler(UpdateResource);
Animatable_IsResourceInvalidationNecessary = false;
}
}
}
}
在上面代码中,因为 Animatable_IsResourceInvalidationNecessary 默认值是 false 因此这个函数啥都没有做
可以看到无论是在 SolidColorBrush 的构造函数有没有设置参数,执行的代码逻辑都非常少,执行时间基本都可以忽略。从执行性能层面,可以认为创建 SolidColorBrush 的性能是特别好的,以上代码的执行时间预计不会比创建一个空对象慢多少。从内存层面,在 SolidColorBrush 类本身,不算继承类的情况下,只有一个字段和一个依赖属性,占用内存量不会比 Color 结构体多多少。所以可以放心创建 SolidColorBrush 对象。好吧,本文说的是创建的性能,如果要将 SolidColorBrush 用上,这就是另一个坑了,建议如果是要使用的 SolidColorBrush 对象,还是使用缓存比较好,非托管的占用还是比较多的
当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-%E8%AF%BB-WPF-%E6%BA%90%E4%BB%A3%E7%A0%81%E7%AC%94%E8%AE%B0-%E5%88%9B%E5%BB%BA-SolidColorBrush-%E6%80%A7%E8%83%BD%E6%B2%A1%E6%9C%89%E6%83%B3%E8%B1%A1%E9%82%A3%E4%B9%88%E5%B7%AE.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利