65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windows 11 on Arm64 和 .NET 实现桌面应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023 年 9 月 11 日

CPOL

13分钟阅读

viewsIcon

9808

downloadIcon

80

本文演示了如何使用 .NET 8.0 的 WPF 来实现一个在 Arm64 上运行的桌面应用程序。

.NET 是一个开源框架,用于构建跨平台的移动、桌面、物联网 (IoT) 和 Web 应用程序。.NET 平台还支持容器化和云原生解决方案。

传统的 .NET Framework 提供了 Windows Presentation Foundation (WPF),这是一个用于构建现代桌面应用的全面 UI 框架。通过 Model-View-ViewModel (MVVM) 架构设计模式,WPF 将用户界面 (UI) 设计(使用 XAML 标记创建)与应用的逻辑分离开来。

这种灵活性提高了跨平台移动开发对清晰关注点分离的吸引力。具体来说,Xamarin.Forms 引入了一种将 XAML 代码翻译成平台特定(iOS 或 Android)控件的方法,同时应用通过可重用视图模型共享其逻辑。Microsoft 将这种方法改编用于 .NET Multi-platform App UI (.NET MAUI),帮助开发人员快速为包括 Windows、iOS、Android 和 macOS 在内的各种平台实现应用程序。

其他框架也支持跨平台开发,例如 C++ 开发的 Qt 框架。一篇 CodeProject 文章演示了如何在 Windows 11 on Arm64 上本地运行 Qt 框架,其应用程序的运行速度比 X64 快,无需模拟。这种方法还受益于其他 Arm64 优势,包括低功耗和片上系统 (SoC) 架构。现在,在原生支持 Arm64 的 .NET 版本中,这些优势对不熟悉 C++ 的 C# 开发人员也可用。

本文演示了如何使用 .NET 8.0 的 WPF 来实现一个在 Arm64 上运行的桌面应用程序。该应用程序执行密集的计算工作,并重用了本系列第一部分实现的方阵乘法。它在标签上显示计算所需的时间,并将其渲染到图表中。您可以使用 Syncfusion.SfChart.WPF 库创建图表,该库可以作为 NuGet 包安装。最终,您可以将在此学到的关于 WPF 应用设计的所有知识迁移到 .NET MAUI。

本文使用 MVVM 设计模式进行实现,并为 x64 和 Arm64 平台编译应用程序。运行这两个应用程序表明,Arm64 在矩阵乘法方面的性能比 x64 提升了近 2.8 倍,如下面的图所示。这种性能提升来自于 Arm64 的原生运行,而不是使用模拟器翻译 x64 代码。

本教程手动实现了许多 MVVM 功能,为初学者打下良好基础。您可以使用 MVVM Community Toolkit 来加速开发。这种方法通过专用属性替换代码,帮助您避免手动实现各种接口。

查看本文的 配套代码,开发人员使用 Windows Dev Kit 2023 对其进行了测试。

项目设置

首先,下载 Visual Studio 2022 Community Preview(版本为 17.7.0 preview 2)。然后,安装 .NET 桌面开发工作负载。

下载 Visual Studio 的 .NET 8.0 SDK,包括适用于 x86x64Arm64 的 .NET 8.0。本文使用 .NET SDK 8.0.100 preview 5。另外,安装 x86 .NET SDK — Visual Studio 2022 需要它来渲染设计模式下的 WPF 视图。

现在,打开 Visual Studio 2022,在快速操作窗格中点击新建项目图标。

创建新项目窗口中,在搜索框中键入“WPF”。然后,选择 C# WPF 应用程序项目模板,然后单击下一步

按如下方式配置项目

  • 项目名称: Arm64.DesktopApp.WPF
  • 位置: 选择您想要的任何位置。
  • 解决方案名称: Arm64.DesktopApp

单击下一步进入最后一步(其他信息)。从框架下拉列表中选择 .NET 8.0 (Preview)。最后,单击创建。您会看到包含 MainWindow 中 XAML 声明的屏幕。

