dotnet 如何訪問到 UNO 框架裡面的 internal 不公開成員

lindexi發表於2024-06-12

本文和大家介紹一個 Hack 的方式,透過此方式可實現訪問 UNO 框架裡面的 internal 不公開成員,呼叫 UNO 框架裡面的不公開的 API 方法和屬性,訪問 UNO 裡面不公開的型別

核心原理是基於 UNO 框架裡面的 InternalsVisibleToAttribute 程式集特性,指定給到 SamplesApp 等程式集可見。因此只需要新建一個程式集,設定 AssemblyName 為 SamplesApp 即可

以下是我新建的名為 UnoHacker 的專案,此專案和所有的程式碼都可以在本文末尾找到下載的方法

新建的 UnoHacker 專案選定使用的是 net8.0 的框架,由於本文提供的方式強依賴於 UNO 框架的實現,本文寫於 2024.06 如果你閱讀本文距離本文編寫的時間太長,可能本文將包含不適用於你當前使用的 UNO 框架的知識

本文面向的是 5.2.161 的 UNO 框架版本,不代表後續 UNO 版本也能適用,推薦大家按照本文提供的方式自己進行測試

編輯 UnoHacker 專案的 csproj 專案檔案,先配置 AssemblyName 屬性,用於指定程式集名,如以下程式碼

  <!-- 程式集使用特殊名稱,這樣才能訪問到 internal API。 -->
  <PropertyGroup>
    <AssemblyName>SamplesApp</AssemblyName>
  </PropertyGroup>

接下來新增對 Uno.WinUI 的引用,新增此引用只是為了拿到實際的 UNO 引用程式集而已,而不是真的需要引用此包。這也就是為什麼以下程式碼需要新增不使用任何內容的 ExcludeAssets="all" 程式碼。以下程式碼還新增了 GeneratePathProperty 屬性配置,透過此屬性配置可以用於拿到對應的包在快取資料夾的路徑,用於引用包裡面的內容

  <!-- 僅為獲取到 NuGet 目錄,不使用任何內容(包括 compile;runtime;build 等) -->
  <ItemGroup>
    <PackageReference Include="Uno.WinUI" GeneratePathProperty="true" PrivateAssets="all" ExcludeAssets="all" />
  </ItemGroup>

由於預設使用了中央包管理,以上程式碼引用可以不新增版本號。新增以上程式碼之後,即可確保本地存在 NuGet 包,且透過 GeneratePathProperty 屬性配置,可以透過 $(PKGUno_WinUI) 屬性拿到 Uno.WinUI 這個包的本地路徑

在我裝置上拿到的 $(PKGUno_WinUI) 屬性的內容如下

C:\Users\lindexi\.nuget\packages\uno.winui\5.2.139

透過此即可拼接路徑,拿到 NuGet 包裡面的檔案,如以下程式碼使用了 UNO 真正在 Skia 平臺下的釋出檔案

  <!-- 引用 net8.0-desktop 真正生效的程式集,而不是專供引用的程式集。 -->
  <ItemGroup>
    <Reference Include="Uno" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.dll</HintPath>
    </Reference>
    <Reference Include="Uno.UI" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.UI.dll</HintPath>
    </Reference>
    <Reference Include="Uno.UI.Composition" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.UI.Composition.dll</HintPath>
    </Reference>
  </ItemGroup>

這裡需要額外說明的是 UNO 讓大家寫程式碼引用的程式集,和釋出後最終輸出的 DLL 程式集不是相同的檔案。讓大家編寫程式碼使用的是 NuGet 包裡面 Lib 資料夾下的,而實際釋出輸出的是 DLL 是在 uno-runtime 下的。透過使用不同的 DLL 即可讓 UNO 更好的支援多個不同的平臺,對於不同的平臺可使用不同的 DLL 輸出

完成以上程式碼之後的 csproj 專案檔案的程式碼大概如下

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <OutputType>Library</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <DefineConstants>$(DefineConstants);HAS_UNO</DefineConstants>

    <!--
      UnoFeatures let's you quickly add and manage implicit package references based on the features you want to use.
      https://aka.platform.uno/singleproject-features
    -->
    <!--
    <UnoFeatures></UnoFeatures>
    -->
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="SkiaSharp" />
  </ItemGroup>

  <!-- ================ !!! UNO HACKER !!! ================ -->

  <!-- 程式集使用特殊名稱,這樣才能訪問到 internal API。 -->
  <PropertyGroup>
    <AssemblyName>SamplesApp</AssemblyName>
  </PropertyGroup>

  <!-- 僅為獲取到 NuGet 目錄,不使用任何內容(包括 compile;runtime;build 等) -->
  <ItemGroup>
    <PackageReference Include="Uno.WinUI" GeneratePathProperty="true" PrivateAssets="all" ExcludeAssets="all" />
  </ItemGroup>

  <!-- 引用 net8.0-desktop 真正生效的程式集,而不是專供引用的程式集。 -->
  <ItemGroup>
    <Reference Include="Uno" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.dll</HintPath>
    </Reference>
    <Reference Include="Uno.UI" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.UI.dll</HintPath>
    </Reference>
    <Reference Include="Uno.UI.Composition" Private="False">
      <HintPath>$(PKGUno_WinUI)\uno-runtime\net8.0\skia\Uno.UI.Composition.dll</HintPath>
    </Reference>
  </ItemGroup>

  <!-- ================ !!! UNO HACKER !!! ================ -->
</Project>

嘗試編寫程式碼測試訪問 UNO 裡面的不公開的成員

using Windows.UI.ViewManagement;
using Microsoft.UI.Windowing;
#if HAS_UNO
using Windows.UI;
using Uno.UI.Xaml.Core;
#endif

namespace UnoHacker;

public static class ApplicationViewExtension
{
#if HAS_UNO
    public static ApplicationView GetApplicationView(this AppWindow appWindow) =>
        ApplicationView.GetForWindowId(appWindow.Id);
#endif
}

可以看到程式碼編寫非常方便,且避免使用反射,其效能更高

透過此方式即可使用到一些 UNO 裡面不公開的成員,從而實現一些特定的需求。但必須說明的是 UNO 不對不公開的 API 進行穩定性承諾,大家使用的時候需要進行足夠的測試

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 5a2adba87164ab5c2de480cb4f04a8e28bb28bce

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 5a2adba87164ab5c2de480cb4f04a8e28bb28bce

獲取程式碼之後,進入 UnoDemo/UnoSkiaWeelelqairjiwarfekemGahinabai 資料夾,即可獲取到原始碼

相關文章