dotnet 职业技术学院 发布于 2019-11-24
C# 8.0 引入了可为空引用类型和不可为空引用类型。当你需要给你或者团队更严格的要求时,可能需要定义这部分的警告和错误级别。
本文将介绍 C# 可空引用类型部分的警告和错误提示,便于进行个人项目或者团队项目的配置。
本文的内容本身没什么意义,但如果你试图进行一些团队配置,那么本文的示例可能能带来一些帮助。
CS8600
将 null 文本或可能的 null 值转换为非 null 类型。
string walterlv = null;
CS8601
可能的 null 引用赋值。
string Text { get; set; }
void Foo(string? text)
{
// 将可能为 null 的文本向不可为 null 的类型赋值。
Text = text;
}
CS8602
null 引用可能的取消引用。
// 当编译器判定 walterlv 可能为 null 时才会有此警告。
var value = walterlv.ToString();
CS8603
可能的 null 引用返回。
string Foo()
{
return null;
}
CS8604
将可能为 null
的引用作为参数传递到不可为 null
的方法中:
void Foo()
{
string text = GetText();;
Bar(text);
}
string? GetText()
{
return null;
}
CS8609
返回类型中引用类型的为 Null 性与重写成员不匹配。
比如你的基类中返回值不允许为 null,但是实现中返回值却允许为 null。
protected virtual async Task<string> FooAsync()
{
}
protected override async Task<string?> FooAsync()
{
}
CS8610
参数中引用类型的为 Null 性与重写成员不匹配。
比如你的基类中方法参数值不允许为 null,但是实现中方法参数却允许为 null。
protected virtual void FooAsync(string value)
{
}
protected override void FooAsync(string? value)
{
}
CS8616
接口中定义的成员中的 null 性与实现中成员的 null 型不匹配。
比如你的接口中不允许为 null,但是实现中却允许为 null。
CS8618
未初始化不可以为 null 的字段 “_walterlv”。
如果一个类型中存在不可以为 null 的字段,那么需要在构造函数中初始化,如果没有初始化,则会发出警告或者异常。
CS8619
一个类型与构造这个类型的 null 性不匹配。
例如:
Task<object?> foo = new Task<object>(() => new object());
CS8622
委托定义的参数中引用类型的为 null 性与目标委托不匹配。
比如你定义了一个委托:
void Foo(object? sender, EventArgs e);
然而在订阅事件的时候,使用的函数 null 性不匹配,则会出现警告:
void OnFoo(object sender, EventArgs e)
{
// 注意到这里的 object 本应该写作 object?
}
CS8625
无法将 null 文本转换为非 null 引用或无约束类型参数。
void Foo(string walterlv = null)
{
}
CS8653
对于泛型 T,使用 default
设置其值。如果 T 是引用类型,那么 default
就会将这个泛型类型赋值为 null
。然而并没有将泛型 T 的使用写为 T?。
dotnet 职业技术学院 发布于 2019-11-21
本文收集一些已知的导致电脑屏幕不关闭的程序。如果你发现无论你设置多短的屏幕关闭超时时间但一直都不关闭,那么可以参考本文检查是否打开了这些程序。
先检查一下你系统设置中的电源和睡眠选项,时间不应该太长。一定要先看看这里,别到时候折腾了半天发现是自己设错了就亏了……
另外,找程序的时候,不要第一眼看过去没有就忽略它了。因为你可能像我一样有很多个桌面。最好还是用任务管理器找,不会漏掉。
如果你有游戏没关,你第一个就应该怀疑它!
我不想把我正在玩的游戏列举出来,因为容易过时还会暴露些什么……
因为我总是时不时发现某一天电脑屏幕一直亮着。到了晚上很刺眼的,而且费电……
所以,每发现一个就补充一个好了。如果你有已知的,麻烦在评论区告诉我哟!如果看不到评论区,可以前往这里评论,或者给我发邮件。
dotnet 职业技术学院 发布于 2019-11-20
有一些程序不支持被直接启动,而要求通过命令行启动。这个时候,你就需要使用 PowerShell 或者 PowerShell Core 来启动这样的程序。我们都知道如何在命令行或各种终端中启动一个程序,但是当你需要自动启动这个程序的时候,你就需要知道如何通过 PowerShell 或其他命令行终端来启动一个程序,而不是手工输入然后回车运行了。
本文就介绍 PowerShell 的命令行启动参数。利用这些参数,你可以自动化地通过 PowerShell 程序来完成一些原本需要通过手工执行的操作或者突破一些限制。
一般来说,编译生成的 exe 程序都可以直接启动,即便是命令行程序也是如此。但是有一些程序就是要做一些限制。比如下面的 FRP 反向代理程序:
借助 cmd.exe 来启动的方法可以参见我的另一篇博客:
那么我们如何能够借助于 PowerShell 或者 PowerShell 来启动它呢?
先打开一个 PowerShell。
对于 Windows 自带的基于 .NET Framework 的 PowerShell,使用 powershell
命令可以直接启动 PowerShell。对于基于 .NET Core 版本的 PowerShell Core,使用 pwsh
命令可以直接启动。
关于 .NET Core 版本的 PowerShell Core 可以参见我的另一篇博客:
接下来输入下面三个命令中的任何一个:
PowerShell -Help
PowerShell -?
PowerShell /?
或者对于 PowerShell Core 来说,是下面三个命令中的任何一个:
pwsh -Help
pwsh -?
pwsh /?
你就可以看到 PowerShell 的使用说明:
下面的命令,使用 PowerShell 间接启动 frpc.exe 反向代理程序,并给 frpc.exe 程序传入 -c ./frpc.ini
的启动参数:
> pwsh -Command "D:\walterlv\frpc.exe -c ./frpc.ini"
或者简写为:
> pwsh -c "D:\walterlv\frpc.exe -c ./frpc.ini"
实际上使用 PowerShell 来做这些事情简直是用牛刀杀鸡,因为本身 PowerShell 非常强大。我们只是因为一些程序的限制不得不使用这样的方案来启动程序而已。
比如其中之一,执行脚本。
需要加上 -NoExit
参数。
> pwsh -NoExit -c "D:\walterlv\frpc.exe -c ./frpc.ini"
一定要注意,-c
和后面的命令必须放到最末尾,因为 -c
后面的所有字符串都会被解析为需要执行的命令。
多条脚本之间使用 ;
作为分隔:
> pwsh -c "D:\walterlv\frpc.exe -c ./frpc.ini"; "D:\walterlv\frps.exe -c ./frps.ini"
如果引号里面还需要写引号,则可以把里面的引号改成单引号 '
或者把外面的引号改为单引号 '
。
# Execute a PowerShell Command in a session
PowerShell -Command "Get-EventLog -LogName security"
# Run a script block in a session
PowerShell -Command {Get-EventLog -LogName security}
# An alternate way to run a command in a new session
PowerShell -Command "& {Get-EventLog -LogName security}"
PowerShell[.exe] [-PSConsoleFile <文件> | -Version <版本>] [-NoLogo] [-NoExit] [-Sta] [-Mta] [-NoProfile] [-NonInteractive] [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}] [-WindowStyle <样式>] [-EncodedCommand <Base64 编码命令>] [-ConfigurationName <字符串>] [-File <文件路径> <参数>] [-ExecutionPolicy <执行策略>] [-Command { - | <脚本块> [-args <参数数组>] | <字符串> [<命令参数>] } ] PowerShell[.exe] -Help | -? | /? -PSConsoleFile 加载指定的 Windows PowerShell 控制台文件。若要创建控制台 文件,请在 Windows PowerShell 中使用 Export-Console。 -Version 启动指定版本的 Windows PowerShell。 使用参数输入版本号,如 "-version 2.0"。 -NoLogo 启动时隐藏版权标志。 -NoExit 运行启动命令后不退出。 -Sta 使用单线程单元启动 shell。 单线程单元(STA)是默认值。 -Mta 使用多线程单元启动 shell。 -NoProfile 不加载 Windows PowerShell 配置文件。 -NonInteractive 不向用户显示交互式提示。 -InputFormat 描述发送到 Windows PowerShell 的数据的格式。有效值为 "Text" (文本字符串)或 "XML" (序列化的 CLIXML 格式)。 -OutputFormat 确定如何设置 Windows PowerShell 输出内容的格式。有效值 为 "Text" (文本字符串)或 "XML" (序列化的 CLIXML 格式)。 -WindowStyle 将窗口样式设置为 Normal、Minimized、Maximized 或 Hidden。 -EncodedCommand 接受 base-64 编码字符串版本的命令。使用此参数 向 Windows PowerShell 提交需要复杂引号 或大括号的命令。 -ConfigurationName 指定运行 Windows PowerShell 的配置终结点。 该终结点可以是在本地计算机上注册的任何终结点,包括 默认的 Windows PowerShell 远程处理终结点或具有特定用户角色功能 的自定义终结点。 -File 在本地作用域("dot-sourced")中运行指定的脚本,以便 脚本创建的函数和变量可以在当前 会话中使用。输入脚本文件路径和任何参数。 File 必须是命令中的最后一个参数,因为在 File 参数 名称后面键入的所有字符都将解释 为后跟脚本参数的脚本文件路径。 -ExecutionPolicy 设置当前会话的默认执行策略,并将其保存 在 $env:PSExecutionPolicyPreference 环境变量中。 该参数不会更改在注册表中 设置的 Windows PowerShell 执行策略。 -Command 执行指定的命令(和任何参数),就好像它们是 在 Windows PowerShell 命令提示符下键入的一样,然后退出,除非 指定了 NoExit。Command 的值可以为 "-"、字符串或 脚本块。 如果 Command 的值为 "-",则从标准输入中读取 命令文本。 如果 Command 的值为脚本块,则脚本块必须 用大括号({})括起来。只有在 Windows PowerShell 中运行 PowerShell.exe 时, 才能指定脚本块。脚本块的结果将作为反序列化的 XML 对象 (而非活动对象)返回到父 Shell。 如果 Command 的值为字符串,则 Command 必须是命令中的 最后一个参数,因为在命令后面键入的所有字符 都将解释为命令参数。 若要编写运行 Windows PowerShell 命令的字符串,请使用以下格式: "& {<命令>}" 其中,引号表示一个字符串,调用运算符(&) 导致执行命令。 -Help, -?, /? 显示此消息。如果在 Windows PowerShell 中键入 PowerShell.exe 命令,请在命令参数前面添加连字符(-),而不是添加正 斜杠(/)。你可以在 Cmd.exe 中使用连字符或正斜杠。 示例 PowerShell -PSConsoleFile SqlSnapIn.Psc1 PowerShell -version 2.0 -NoLogo -InputFormat text -OutputFormat XML PowerShell -ConfigurationName AdminRoles PowerShell -Command {Get-EventLog -LogName security} PowerShell -Command "& {Get-EventLog -LogName security}" # To use the -EncodedCommand parameter: $command = 'dir "c:\program files" ' $bytes = [System.Text.Encoding]::Unicode.GetBytes($command) $encodedCommand = [Convert]::ToBase64String($bytes) powershell.exe -encodedCommand $encodedCommand
参考资料
dotnet 职业技术学院 发布于 2019-11-05
你可能接触过 git-filter-branch
来清理 git 仓库,不过同时也能体会到这个命令使用的繁琐,以及其超长的执行时间。
现在,你可以考虑使用 bfg
来解决问题了!
这里并不推荐使用传统方式安装,因为传统方式安装后,bfg
不会成为你计算机的命令。在实际使用工具的时候,你必须为你的每一句命令加上 java -jar bfg.jar
前缀来使用 Java 运行时间接运行。
如果你使用包管理器 scoop,那么安装将会非常简单,只需要以下几个命令。
scoop install bfg
scoop bucket add java
scoop install java/openjdk
安装 bfg:
PS C:\Users\lvyi> scoop install bfg
Installing 'bfg' (1.13.0) [64bit]
bfg-1.13.0.jar (12.8 MB) [============================================================================================================================] 100%
Checking hash of bfg-1.13.0.jar ... ok.
Linking ~\scoop\apps\bfg\current => ~\scoop\apps\bfg\1.13.0
Creating shim for 'bfg'.
'bfg' (1.13.0) was installed successfully!
'bfg' suggests installing 'java/oraclejdk' or 'java/openjdk'.
安装 Java 源:
PS C:\Users\lvyi> scoop bucket add java
Checking repo... ok
The java bucket was added successfully.
安装 Jdk:
PS C:\Users\lvyi> scoop install java/openjdk
Installing 'openjdk' (13.0.1-9) [64bit]
openjdk-13.0.1_windows-x64_bin.zip (186.9 MB) [=======================================================================================================] 100%
Checking hash of openjdk-13.0.1_windows-x64_bin.zip ... ok.
Extracting openjdk-13.0.1_windows-x64_bin.zip ... done.
Linking ~\scoop\apps\openjdk\current => ~\scoop\apps\openjdk\13.0.1-9
'openjdk' (13.0.1-9) was installed successfully!
当你准备好清理你的仓库的时候,需要进行一些准备。
git clone
命令加上 --mirror
参数)
git push
的时候,会更新远端仓库的所有引用cd
到你要清理的仓库路径的根目录
--no-blob-protection
参数使用 bfg
来清理仓库比 git 原生的 git-filter-branch
快得多。官方说法是,10-720 倍:
turning an overnight job into one that takes less than ten minutes.
将一整夜的工作缩减到不到十分钟。
使用下面的命令,可以将仓库历史中大于 500M 的文件都删除掉。
> bfg --strip-blobs-bigger-than 500M
删除 walterlv.snk
文件:
> bfg --delete-files walterlv.snk
删除 walterlv.snk 或 lindexi.snk 文件:
> bfg --delete-files {walterlv,lindexi}.snk
比如原来仓库结构是这样的:
- README.md
- Security.md
- walterlv.snk
+ test
- lindexi.snk
那么删除完后,根目录的 walterlv.snk 和 test 子目录下的 lindexi.snk 就都删除了。
删除名字为 walterlv 的文件夹:
> bfg --delete-folders walterlv
此命令可以与上面的 --delete-files
放在一起执行:
> bfg --delete-folders walterlv --delete-files walterlv.snk
> bfg --replace-text expression-file.txt
注意,这里的 expression-file.txt 名称是随便取的,你可以取其他任何名称,只要在命令里输入正确的名称(可能需要包含路径)就行。
但是 expression-file.txt 里面的内容却是我们需要关注的重点。
此文件中的每一行是一个匹配表达式。默认情况下,每一个表达式被视为一段文本常量,但你可以通过指定 regex:
前缀来说明此表达式是一个正则表达式,或者指定 glob:
前缀。每一个表达式的后面可以加上 ‘==>’ 来指定匹配的文件应该被替换成什么(如果没有指定,就会被替换成默认值 ***REMOVED***
。
下面这个例子示例将 git 仓库中所有文件中的 密码:123456
字符串替换成 ***REMOVED***
:
密码:123456
更复杂一点的,下面的例子示例将 git 仓库中所有文件中的 密码:123456
字符串替换成 密码:******
:
密码:123456 ==> 密码:******
还可以使用正则表达式:
regex:密码:\d+ ==> 密码:******
当你在本地操作完镜像仓库之后,可以将其推回原来的远端仓库了。
> git push
最后,有一个不必要的操作。就是回收已经没有引用的旧提交,这可以减小本地仓库的大小:
> git reflog expire --expire=now --all && git gc --prune=now --aggressive
直接在命令行输入 bfg
可以看 bfg
命令行的用法。我贴在下面可以让还没安装的小伙伴感受一下它的功能:
PS C:\Users\lvyi\Desktop\BfgDemoRepo> bfg
bfg 1.13.0
Usage: bfg [options] [<repo>]
-b, --strip-blobs-bigger-than <size>
strip blobs bigger than X (eg '128K', '1M', etc)
-B, --strip-biggest-blobs NUM
strip the top NUM biggest blobs
-bi, --strip-blobs-with-ids <blob-ids-file>
strip blobs with the specified Git object ids
-D, --delete-files <glob>
delete files with the specified names (eg '*.class', '*.{txt,log}' - matches on file name, not path within repo)
--delete-folders <glob> delete folders with the specified names (eg '.svn', '*-tmp' - matches on folder name, not path within repo)
--convert-to-git-lfs <value>
extract files with the specified names (eg '*.zip' or '*.mp4') into Git LFS
-rt, --replace-text <expressions-file>
filter content of files, replacing matched text. Match expressions should be listed in the file, one expression per line - by default, each expression is treated as a literal, but 'regex:' & 'glob:' prefixes are supported, with '==>' to specify a replacement string other than the default of '***REMOVED***'.
-fi, --filter-content-including <glob>
do file-content filtering on files that match the specified expression (eg '*.{txt,properties}')
-fe, --filter-content-excluding <glob>
don't do file-content filtering on files that match the specified expression (eg '*.{xml,pdf}')
-fs, --filter-content-size-threshold <size>
only do file-content filtering on files smaller than <size> (default is 1048576 bytes)
-p, --protect-blobs-from <refs>
protect blobs that appear in the most recent versions of the specified refs (default is 'HEAD')
--no-blob-protection allow the BFG to modify even your *latest* commit. Not recommended: you should have already ensured your latest commit is clean.
--private treat this repo-rewrite as removing private data (for example: omit old commit ids from commit messages)
--massive-non-file-objects-sized-up-to <size>
increase memory usage to handle over-size Commits, Tags, and Trees that are up to X in size (eg '10M')
<repo> file path for Git repository to clean
我觉得你可能需要中文版,于是自己翻译了一下:
PS C:\Users\lvyi\Desktop\BfgDemoRepo> bfg
bfg 1.13.0
用法: bfg [options] [<repo>]
-b, --strip-blobs-bigger-than <size>
移除大于 <size> 大小的文件(<size> 可填写诸如 '128K'、'1M')
-B, --strip-biggest-blobs NUM
从大到小移除 NUM 数量的文件
-bi, --strip-blobs-with-ids <blob-ids-file>
移除具有指定 git 对象 id 的文件
-D, --delete-files <glob>
移除具有指定名称的文件(例如 '*.class'、'*.{txt,log}',仅匹配文件名而不能匹配路径)
--delete-folders <glob> 移除具有指定名称的文件夹(例如 '.svn'、'*-tmp',仅匹配文件夹名而不能匹配路径)
--convert-to-git-lfs <value>
将指定名称的文件(例如 '*.zip' 或 '*.mp4')解压到 Git LFS
-rt, --replace-text <expressions-file>
查找文件内容,并替换其中匹配的文本。<expressions-file> 是一个包含一个或多个匹配表达式的文件,文件中每一行是一个匹配表达式。
默认情况下,每一个表达式被视为一段文本常量,但你可以通过指定 'regex:' 前缀来说明此表达式是一个正则表达式,或者指定 'glob:' 前缀。
每一个表达式的后面可以加上 '==>' 来指定匹配的文件应该被替换成什么(如果没有指定,就会被替换成默认值 '***REMOVED***'。
-fi, --filter-content-including <glob>
指定文件名(例如 '*.{txt,properties}'),在进行内容替换的时候只对这些文件进行处理。
-fe, --filter-content-excluding <glob>
指定文件名(例如 '*.{xml,pdf}'),在进行内容替换的时候不对这些文件进行处理。
-fs, --filter-content-size-threshold <size>
仅对小于 <size> 指定的大小的文件替换内容。(默认值为 1048576 字节)
-p, --protect-blobs-from <refs>
protect blobs that appear in the most recent versions of the specified refs (default is 'HEAD')
--no-blob-protection allow the BFG to modify even your *latest* commit. Not recommended: you should have already ensured your latest commit is clean.
--private 仅将本次操作视为个人数据的修改(这样生成的新提交会使用旧提交的 Id,其他人拉取仓库的时候因为这些 Id 已经存在于是不会更新,以至于此更改实际上只影响自己)。
--massive-non-file-objects-sized-up-to <size>
increase memory usage to handle over-size Commits, Tags, and Trees that are up to X in size (eg '10M')
<repo> file path for Git repository to clean
参考资料
dotnet 职业技术学院 发布于 2019-10-29
当我们在写 +=
和 -=
事件的时候,我们会在 +=
或 -=
的右边写上事件处理函数。我们可以写很多种不同的事件处理函数的形式,那么这些形式都是一样的吗?如果你不注意,可能出现内存泄漏问题。
本文将讲解事件处理函数的不同形式,理解了这些可以避免编写代码的时候出现内存相关的问题。
事件处理函数本质上是一个委托,比如 FileSystemWatcher
的 Changed
事件是这样定义的:
// 这是简化的代码。
public event FileSystemEventHandler Changed;
这里的 FileSystemEventHandler
是一个委托类型:
public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);
一个典型的事件的 +=
会像下面这样:
void Subscribe(FileSystemWatcher watcher)
{
watcher.Changed += new FileSystemEventHandler(OnChanged);
}
void OnChanged(object sender, FileSystemEventArgs e)
{
}
+=
的右边传入的是一个 new
出来的委托实例。
除了上面直接创建的目标类型的委托之外,还有其他类型可以放到 +=
的右边:
// 方法组。
watcher.Changed += OnChanged;
// Lambda 表达式。
watcher.Changed += (sender, e) => Console.WriteLine(e.ChangeType);
// Lambda 表达式。
watcher.Changed += (sender, e) =>
{
// 事件引发时,代码会在这里执行。
};
// 匿名方法。
watcher.Changed += delegate (object sender, FileSystemEventArgs e)
{
// 事件引发时,代码会在这里执行。
};
// 委托类型的局部变量(或者字段)。
FileSystemEventHandler onChanged = (sender, e) => Console.WriteLine(e.ChangeType);
watcher.Changed += onChanged;
// 局部方法(或者局部静态方法)。
watcher.Changed += OnChanged;
void OnChanged(object sender, FileSystemEventArgs e)
{
}
因为我们可以通过编写事件的 add
和 remove
方法来观察事件 +=
-=
传入的 value
是什么类型的什么实例,所以可以很容易验证以上每一种实例最终被加入到事件中的真实实例。
实际上我们发现,无论哪一个,最终传入的都是 FileSystemEventHandler
类型的实例。
然而我们知道,只有直接 new
出来的那个和局部变量那个真正是 FileSystemEventHandler
类型的实例,其他都不是。
那么中间发生了什么样的转换使得我们所有种类的写法最终都可以 +=
呢?
具有相同签名的不同委托类型,彼此之前并没有继承关系,因此在运行时是不可以进行类型转换的。
比如:
FileSystemEventHandler onChanged1 = (sender, e) => Console.WriteLine(e.ChangeType);
Action<object, FileSystemEventArgs> onChanged2 = (sender, e) => Console.WriteLine(e.ChangeType);
这里,onChanged1
的实例不可以赋值给 onChanged2
,反过来 onChanged2
的实例也不可以赋值给 onChanged1
。于是这里只有 onChanged1
才可以作为 Changed
事件 +=
的右边,而 onChanged2
放到 +=
右边是会出现编译错误的。
然而,我们可以放 Lambda 表达式,可以放匿名函数,可以放方法组,也可以放局部函数。因为这些类型可以在编译期间,由编译器帮助进行类型转换。而转换的效果就类似于我们自己编写 new FileSystemEventHandler(xxx)
一样。
看下面这一段代码,你认为可以 -=
成功吗?
void Subscribe(FileSystemWatcher watcher)
{
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.Changed -= new FileSystemEventHandler(OnChanged);
}
void OnChanged(object sender, FileSystemEventArgs e)
{
}
实际上这是可以 -=
成功的。
我们平时编写代码的时候,下面的情况可能会多一些,于是自然而然以为 +=
和 -=
可以成功,因为他们“看起来”是同一个实例:
watcher.Changed += OnChanged;
watcher.Changed -= OnChanged;
在读完刚刚那一段之后,我们就可以知道,实际上这一段和上面 new
出来委托的写法在运行时是一模一样的。
如果你想测试,那么在 +=
的时候为对象加上一个 Id,在 -=
的时候你就会发现这是一个新对象(因为没有 Id)。
然而,你平时众多的编码经验会告诉你,这里的 -=
是一定可以成功的。也就是说,+=
和 -=
时传入的委托实例即便不是同一个,也是可以成功 +=
和 -=
的。
+=
-=
是怎么做的+=
和 -=
到底是怎么做的,可以在不同实例时也能 +=
和 -=
成功呢?
+=
和 -=
实际上是调用了 Delegate
的 Combine
和 Remove
方法,并生成一个新的委托实例赋值给 +=
-=
的左边。
public event FileSystemEventHandler Changed
{
add
{
onChangedHandler = (FileSystemEventHandler)Delegate.Combine(onChangedHandler, value);
}
remove
{
onChangedHandler = (FileSystemEventHandler)Delegate.Remove(onChangedHandler, value);
}
}
而最终的判断也是通过 Delegate
的 Equals
方法来比较委托的实例是否相等的(==
和 !=
也是调用的 Equals
):
public override bool Equals(object? obj)
{
if (obj == null || !InternalEqualTypes(this, obj))
return false;
Delegate d = (Delegate)obj;
// do an optimistic check first. This is hopefully cheap enough to be worth
if (_target == d._target && _methodPtr == d._methodPtr && _methodPtrAux == d._methodPtrAux)
return true;
// even though the fields were not all equals the delegates may still match
// When target carries the delegate itself the 2 targets (delegates) may be different instances
// but the delegates are logically the same
// It may also happen that the method pointer was not jitted when creating one delegate and jitted in the other
// if that's the case the delegates may still be equals but we need to make a more complicated check
if (_methodPtrAux == IntPtr.Zero)
{
if (d._methodPtrAux != IntPtr.Zero)
return false; // different delegate kind
// they are both closed over the first arg
if (_target != d._target)
return false;
// fall through method handle check
}
else
{
if (d._methodPtrAux == IntPtr.Zero)
return false; // different delegate kind
// Ignore the target as it will be the delegate instance, though it may be a different one
/*
if (_methodPtr != d._methodPtr)
return false;
*/
if (_methodPtrAux == d._methodPtrAux)
return true;
// fall through method handle check
}
// method ptrs don't match, go down long path
//
if (_methodBase == null || d._methodBase == null || !(_methodBase is MethodInfo) || !(d._methodBase is MethodInfo))
return Delegate.InternalEqualMethodHandles(this, d);
else
return _methodBase.Equals(d._methodBase);
}
于是可以看出来,判断相等就是两个关键对象的判断相等:
MethodInfo
)继续回到这段代码:
void Subscribe(FileSystemWatcher watcher)
{
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.Changed -= new FileSystemEventHandler(OnChanged);
}
void OnChanged(object sender, FileSystemEventArgs e)
{
}
这里的对象就是 this
,方法信息就是 OnChanged
的信息,也就是:
// this 就是对象,OnChanged 就是方法信息。
this.OnChanged
-=
于是什么样的 -=
才可以把 +=
加进去的事件处理函数减掉呢?
所以:
+=
和 -=
的时候无视哪个委托实例,都是可以减掉的;dotnet 职业技术学院 发布于 2019-10-29
有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe
fixed
关键字。但使用此关键字的前提是需要在项目中开启不安全代码。
本文介绍如何在项目中开启不安全代码。
第一步:在你需要启用不安全代码的项目上点击右键,然后选择属性:
第二步:在“生成”标签下,勾选上“允许不安全代码”:
第三步:切换到 Release 配置,再勾上一次“允许不安全代码”(确保 Debug 和 Release 都打开)
方法结束。
如果你一开始选择了“所有配置”,那么就不需要分别在 Debug 和 Release 下打开了,一次打开即可。
推荐
如果你使用 .NET Core / .NET Standard 项目,那么你可以修改项目文件来实现,这样项目文件会更加清真。
第一步:在你需要启用不安全代码的项目上点击右键,然后选择编辑项目文件:
第二步:在你的项目文件的属性组中添加一行 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
:
我已经把需要新增的行高亮出来了
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
++ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
不推荐
如果你只是临时希望加上不安全代码开关,则可以在编译的时候加入 -unsafe
命令行参数:
csc -unsafe walterlv.cs
注意,不能给 msbuild
或者 dotnet build
加上 -unsafe
参数来编译项目,只能使用 csc
加上 -unsafe
来编译文件。因此使用场景非常受限,不推荐使用。
第一种方法(入门方法)和第二种方法(高级方法)最终的修改是有一些区别的。入门方法会使得项目文件中有针对于 Debug 和 Release 的不同配置,代码会显得冗余;而高级方法中只增加了一行,对任何配置均生效。
因此如果可能,尽量使用高级方法呗。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
++ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
-- <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
-- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-- </PropertyGroup>
-- <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-- </PropertyGroup>
</Project>
即使是 .NET Framework 也是可以使用 SDK 风格的项目文件的,详情请阅读:
dotnet 职业技术学院 发布于 2019-10-29
我只是增加库的一个 API,比如增加几个类而已,应该不会造成兼容性问题吧。对于编译好的二进制文件来说,不会造成兼容性问题;但——可能造成源码不兼容。
本文介绍可能的源码不兼容问题。
This post is written in multiple languages. Please select yours:
比如我有一个项目 P 引用 A 和 B 两个库。其中使用到了 A 库中的 Walterlv.A.Diagnostics.Foo
类型。
using Walterlv.A;
using Walterlv.B;
namespace Walterlv.Demo
{
class Hello
{
Run(Diagnostics.Foo foo)
{
}
}
}
现在,我们在 B 库中新增一个类型 Walterlv.B.Diagnostics.Bar
类型。
那么上面的代码将无法完成编译,因为 Diagnosis
命名空间将具有不确定的含义,其中的 Foo
类型也将无法在不确定的命名空间中找到。
因此:
using
),要么写全命名空间(从第一段开始写,不要省略任何部分),否则就容易与其他命名空间冲突;是的,即使是单纯的新增 API 也可能会导致使用库的一方在源码级不兼容。当然二进制还是兼容的。
另外,OpportunityLiu 提醒,如果命名空间是 Walterlv.B.Walterlv.A.Diagnostics.Bar
,一样可以让写全了的命名空间炸掉。呃……还是不要在库里面折腾这样的命名空间好……不然代码当中到处充斥着 global::
可是非常难受的。
dotnet 职业技术学院 发布于 2019-10-22
我们知道,32 位程序在读取注册表的时候,会自动将注册表的路径映射到 32 位路径下,即在 Wow6432Node
子节点下。但是 64 位程序不会映射到 32 位路径下。那么 64 位程序如何读取到 32 位程序写入的注册表路径呢?
对于 32 位程序,读取注册表路径的时候,会读到 Wow6432Node
节点下的项:
这张图读取的就是前面截图中的节点。
那么怎样编译的程序是 32-bit 的程序呢?
对于 64 位程序,读取的时候就不会有 Wow6432Node
路径部分。由于我没有在那个路径放注册表项,所以会得到 null
。
那么怎样编译的程序是 64-bit 的程序呢?
前面我们的例子代码是这样的:
var value = RegistryHive.LocalMachine.Read(@"SOFTWARE\Walterlv");
可以看到,相同的代码,在 32 位和 64 位进程下得到的结果是不同的:
Wow6432Node
。那么如何在 64 位进程中读取 32 位注册表路径呢?
方法是在打开注册表项的时候,传入 RegistryView.Registry32
。
RegistryKey.OpenBaseKey(root, RegistryView.Registry32);
可以在我的 GitHub 仓库中查看完整的实现。当然,除了上面那句话,其他都不是关键代码,在哪里都可以找得到的。
参考资料
dotnet 职业技术学院 发布于 2019-10-22
因为 Win32 的窗口句柄是可以跨进程传递的,所以可以用来实现跨进程 UI。不过,本文不会谈论跨进程 UI 的具体实现,只会提及其实现中的一个重要缓解,使用子窗口的方式。
你有可能在使用子窗口之后,发现拖拽改变窗口大小的时候,子窗口中的内容不断闪烁。如果你也遇到了这样的问题,那么正好可以阅读本文来解决。
你可以看一下下面的这张动图,感受一下窗口的闪烁:
实际上在拖动窗口的时候,是一直都在闪的,只是每次闪烁都非常快,截取 gif 的时候截不到。
如果你希望实际跑一跑项目看看,可以使用下面的代码:
我特地提取了一个提交下的代码,如果你要尝试,不能使用 master
分支,因为 master
分支修复了闪烁的问题。
后来使用 CreateWindowEx
创建了一个纯 Win32 窗口,这种闪烁现象更容易被截图:
public class HwndWrapper : HwndHost
{
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
const int WS_CHILD = 0x40000000;
++ const int WS_CLIPCHILDREN = 0x02000000;
var owner = ((HwndSource)PresentationSource.FromVisual(this)).Handle;
var parameters = new HwndSourceParameters("demo")
{
ParentWindow = owner,
-- WindowStyle = (int)(WS_CHILD),
++ WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
};
var source = new HwndSource(parameters);
source.RootVisual = new ChildPage();
return new HandleRef(this, source.Handle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
}
}
正在探索……
参考资料
dotnet 职业技术学院 发布于 2019-10-22
在 WPF 获取鼠标当前坐标的时候,可能会得到一个异常:System.ComponentModel.Win32Exception:“无效的窗口句柄。”
。
本文解释此异常的原因和解决方法。
获取鼠标当前相对于元素 element
的坐标的代码:
var point = Mouse.GetPosition(element);
或者,还有其他的代码:
var point1 = e.PointFromScreen(new Point());
var point2 = e.PointToScreen(new Point());
如果在按下窗口关闭按钮的时候调用以上代码,则会引发异常:
System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。
at Point MS.Internal.PointUtil.ClientToScreen(Point pointClient, PresentationSource presentationSource)
at Point System.Windows.Input.MouseDevice.GetScreenPositionFromSystem()
将窗口上的点转换到控件上的点的方法是这样的:
/// <summary>
/// Convert a point from "client" coordinate space of a window into
/// the coordinate space of the screen.
/// </summary>
/// <SecurityNote>
/// SecurityCritical: This code causes eleveation to unmanaged code via call to GetWindowLong
/// SecurityTreatAsSafe: This data is ok to give out
/// validate all code paths that lead to this.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
public static Point ClientToScreen(Point pointClient, PresentationSource presentationSource)
{
// For now we only know how to use HwndSource.
HwndSource inputSource = presentationSource as HwndSource;
if(inputSource == null)
{
return pointClient;
}
HandleRef handleRef = new HandleRef(inputSource, inputSource.CriticalHandle);
NativeMethods.POINT ptClient = FromPoint(pointClient);
NativeMethods.POINT ptClientRTLAdjusted = AdjustForRightToLeft(ptClient, handleRef);
UnsafeNativeMethods.ClientToScreen(handleRef, ptClientRTLAdjusted);
return ToPoint(ptClientRTLAdjusted);
}
最关键的是 UnsafeNativeMethods.ClientToScreen
,此方法要求窗口句柄依然有效,然而此时窗口已经关闭,句柄已经销毁。
dotnet 职业技术学院 发布于 2019-10-22
如果你得到了一个来自于其他进程或者其他模块的 Direct3D11 的共享资源,即 SharedHandle 句柄,那么可以使用本文提到的方法将其转换成 Direct3D11 的设备和纹理,这样你可以进行后续的其他处理。
本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:
<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />
Direct3D 支持在不同的 Direct3D 设备之间共享资源。需要设置 ResourceOptionFlags
为 Shared
的纹理才可以支持共享,当然这不是本文要说的重点。
本文要说的是,如果你拿到了一个来自于其他模块的共享资源句柄的时候,你可以如何使用它。
你的使用可能类似于这样:
public void OnAcceleratedPaint(IntPtr sharedHandle, Int32Rect dirtyRect)
{
// 通过 sharedHandle 进行后续的处理。
}
DirectX 中用来表示 Direct3D11 的设备类型是 ID3D11Device
,它有一个 OpenSharedResource
方法可以用来打开来自于其他设备的共享资源。
对应到 SharpDX 中,用来表示 Direct3D11 的设备的类型是 SharpDX.Direct3D11.Device
,其有一个 OpenSharedResource<T>
方法来打开来自于其他设备的共享资源。
我们必须要创建一个自己的 Direct3D11 设备,因为设备是不共享的,代码如下:
var device = new SharpDX.Direct3D11.Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport);
var resource = device.OpenSharedResource<SharpDX.Direct3D11.Resource>(sharedHandle);
在得到此共享资源之后,我们可以获得更多关于此资源的描述,以及有限地使用此资源的方法。
可以通过 QueryInterface
获取某个资源相关的 COM 对象的引用。我们拿到的共享资源是 2D 纹理的话,我们可以使用 QueryInterface
获取 SharpDX.Direct3D11.Texture2D
COM 对象的引用。
var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
可以从 Texture2D
的实例中获取到 Texture2DDescription
,这是用来描述此 2D 纹理创建时的各种信息。
// 在 DirectX 的传统代码中,通常使用 desc 来作为 Texture2DDescription 实例命名的后缀。
// 不过 C# 代码通常不这么干,这是 C++ 代码的习惯。在这里这么写是为了在得到 C++ 搜索结果的时候可以与本文所述的 C# 代码对应起来。
var desc = texture.Description;
或者,我们可以获取到 2D 图面,用于做渲染、绘制等操作。当然,是否能真正进行这些操作取决于 Texture2DDescription
中是否允许相关的操作。
var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>();
在获取到 SharpDX.DXGI.Surface
的 COM 组件引用之后,可以在内存中映射位图用于调试,可以参见:
参考资料
dotnet 职业技术学院 发布于 2019-10-22
Direct3D11 的使用通常不是应用程序唯一的部分,于是使用 Direct3D11 的代码如何与其他模块正确地组合在一起就是一个需要解决的问题。
本文介绍将 Direct3D11 在 GPU 中绘制的纹理映射到内存中,这样我们可以直接观察到此纹理是否是正确的,而不用担心是否有其他模块影响了最终的渲染过程。
本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:
<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />
本文不会说如何创建或者获取来自 Direct3D11 的渲染纹理,不过如果你希望了解,可以:
本文接下来的内容,是在你已经获得了 SharpDX.Direct3D11.Resource
的引用,或者 SharpDX.Direct3D11.Texture2D
的前提之下。当然,如果你获得了其中任何一个实例,可以通过 COM 组件的 QueryInterface
方法获得其他实例。
var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
var resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();
要获得 GPU 中渲染的图片,我们必须要将其映射到内存中才行。而映射到内存中的核心代码是 SharpDX.DXGI.Surface
对象的 Map
方法。
using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
{
var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
for (var y = 0; y < surface.Description.Height; y++)
{
for (var x = 0; x < surface.Description.Width; x++)
{
// 在这里使用位图的像素数据,坐标为 (x, y)。
// 得到此坐标下的像素指针:
// var ptr = ((byte*)map.DataPointer) + y * map.Pitch;
// 得到此像素的颜色值:
// var b = *(ptr + 4 * x);
// var g = *(ptr + 4 * x + 1);
// var r = *(ptr + 4 * x + 2);
// var a = *(ptr + 4 * x + 3);
}
}
dataStream.Dispose();
surface.Unmap();
}
注意以上代码使用了不安全代码(指针),你需要为你的项目开启不安全代码开关,详见:
实际上,在使用上面的代码时,你可能会遇到错误,错误出现在 Map
方法的调用上,描述为“参数错误”。实际上真正检查这里的两个参数时并不能发现究竟是哪个参数出了问题。
实际上出问题的参数是 surface
的实例。
一段 GPU 中的纹理要能够被映射到内存,必须要具有 CPU 的访问权。而是否具有 CPU 访问权在创建纹理的时候就已经确定下来了。
如果前面你得到的纹理是自己创建的,那么恭喜你,你只需要改一下创建纹理的参数就好了。给 Texture2DDescription
的 CpuAccessFlags
属性加上 CpuAccessFlags.Read
标识。
desc.CpuAccessFlags = CpuAccessFlags.Read;
但是,如果此纹理不是由你自己创建的,那么就需要拷贝一份新的纹理了。当然,拷贝过程发生在 GPU 中,占用的也是 GPU 专用内存(即显存,如果有的话)。
拷贝需要做到两点:
Texture2DDescription
(一定要是新的实例,你不能影响原来的实例),然后修改其 CPU 访问权限为 Read
;ImmediateContext
实例的 CopyResource
方法来拷贝资源(此实例可以通过 SharpDX.Direct3D11.Device
来找到)。var originalDesc = originalTexture.Description;
var desc = new Texture2DDescription
{
CpuAccessFlags = CpuAccessFlags.Read,
BindFlags = BindFlags.None,
Usage = ResourceUsage.Staging,
Width = originalDesc.Width,
Height = originalDesc.Height,
Format = originalDesc.Format,
MipLevels = 1,
ArraySize = 1,
SampleDescription =
{
Count = 1,
Quality = 0
},
};
var texture2D = new Texture2D(device, desc);
device.ImmediateContext.CopyResource(originalTexture, texture2D);
需要注意,拷贝纹理会额外占用显存,一般不建议这么做,除非你真的有需求一定要 CPU 能够访问到这段纹理。
实际上,当你组合起来以上以上方法,你应该能够将纹理导出成图片了。
不过,为了理解更方便一些,我还是将导出成图片的全部代码贴出来:
public static unsafe void MapTexture2DToFile(SharpDX.Direct3D11.Texture2D texture, string fileName)
{
// 获取 Texture2D 的相关实例。
var device = texture.Device;
var originDesc = texture.Description;
// 创建新的 Texture2D 对象。
var desc = new Texture2DDescription
{
CpuAccessFlags = CpuAccessFlags.Read,
BindFlags = BindFlags.None,
Usage = ResourceUsage.Staging,
Width = originDesc.Width,
Height = originDesc.Height,
Format = originDesc.Format,
MipLevels = 1,
ArraySize = 1,
SampleDescription =
{
Count = 1,
Quality = 0
},
OptionFlags = ResourceOptionFlags.Shared
};
var texture2D = new Texture2D(device, desc);
// 拷贝资源。
device.ImmediateContext.CopyResource(texture, texture2D);
var bitmap = new System.Drawing.Bitmap(desc.Width, desc.Height);
using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
{
var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
var lines = (int)(dataStream.Length / map.Pitch);
var actualWidth = surface.Description.Width * 4;
for (var y = 0; y < desc.Height; y++)
{
var h = desc.Height - y;
var ptr = ((byte*)map.DataPointer) + y * map.Pitch;
for (var x = 0; x < desc.Width; x++)
{
var b = *(ptr + 4 * x);
var g = *(ptr + 4 * x + 1);
var r = *(ptr + 4 * x + 2);
var a = *(ptr + 4 * x + 3);
bitmap.SetPixel(x, y, System.Drawing.Color.FromArgb(a, r, g, b));
}
}
dataStream.Dispose();
surface.Unmap();
bitmap.Save(fileName);
}
}
如果你是希望以纯软件的方式渲染到 WPF 中(WriteableBitmap),可以参考:
记得打开不安全代码开关哦!详见:
参考资料
dotnet 职业技术学院 发布于 2019-10-22
我们知道 Windows 系统 NTFS 文件系统提供了硬连接功能,可以通过 mklink
命令开启。如果能够通过代码实现,那么我们能够做更多有趣的事情。
本文提供使用 .NET/C# 代码创建 NTFS 文件系统的硬连接功能(目录联接)。
以管理员权限启动 CMD(命令提示符),输入 mklink
命令可以得知 mklink 的用法。
C:\WINDOWS\system32>mklink
创建符号链接。
MKLINK [[/D] | [/H] | [/J]] Link Target
/D 创建目录符号链接。默认为文件
符号链接。
/H 创建硬链接而非符号链接。
/J 创建目录联接。
Link 指定新的符号链接名称。
Target 指定新链接引用的路径
(相对或绝对)。
我们本次要用 .NET/C# 代码实现的是 /J
目录联接。实现的效果像这样:
这些文件夹带有一个“快捷方式”的角标,似乎是另一些文件夹的快捷方式一样。但这些与快捷方式的区别在于,应用程序读取路径的时候,目录联接会成为路径的一部分。
比如在 D:\Walterlv\NuGet\
中创建 debug
目录联接,目标设为 D:\Walterlv\DemoRepo\bin\Debug
,那么,你在各种应用程序中使用以下两个路径将被视为同一个:
D:\Walterlv\NuGet\debug\DemoRepo-1.0.0.nupkg
D:\Walterlv\DemoRepo\bin\Debug\DemoRepo-1.0.0.nupkg
或者这种:
D:\Walterlv\NuGet\debug\publish\
D:\Walterlv\DemoRepo\bin\Debug\publish\
本文的代码主要参考自 jeff.brown 在 Manipulating NTFS Junction Points in .NET - CodeProject 一文中所附带的源代码。
由于随时可能更新,所以你可以前往 GitHub 仓库打开此代码:
如果希望在代码中创建目录联接,则直接使用:
JunctionPoint.Create("walterlv.demo", @"D:\Developments", true);
后面的 true
指定如果目录联接存在,则会覆盖掉原来的目录联接。
参考资料
dotnet 职业技术学院 发布于 2019-10-22
WPF 渲染框架并没有对外提供多少可以完全控制渲染的部分,目前可以做的有:
本文将解释如何最大程度压榨 WriteableBitmap
在 WPF 下的性能。
创建一个新的 WPF 项目,然后我们在 MainWindow.xaml 中编写一点可以用来显示 WriteableBitmap
的代码:
<Window x:Class="Walterlv.Demo.HighPerformanceBitmap.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Walterlv.Demo.HighPerformanceBitmap"
Title="WriteableBitmap - walterlv" SizeToContent="WidthAndHeight">
<Grid>
<Image x:Name="Image" Width="1280" Height="720" />
</Grid>
</Window>
为了评估其性能,我决定绘制和渲染 4K 品质的位图,并通过以下步骤来评估:
CompositionTarget.Rendering
逐帧渲染以评估其渲染帧率于是,在 MainWindow.xaml.cs 中添加一些测试用的修改 WriteableBitmap
的代码:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Walterlv.Demo.HighPerformanceBitmap
{
public partial class MainWindow : Window
{
private readonly WriteableBitmap _bitmap;
public MainWindow()
{
InitializeComponent();
_bitmap = new WriteableBitmap(3840, 2160, 96.0, 96.0, PixelFormats.Pbgra32, null);
Image.Source = _bitmap;
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
var width = _bitmap.PixelWidth;
var height = _bitmap.PixelHeight;
_bitmap.Lock();
// 在这里添加绘制位图的逻辑。
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
}
}
注意,我留了一行注释说即将添加绘制位图的逻辑,接下来我们的主要内容将从此展开。
为了获取最佳性能,我们需要开启不安全代码。为此,你需要修改一下你的项目属性。
你可以阅读我的另一篇博客了解如何启用不安全代码:
简单点说就是在你的项目文件中添加下面这一行:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
++ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
接下来,我们需要添加一点点代码来评估 WriteableBitmap
的性能:
++ private readonly byte[] _empty4KBitmapArray = new byte[3840 * 2160 * 4];
-- private void CompositionTarget_Rendering(object sender, EventArgs e)
++ private unsafe void CompositionTarget_Rendering(object sender, EventArgs e)
{
var width = _bitmap.PixelWidth;
var height = _bitmap.PixelHeight;
_bitmap.Lock();
++ fixed (byte* ptr = _empty4KBitmapArray)
++ {
++ var p = new IntPtr(ptr);
++ Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++ }
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
嗯,就是将一个空的 4K 大小的数组中的内容复制到 WriteableBitmap
的位图缓存中。
虽然我们看不到任何可变的修改,不过 WriteableBitmap 可不这么认为。因为我们调用了 AddDirtyRect
将整个位图空间都加入到了脏区中,这样 WPF 会重新渲染整幅位图。
Visual Studio 中看到的 CPU 占用率大约维持在 16% 左右(跟具体机器相关);并且除了一开始启动的时候之外,完全没有 GC(这点很重要),内存稳定在一个值上不再变化。
也只有本文一开始提及的三种方法才可能做到渲染任何可能的图形的时候没有 GC
查看界面渲染帧率可以发现跑满 60 帧没有什么问题(跟具体机器相关)。
现在,我们把脏区的区域缩小为 100*100,同样看性能数据。
-- _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++ _bitmap.AddDirtyRect(new Int32Rect(0, 0, 100, 100));
可以发现 CPU 占用降低到一半(确实是大幅降低,但是跟像素数量并不成比例);内存没有变化(废话,4K 图像是确定的);帧率没有变化(废话,只要性能够,帧率就是满的)。
现在,我们将脏区清零。
-- _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++ _bitmap.AddDirtyRect(new Int32Rect(0, 0, 0, 0));
在完全没有脏区的时候,CPU 占用直接降为 0,这个性能提升还是非常恐怖的。
如果我们不把 WriteableBitmap 设置为 Image
的 Source
属性,那么无论脏区多大,CPU 占用都是 0。
从前面的测试中我们可以发现,脏区的大小在 WriteableBitmap
的渲染里占了绝对的耗时。因此,我把脏区大小与 CPU 占用率之间的关系用图表的形式贴出来,这样可以直观地理解其性能差异。
需要注意,CPU 占用率与机器性能强相关,因此其绝对占用没有意义,但相对大小则有参考价值。
脏区大小 | CPU 占用率 | 帧率 |
---|---|---|
0*0 | 0.0% | 60 |
1*1 | 5.1% | 60 |
16*9 | 5.7% | 60 |
160*90 | 6.0% | 60 |
320*180 | 6.5% | 60 |
640*360 | 6.9% | 60 |
1280*720 | 7.5% | 60 |
1920*1080 | 10.5% | 60 |
2560*1440 | 12.3% | 60 |
3840*2160 | 16.1% | 60 |
根据这张表我么可以得出:
但是有一个需要注意的信息是——虽然 CPU 占用率受脏区影响非常大,但主线程却几乎没有消耗 CPU 占用。此占用基本上全是渲染线程的事。
如果我们分析主线程的性能分布,可以发现内存拷贝现在是性能瓶颈:
后面我们会提到 WriteableBitmap 的渲染原理,也会说到这一点。
不过,由于内存数据的拷贝和脏区渲染实际上可以分开到两个不同的线程,如果这两者不同步执行(可能执行次数还有差异)的情况下,内存拷贝也可能成为性能瓶颈的一部分。
于是我将不同的内存拷贝方法进行一个基准测试,便于大家评估使用哪种方法来为 WriteableBitmap 提供渲染数据。
CopyMemory
拷贝内存++ [Benchmark(Description = "CopyMemory")]
++ [Arguments(3840, 2160)]
++ [Arguments(100, 100)]
public unsafe void CopyMemory(int width, int height)
{
_bitmap.Lock();
++ fixed (byte* ptr = _empty4KBitmapArray)
++ {
++ var p = new IntPtr(ptr);
++ CopyMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++ }
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);
MoveMemory
移动内存++ [Benchmark(Description = "RtlMoveMemory")]
++ [Arguments(3840, 2160)]
++ [Arguments(100, 100)]
public unsafe void RtlMoveMemory(int width, int height)
{
_bitmap.Lock();
++ fixed (byte* ptr = _empty4KBitmapArray)
++ {
++ var p = new IntPtr(ptr);
++ MoveMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++ }
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);
Buffer.MemoryCopy
拷贝内存需要注意,Buffer.MemoryCopy
是 .NET Framework 4.6 才引入的 API,在 .NET Framework 后续版本以及 .NET Core 的所有版本才可以使用,更旧版本的 .NET Framework 没有这个 API。
++ [Benchmark(Baseline = true, Description = "Buffer.MemoryCopy")]
++ [Arguments(3840, 2160)]
++ [Arguments(100, 100)]
public unsafe void BufferMemoryCopy(int width, int height)
{
_bitmap.Lock();
++ fixed (byte* ptr = _empty4KBitmapArray)
++ {
++ var p = new IntPtr(ptr);
++ Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++ }
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
++ [Benchmark(Description = "for for")]
++ [Arguments(3840, 2160)]
++ [Arguments(100, 100)]
public unsafe void ForForCopy(int width, int height)
{
_bitmap.Lock();
++ var buffer = (byte*)_bitmap.BackBuffer.ToPointer();
++ for (var j = 0; j < height; j++)
++ {
++ for (var i = 0; i < width; i++)
++ {
++ var pixel = buffer + j * width * 4 + i * 4;
++ *pixel = 0xff;
++ *(pixel + 1) = 0x7f;
++ *(pixel + 2) = 0x00;
++ *(pixel + 3) = 0xff;
++ }
++ }
_bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
_bitmap.Unlock();
}
我们跑一次基准测试:
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
CopyMemory | 2.723 ms | 0.0642 ms | 0.1881 ms | 2.677 ms | 0.84 | 0.08 |
RtlMoveMemory | 2.659 ms | 0.0740 ms | 0.2158 ms | 2.633 ms | 0.82 | 0.08 |
Buffer.MemoryCopy | 3.246 ms | 0.0776 ms | 0.2250 ms | 3.200 ms | 1.00 | 0.00 |
‘for for’ | 10.401 ms | 0.1979 ms | 0.4964 ms | 10.396 ms | 3.21 | 0.25 |
‘CopyMemory with 100*100 dirty region’ | 2.446 ms | 0.0757 ms | 0.2207 ms | 2.368 ms | 0.76 | 0.09 |
‘RtlMoveMemory with 100*100 dirty region’ | 2.415 ms | 0.0733 ms | 0.2161 ms | 2.369 ms | 0.75 | 0.08 |
‘Buffer.MemoryCopy with 100*100 dirty region’ | 3.076 ms | 0.0612 ms | 0.1523 ms | 3.072 ms | 0.95 | 0.08 |
‘for for with 100*100 dirty region’ | 10.014 ms | 0.2398 ms | 0.6995 ms | 9.887 ms | 3.10 | 0.29 |
可以发现:
CopyMemory
和 RtMoveMemory
性能是最好的,其性能差不多;综合前面两者的结论,我们可以发现:
另外,如果你有一些特殊的应用场景,可以适当调整下自己写代码的策略:
在调用 WriteableBitmap 的 AddDirtyRect
方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect
,这是 WPF 专门为 WriteableBitmap 而提供的非托管代码的双缓冲位图的实现。
在 WriteableBitmap 内部数组修改完毕之后,需要调用 Unlock
来解锁内部缓冲区的访问,这时会提交所有的修改。接下来的渲染都交给了 MediaContext
,用来完成双缓冲位图的渲染。
private void SubscribeToCommittingBatch()
{
// Only subscribe the the CommittingBatch event if we are on-channel.
if (!_isWaitingForCommit)
{
MediaContext mediaContext = MediaContext.From(Dispatcher);
if (_duceResource.IsOnChannel(mediaContext.Channel))
{
mediaContext.CommittingBatch += CommittingBatchHandler;
_isWaitingForCommit = true;
}
}
}
在上面的 CommittingBatchHandler
中,将渲染指令发送到了渲染线程。
channel.SendCommand((byte*)&command, sizeof(DUCE.MILCMD_DOUBLEBUFFEREDBITMAP_COPYFORWARD));
前面我们通过脏区大小可以得出内存拷贝不是 CPU 占用率的瓶颈,脏区大小才是,不过是渲染线程在占用这 CPU 而不是主线程。但是内存拷贝却成为了主线程的瓶颈(当然前面我们给出了数据,实际上非常小)。所以如果试图分析这么高 CPU 的占用,会发现并不能从主线程上调查得出符合预期的结论(因为即便你完全干掉了内存拷贝,CPU 占用依然是这么高)。
dotnet 职业技术学院 发布于 2019-10-17
在 WPF 中,如果想做一个背景透明的异形窗口,基本上都要设置 WindowStyle="None"
、AllowsTransparency="True"
这两个属性。如果不想自定义窗口样式,还需要设置 Background="Transparent"
。这样的设置会让窗口变成 Layered Window,WPF 在这种类型窗口上的渲染性能是非常糟糕的。
本文介绍如何使用 WindowChrome
而不设置 AllowsTransparency="True"
制作背景透明的异形窗口,这可以避免异形窗口导致的低渲染性能。
如下是一个背景透明异形窗口的示例:
此窗口包含很大的圆角,还包含 DropShadowEffect
制作的阴影效果。对于非透明窗口来说,这是不可能实现的。
要实现这种背景透明的异形窗口,需要为窗口设置以下三个属性:
WindowStyle="None"
ResizeMode="CanMinimize"
或 ResizeMode="NoResize"
WindowChrome.GlassFrameThickness="-1"
或设置为其他较大的正数(可自行尝试设置之后的效果)如下就是一个最简单的例子,最关键的三个属性我已经高亮标记出来了。
<Window x:Class="Walterlv.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
++ WindowStyle="None" ResizeMode="CanMinimize"
Title="walterlv demo" Height="450" Width="800">
++ <WindowChrome.WindowChrome>
++ <WindowChrome GlassFrameThickness="-1" />
++ </WindowChrome.WindowChrome>
<Window.Template>
<ControlTemplate TargetType="Window">
<Border Padding="64" Background="Transparent">
<Border CornerRadius="16" Background="White">
<Border.Effect>
<DropShadowEffect BlurRadius="64" />
</Border.Effect>
<ContentPresenter ClipToBounds="True" />
</Border>
</Border>
</ControlTemplate>
</Window.Template>
<Grid>
<TextBlock FontSize="20" Foreground="#0083d0"
TextAlignment="Center" VerticalAlignment="Center">
<Run Text="欢迎访问吕毅的博客" />
<LineBreak />
<Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
</TextBlock>
</Grid>
</Window>
在网上流传的主流方法中,AllowsTransparency="True"
都是一个必不可少的步骤,另外也需要 WindowStyle="None"
。但是我一般都会极力反对大家这么做,因为 AllowsTransparency="True"
会造成很严重的性能问题。
如果你有留意到我的其他博客,你会发现我定制窗口样式的时候都在极力避开设置此性能极差的属性:
既然特别说到性能,那也是口说无凭,我们要拿出数据来说话。
以下是我用来测试渲染性能所使用的例子:
相比于上面的例子来说,主要就是加了背景动画效果,这可以用来测试帧率。
<Window x:Class="Walterlv.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None" ResizeMode="CanMinimize"
Title="walterlv demo" Height="450" Width="800">
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
<Window.Template>
<ControlTemplate TargetType="Window">
<Border Padding="64" Background="Transparent">
<Border CornerRadius="16" Background="White">
<Border.Effect>
<DropShadowEffect BlurRadius="64" />
</Border.Effect>
<ContentPresenter ClipToBounds="True" />
</Border>
</Border>
</ControlTemplate>
</Window.Template>
<Grid>
++ <Rectangle x:Name="BackgroundRectangle" Margin="0 16" Fill="#d0d1d6">
++ <Rectangle.RenderTransform>
++ <TranslateTransform />
++ </Rectangle.RenderTransform>
++ <Rectangle.Triggers>
++ <EventTrigger RoutedEvent="FrameworkElement.Loaded">
++ <BeginStoryboard>
++ <BeginStoryboard.Storyboard>
++ <Storyboard RepeatBehavior="Forever">
++ <DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
++ Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
++ From="800" To="-800" />
++ </Storyboard>
++ </BeginStoryboard.Storyboard>
++ </BeginStoryboard>
++ </EventTrigger>
++ </Rectangle.Triggers>
++ </Rectangle>
<TextBlock FontSize="20" Foreground="#0083d0"
TextAlignment="Center" VerticalAlignment="Center">
<Run Text="欢迎访问吕毅的博客" />
<LineBreak />
<Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
</TextBlock>
</Grid>
</Window>
那么性能数据表现如何呢?我们让这个窗口在 2560×1080 的屏幕上全屏渲染,得出以下数据:
方案 | WindowChrome | AllowsTransparency |
---|---|---|
帧率(fps)数值越大越好,60 为最好 | 59 | 19 |
脏区刷新率(rects/s)数值越大越好 | 117 | 38 |
显存占用(MB)数值越小越好 | 83.31 | 193.29 |
帧间目标渲染数(个)数值越大越好 | 2 | 1 |
另外,对于显存的使用,如果我在 7680×2160 的屏幕上全屏渲染,WindowChrome
方案依然保持在 80+MB,而 AllowsTransparency
已经达到惊人的 800+MB 了。
可见,对于渲染性能,使用 WindowChrome
制作的背景透明异形窗口性能完虐使用 AllowsTransparency
制作的背景透明异形窗口,实际上跟完全没有设置透明窗口的性能保持一致。
既然 WindowChrome
方法在性能上完虐网上流传的设置 AllowsTransparency
方法,那么功能呢?
值得注意的是,由于在使用 WindowChrome
制作透明窗口的时候设置了 ResizeMode="None"
,所以你拖动窗口在屏幕顶部和左右两边的时候,Windows 不会再帮助你最大化窗口或者靠边停靠窗口,于是你需要自行处理。不过窗口的标题栏拖动功能依然保留了下来,标题栏上的右键菜单也是可以继续使用的。
方案 | WindowChrome | AllowsTransparency |
---|---|---|
拖拽标题栏移动窗口 | 保留 | 自行实现 |
最小化最大化关闭按钮 | 丢失 | 丢失 |
拖拽边缘调整窗口大小 | 丢失 | 丢失 |
移动窗口到顶部可最大化 | 丢失 | 自行实现 |
拖拽最大化窗口标题栏还原窗口 | 保留 | 自行实现 |
移动窗口到屏幕两边可侧边停靠 | 丢失 | 自行实现 |
拖拽摇动窗口以最小化其他窗口 | 保留 | 自行实现 |
窗口打开/关闭/最小化/最大化/还原动画 | 丢失 | 丢失 |
表格中:
保留
表示此功能无需任何处理即可继续支持自行实现
表示此功能已消失,但仅需要一两行代码即可补回功能丢失
表示此功能已消失,如需实现需要编写大量代码另外,以上表格仅针对鼠标操作窗口。如果算上使用触摸来操作窗口,那么所有标记为 自行实现
的都将变为 丢失
。因为虽然你可以一句话补回功能,但在触摸操作下各种 Bug,你解不完……
这两种实现的窗口之间还有一些功能上的区别:
方案 | WindowChrome | AllowsTransparency | 说明 |
---|---|---|---|
点击穿透 | 在完全透明的部分点击依然点在自己的窗口上 | 在完全透明的部分点击会穿透到下面的其他窗口 | 感谢 nocanstillbb (huang bin bin) 提供的信息 |