在设计 UI 之前,您需要安装 Syncfusion.SfChart.WPF NuGet 包。

在 Visual Studio 2022 中,打开视图 > 解决方案资源管理器。在解决方案资源管理器中,右键单击 Arm64.DesktopApp.WPF 下的依赖项。接下来,从上下文菜单中选择管理 NuGet 程序包。此操作将打开NuGet 程序包管理器

浏览选项卡下的NuGet 程序包管理器中,键入“Sf chart WPF”。然后,选择 Syncfusion.SfChart.WPF 并单击安装。

预览更改窗口中,单击确定以接受对解决方案所做的更改。

设计 UI

XAML 声明式标记语言有助于快速设计 WPF 应用程序的 UI,并使其易于将 UI 移植到多个环境。

通过在设计模式下手动将控件放置在窗口中来创建 UI。首先,通过在解决方案资源管理器中双击 MainWindow.xaml 来打开 MainWindow.xaml。XAML 代码将在窗口底部(应与下面的代码类似)。窗口顶部渲染 XAML 语句以生成预览(稍后在本节中显示)。

接下来,修改 MainWindow.xaml 文件,添加以下代码中粗体显示的语句

<Window x:Class="Arm64.DesktopApp.WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Arm64.DesktopApp.WPF"
        xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
        mc:Ignorable="d"
        Title="Arm"
        Height="450"
        Width="500">
 
    <Grid>
    </Grid>
</Window>

这些声明更新了窗口的标题和尺寸。此外,它们导入了 Syncfusion 控件的 XAML 命名空间。

现在,通过在 <Grid> 标记之前放置以下声明来定义匿名样式。

<Window.Resources>
    <Style TargetType="Button">
        <Setter Property="Margin"
                Value="2,2,2,0" />
        <Setter Property="FontWeight"
                Value="Bold" />
    </Style>
 
    <Style TargetType="Label">
        <Setter Property="FontWeight"
                Value="Bold" />
    </Style>
 
    <Style TargetType="syncfusion:SfChart">
        <Setter Property="Margin"
                Value="10" />
    </Style>
</Window.Resources>

这些声明创建了三个匿名样式。在当前窗口中,应用程序将它们应用于所有按钮、标签和 Syncfusion 图表。您将使用这些样式来配置边距和字体粗细。

接下来,配置网格的布局以包含两行两列。第一行的行高自动调整以容纳所有控件,而第二行占据窗口的剩余区域。列的宽度相等。实际上,这种配置创建了一个两列两行的表格布局

<Grid>   
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
</Grid>

使用此布局通过在关闭 </Grid> 标记之前添加以下语句来添加两个按钮、一个标签和一个图表

<!--Buttons-->
<StackPanel Grid.Row="0"
            Grid.Column="0"
            Orientation="Vertical">
    <Button Content="Run calculations"/>
    <Button Content="Plot results" />
</StackPanel>
 
<!--Label-->
<Label Grid.Row="0"
       Grid.Column="1"
       Content="" />
 
<!--Chart-->
<syncfusion:SfChart Grid.Row="1"
                    Grid.ColumnSpan="2">
    <syncfusion:SfChart.PrimaryAxis>
        <syncfusion:NumericalAxis Header="Trial number"
                                  FontSize="14" />
    </syncfusion:SfChart.PrimaryAxis>
 
    <syncfusion:SfChart.SecondaryAxis>
        <syncfusion:NumericalAxis Header="Computation time [ms]"
                                  FontSize="14"
                                  Maximum="3000"
                                  Minimum="0">
        </syncfusion:NumericalAxis>
    </syncfusion:SfChart.SecondaryAxis>
 
    <syncfusion:LineSeries EnableAnimation="True"
                           Label="Computation time">
    </syncfusion:LineSeries>
</syncfusion:SfChart>

此声明将两个按钮放在第一行的左单元格中,将标签放在第一行的第二个单元格中。它将跨越两个单元格的图表添加到第二行。

它还配置了图表以包含两个数值轴。水平(主)轴显示试验编号,垂直(次)轴显示该试验的计算时间。通过多次运行计算,您可以生成连续试验计算时间的图表。

完整的 XAML 声明在设计区域渲染以下视图,包括用于运行计算和绘制结果的按钮,以及用于显示每次试验计算时间的图表。

实现逻辑

您现在可以实现逻辑了。遵循 WPF 应用开发的最佳实践,并使用 MVVM 架构模式。

我们将从定义一个表示图表中的点的模型类开始。然后,我们将实现用于密集计算工作的辅助类,然后实现包含实际逻辑的视图模型。最后,我们将通过数据绑定将视图模型附加到视图。

在添加实现文件之前,请在 Arm64.DesktopApp.WPF 项目中创建以下文件夹

  • 模型
  • 辅助函数
  • ViewModels

使用解决方案资源管理器创建文件夹。首先,右键单击 Arm64.DesktopApp.WPF 项目。然后,从上下文菜单中选择添加 > 新建文件夹并键入文件夹名称。

模型

Models 文件夹中添加一个新文件来实现模型。在解决方案资源管理器中右键单击该文件夹。接下来,选择添加 > 类,然后在出现的窗口中,键入“DataPoint2d.cs”并按Enter

按如下方式修改新文件

public class DataPoint2d
{
    public double X { get; set; }
 
    public double Y { get; set; }
}

上述类表示图表中 XY 坐标。

辅助函数

接下来,在 Helpers 文件夹中创建另一个文件 MatrixHelper.cs此文件包含 MatrixHelper 类的定义,该类实现了方阵乘法。算法如本系列第一篇文章所述。对于以下讨论,最重要的是静态方法 MatrixHelper.SquareMatrixMultiplication,它运行计算密集型工作。

现在,您需要代码来测量计算时间。与第一篇文章一样,您依赖于 System.Diagnostics.Stopwatch 类。在 Helpers 文件夹中创建一个 PerformanceHelper.cs 文件并相应地修改该文件

using System.Diagnostics;
 
namespace Arm64.DesktopApp.WPF.Helpers
{
    public static class PerformanceHelper
    {
        private static readonly Stopwatch stopwatch = new();
 
        public static double MeasurePerformance(Action method, int executionCount)
        {
            stopwatch.Restart();
 
            for (int i = 0; i < executionCount; i++)
            {
                method();
            }
 
            stopwatch.Stop();
 
            return stopwatch.ElapsedMilliseconds;
        }
    }
}

此类包含一个公共静态方法 MeasurePerformance。此方法需要一个要执行的操作,而另一个参数 — executionCount — 指定调用该操作的次数。如上所示,PerformanceHelper.MeasurePerformance 调用 stopwatch.Restart 方法将秒表重置为零。然后,该方法将操作调用 executionCount 次。之后,它调用 stopwatch.Stop 并返回自重新启动秒表以来经过的时间(以毫秒为单位)。

ViewModel

为了使您的实现通用,请遵循最佳 MVVM 实践。首先,在 ViewModels 中创建一个新的 BaseViewModel.cs 文件并修改该文件

using System.ComponentModel;
using System.Runtime.CompilerServices;
 
namespace Arm64.DesktopApp.WPF.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
 
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(propertyName));
        }
 
        protected void SetProperty<T>(ref T property,
            T value, [CallerMemberName] string propertyName = "")
        {
            property = value;
            OnPropertyChanged(propertyName);
        }
    }
}

此类实现了可以在其他视图模型中重用的功能。BaseViewModel 使用 INotifyPropertyChanged 接口进行 数据绑定。此接口通知关联的视图有关基础属性更改的信息,从而允许视图更新它们向用户显示的内容。

为了实现 INotifyPropertyChanged 接口,此类定义了 PropertyChanged 事件。与视图模型关联的视图会自动订阅此事件以更新基础控件。

此外,您还实现了两个受保护的方法(可在派生类中使用)

  • OnPropertyChanged 引发 PropertyChanged 事件,以在视图模型属性更改时通知视图。
  • SetProperty 是一个辅助方法。它更新 property 值并调用 OnPropertyChanged 方法以将 property 更改传播到视图。

下一步是实现 命令。命令是控件事件处理程序的替代方案。但是,命令通常是框架独立的 — 您可以直接在视图模型中实现它们。因此,您可以在 WPF、Xamarin 或 .NET MAUI 应用中使用相同的命令。

创建一个实现 System.Windows.Input.ICommand 接口的类。此接口提供三个元素

  • Execute:用户操作(例如,按钮单击)调用的方法
  • CanExecute:一个指定是否可以调用命令的方法。通常,您使用此方法来确保应用程序状态或用户提供的输入有效,并允许命令执行。
  • CanExecuteChanged:每当应用程序状态的某些内容更改影响 CanExecute 方法的结果时引发的事件

使用 ICommand 接口的一个简单实现,该实现在一个名为 SimpleCommand.cs 的新文件中实现,代码如下。将其保存在 ViewModels 文件夹中。

using System.Windows.Input;
 
namespace Arm64.DesktopApp.WPF.ViewModels
{
    public class SimpleCommand : ICommand
    {
        public event EventHandler? CanExecuteChanged;
 
        private readonly Action<object?> action;
 
        public SimpleCommand(Action<object?> action)
        {
            this.action = action;
        }
 
        public bool CanExecute(object? parameter)
        {
            return true;
        }
 
        public void Execute(object? parameter)
        {
            if (CanExecute(parameter))
            {
                action(parameter);
            }
        }
    }
}

SimpleCommand 类包含 CanExecuteChanged 事件。它实现了 ICommand 接口中的 CanExecuteExecute 方法。

首先,Execute 方法调用 CanExecute 方法。如果该方法返回 trueExecute 将调用存储在 SimpleCommandaction 字段中的方法。action 字段封装了实现实际命令的技术。SimpleCommand 类的构造函数设置 action 字段。

现在,通过在 ViewModels 文件夹中创建一个名为 MainViewModel.cs 的新文件来实现实际的视图模型,如下所示

using Arm64.DesktopApp.WPF.Helpers;
using Arm64.DesktopApp.WPF.Models;
using System.Collections.ObjectModel;
 
namespace Arm64.DesktopApp.WPF.ViewModels
{
    public class MainViewModel : BaseViewModel
    {
        private string computationTime = "";
 
        public string ComputationTime
        {
            get => computationTime;
            set => SetProperty(ref computationTime, value);
        }
 
        private readonly List<DataPoint2d> computationTimeHistory = new();
 
        public ObservableCollection<DataPoint2d> DataPoints { get; set; } =
            new ObservableCollection<DataPoint2d>();
 
        private SimpleCommand? runCalculationsCommand;
        public SimpleCommand RunCalculationsCommand
        {
            get => runCalculationsCommand ??= new SimpleCommand((object? parameter) =>
            {
                var computationTime = PerformanceHelper.MeasurePerformance(
                    MatrixHelper.SquareMatrixMultiplication, executionCount: 2);
 
                ComputationTime = string.Format($"Platform: " +
                        $"{Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}\n" +
                        $"Computation time: {computationTime:f2} ms");
 
                // Add computation time to the history
                computationTimeHistory.Add(new DataPoint2d
                {
                    X = computationTimeHistory.Count + 1,
                    Y = computationTime
                });
            });
        }
 
        private SimpleCommand? plotResultsCommand;
        public SimpleCommand PlotResultsCommand
        {
            get => plotResultsCommand ??= new SimpleCommand((object? parameter) =>
            {
                DataPoints.Clear();
 
                // Copy computation time history to DataPoints collection
                foreach (DataPoint2d point in computationTimeHistory)
                {
                    DataPoints.Add(point);
                }
            });
        }
    }
}

MainViewModel 类派生自 BaseViewModel。然后,代码创建私有字段 computationTime 和关联属性 ComputationTime。请注意 BaseViewModel 中的 SetProperty 如何引发 PropertyChanged 事件来通知视图 ComputationTime 属性中的更改,如下所示

private string computationTime = "";
 
public string ComputationTime
{
    get => computationTime;
    set => SetProperty(ref computationTime, value);
}

通过这种方法,您无需手动将值从 ComputationTime 属性重写到视觉控件的属性(例如 Label.Content)。相反,您使用数据绑定。视图会自动收到源属性更改的通知。您使用 RunCalculationsCommand 命令在视图模型中设置 ComputationTime。这没有显式引用视图,从而有效地将逻辑与其解耦。

使用 SimpleCommand 类的构造函数创建 RunCalculationsCommand,如下所示。构造函数使用 C# 的 lambda 表达式内联定义操作。

private SimpleCommand? runCalculationsCommand;
public SimpleCommand RunCalculationsCommand
{
    get => runCalculationsCommand ??= new SimpleCommand((object? parameter) =>
    {
        var computationTime = PerformanceHelper.MeasurePerformance(
            MatrixHelper.SquareMatrixMultiplication, executionCount: 2);
 
        ComputationTime = string.Format($"Platform: " +
                $"{Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}\n" +
                $"Computation time: {computationTime:f2} ms");
 
        // Add computation time to the history
        computationTimeHistory.Add(new DataPoint2d
        {
            X = computationTimeHistory.Count + 1,
            Y = computationTime
         });
    });
}

action 首先调用 PerformanceHelper.MeasurePerformance 来测量两次调用 MatrixHelper.SquareMatrixMultiplication 方法的执行时间。然后,它将结果计算时间(以毫秒为单位)存储在 computationTime 局部变量中。后者创建一个字符串,其中包含处理器体系结构(使用 PROCESSOR_ARCHITECTURE 环境变量)和实际计算时间,并带有 ms 后缀。生成的字符串成为 MainViewModelComputationTime 属性。

RunCalculationsCommand 还将计算时间存储在 computationTimeHistory 字段中,该字段是 DataPoint2D 类的实例列表。由于 DataPoint2D 有两个属性 — XY — 您提供 XY 值以将计算时间添加到历史记录中。X 是一个整数,其值设置为 computationTimeHistory 集合中的元素数量加一。对于 Y 值,您提供 computationTime

要绘制计算时间,请使用 PlotResultsCommand 方法,如下所示

public ObservableCollection<DataPoint2d> DataPoints { get; set; } =
    new ObservableCollection<DataPoint2d>();
 
public SimpleCommand PlotResultsCommand
{
    get => plotResultsCommand ??= new SimpleCommand((object? parameter) =>
    {
        DataPoints.Clear();
 
        // Copy computation time history to DataPoints collection
        foreach (DataPoint2d point in computationTimeHistory)
        {
            DataPoints.Add(point);
        }
    });
}

代码不调用任何图表相关逻辑。它只是将 computationTimeHistory 中的每个元素复制到 DataPoints 属性,该属性是 ObservableCollection 类型。后者是一个特殊的集合,它实现了 INotifyPropertyChanged 接口,并在向 ObservableCollection 添加、更新或删除新项时引发该接口。因此,您无需在此处使用 BaseViewModel.SetProperty 方法。但是,要在添加、更新或删除新项时 更新 ObservableCollection,您可能仍需要实现项的 INotifyPropertyChanged 接口。

将 ViewModel 附加到 View

现在您已经将 MainViewModel 连接到 MainWindow 视图,请打开 MainWindow.xaml 文件。添加粗体显示的声明

<Window x:Class="Arm64.DesktopApp.WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Arm64.DesktopApp.WPF"
        xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
        mc:Ignorable="d"
        Title="Arm"
        Height="450"
        Width="500"
        xmlns:viewModels="clr-namespace:Arm64.DesktopApp.WPF.ViewModels">
 
    <Window.DataContext>
        <viewModels:MainViewModel />
    </Window.DataContext>
    
    <!--Other declarations do not change-->
</Window>

xmlns:viewModels="clr-namespace:Arm64.DesktopApp.WPF.ViewModels" 声明将 C# 命名空间添加到 XAML 代码,使您能够将 C# 类型用作 XAML 标签。第二个声明块创建 MainViewModel 类的实例并将其存储在 Window.DataContext 属性中。视图使用以下属性进行数据绑定

<Window.DataContext>
    <viewModels:MainViewModel />
</Window.DataContext>

接下来,修改 Buttons 部分以添加对 RunCalculationsCommandPlotResultsCommand 的绑定

<!--Buttons-->
<StackPanel Grid.Row="0"
            Grid.Column="0"
            Orientation="Vertical">
    <Button Content="Run calculations"
            Command="{Binding RunCalculationsCommand}" />
    <Button Content="Plot results"
            Command="{Binding PlotResultsCommand}" />
</StackPanel>

之后,添加对 MainViewModelComputationTime 属性的绑定

<!--Label-->
<Label Grid.Row="0" 
       Grid.Column="1" 
       Content="{Binding ComputationTime, Mode=OneWay}" />

然后,修改图表声明,以便图表中显示的值来自 MainViewModelDataPoints 属性

<!--Chart-->
<syncfusion:SfChart Grid.Row="1"
                    Grid.ColumnSpan="2">
    <syncfusion:SfChart.PrimaryAxis>
        <syncfusion:NumericalAxis Header="Trial number"
                           FontSize="14" />
        </syncfusion:SfChart.PrimaryAxis>
 
  <syncfusion:SfChart.SecondaryAxis>
        <syncfusion:NumericalAxis Header="Computation time [ms]"
                           FontSize="14"
                           Maximum="3000"
                           Minimum="0">
        </syncfusion:NumericalAxis>
    </syncfusion:SfChart.SecondaryAxis>
 
    <syncfusion:LineSeries EnableAnimation="True"
                           ItemsSource="{Binding DataPoints, Mode=OneWay}"
                           Label="Computation time"
                           XBindingPath="X"
                           YBindingPath="Y">
    </syncfusion:LineSeries>
</syncfusion:SfChart>

讨论

现在您可以运行应用程序来体验 Arm64 相对于 x64 体系结构的性能优势。首先,为 Arm64x64 配置应用程序。在 Visual Studio 中,单击平台列表,然后选择配置管理器

配置管理器窗口中,单击活动解决方案平台下的新建

  • 类型或选择新平台: Arm64
  • 任何 CPU 复制设置。
  • 选择创建新项目平台

重复相同的过程为 x64 创建解决方案平台。

现在,将模式从调试更改为发布,并使用 x64 配置启动应用程序。

应用程序启动并显示 Syncfusion 控件的许可信息。单击取消,然后多次单击运行计算。最后,单击绘制结果生成折线图。您应该会看到类似下面图像的结果,平均计算时间为 2491.00 ms

现在,使用 Arm64 配置启动应用程序。单击运行计算几次后,单击绘制结果。您应该会看到类似下图的输出,计算时间为 894.00 ms。

Arm64 上的计算时间接近 900 毫秒。相反,在 x64 上,相同的代码需要大约 2,500 毫秒,几乎是 2.8 倍。这个结果表明在 Arm64 上原生运行 .NET 代码的优势。

结论

本教程演示了 .NET 8.0 上的 WPF 作为 Qt 框架的替代方案,尤其适用于计算密集型场景。使用 MVVM 架构设计模式,您可以实现 WPF 应用,并使用 Syncfusion NuGet 包渲染图表。

与 Qt 框架一样,.NET 利用了 Arm64 的原生功能。这个演示展示了 Arm64 在一个乘以相对较大的方阵(500 x 500)的应用程序中的显著性能提升。与在 x64 体系结构上运行应用程序相比,Arm64 将计算时间缩短了近三倍,节省了宝贵的开发时间并缩短了上市时间。

凭借这些优势,.NET 为 Qt 提供了一个绝佳的替代方案,并赋能 Arm64 开发人员。立即尝试 Windows 11 on Arm64,充分发挥您硬件的潜力,并解锁原生级别的性能。

© . All rights reserved.