作者:Dean Ellis - 高级软件工程师
排版:Alan Wang
我们在 .NET 9 中为 .NET 和 .NET MAUI Android 应用程序引入了一种生成资产包的新方法,您可以从今天就开始尝试使用。什么是资产包?为什么要使用它们?如何开始?让我们一起了解一下吧!
什么是资产包?
早在 2018 年,Google 就推出了一种新的软件包格式,用于将 Android 应用程序部署到 Google Play Store。自 .NET 6 以来,.NET Android 就一直支持Android App Bundles (AAB)这种新格式。Android App Bundles 有很多功能,我们优先考虑了开发人员最需要的功能。AAB 格式的一项高级功能是资产包,它现在将成为 .NET 9 的一部分。那么什么是资产包呢?
新软件包格式的一部分是可以将资产放入单独的软件包中。开发者可以上传通常比 Google Play 允许的基本软件包大小更大的游戏和应用。通过将这些资产放入单独的软件包中,开发者可以上传最多2 GB大小的软件包。基本软件包大小为 200 MB。有一个条件,资产包只能包含资产。对于 .NET Android,这意味着具有 AndroidAsset 构建操作的项目。
资产包可以有不同的交付选项。这决定了您的资产将在何时安装在设备上。安装时交付(Install Time)的资产包与应用程序同时安装。这种类型的包最大可达 1 GB,但您只能拥有其中一种。快速跟随(Fast Follow)类型的包将在应用程序安装完成后不久的某个时间点安装。应用程序将能够在安装此类包时启动,因此开发人员在尝试使用资产之前应检查它是否已完成安装。这种资产包最大可达 512 MB。最后一种类型是按需交付(On Demand)类型。除非应用程序特别要求,否则这些资产包永远不会下载到设备上。如上所述,所有资产包的总大小不能超过 2 GB,并且您最多可以拥有 50 个单独的资产包。
如果您的应用程序包含大量资产,例如游戏,您会发现这些类型的包非常有用。音乐、电影或纹理等资产会占用应用程序包中的大量空间。如果能够将它们拆分出来,您可以更自由地将基于代码的功能放入实际游戏或应用程序中。
如果您想了解更多信息,您可以阅读 https://developer.android.com/guide/playcore/asset-delivery 上的 Asset Delivery。
问题:构建资产包
使用 .NET 8 版本的 .NET Android,.NET 构建系统不支持构建资产包。有一个名为"AndroidAppBundleModules"的MSBuild ItemGroup,可以将其他 zip 文件添加到 aab 包。然而,用户仍然需要想办法使用 gradle 或 aapt2 手动构建资产包。有些社区提供的技巧可以使用,但如果能够直接在 .NET Android 中使用此功能就更好了。这些技巧中的大多数都需要创建一个单独的项目,并在解决方案中添加自定义构建目标,以构建相应的zip文件。这使得操作起来有点不便。
解决方案:AssetPack 元数据
我们花了很长时间思考如何最好地实现资产包支持。理想情况下,我们希望不仅 .NET Android 开发人员可以轻松创建资产包,还允许 .NET Maui 开发人员使用它。
我们决定使用 MSBuild 元数据来控制资产包的创建。无需创建任何额外的项目或处理自定义目标。只需为AndroidAsset项目添加一些简单的元数据即可让用户定义资产包。
以下是示例项目中的文件
csharp
MyProject.csproj
MainActivity.cs
Assets/
MyLargeAsset.mp4
MySmallAsset.json
AndroidManifest.xml
文件 MyLargeAsset.mp4 和 MySmallAsset.json 都会在发布应用程序时被打包到主 .aab 文件中。假设 MyLargeAsset.mp4 超过了 200 MB的限制,因此将其放在主应用程序包中是不可行的。所以我们想要做的是将此资产放入一个Install Time资产包中。这样我们就可以确保该资产在应用程序在设备上安装时就已就位。
通过新系统,我们可以利用为 AndroidAsset ItemGroup 支持的两个新元数据属性。
第一个 Metadata 属性是 AssetPack。如果存在该属性,它将控制资产最终将被放入哪个资产包中。如果不存在,则资产将默认被放入主应用包中。AssetPack 名称将采用 $(AndroidPackage).%(AssetPack) 的形式,因此,如果您的项目包名称是 com.foo.myproject,并且 AssetPack 值为 bar,则资产包将为 com.foo.myproject.bar。AssetPack 有一个特殊值base。它可以用于确保某些资产最终被放入主应用程序包而不是资产包。关于这一点,稍后将详细介绍。另一个是 DeliveryType。如果定义了这个属性,您可以使用它来控制创建的资产包类型。此值的有效值为 InstallTime、FastFollow 和 OnDemand。如果不存在,默认值为 InstallTime。
因此,在我们的示例中,如果我们想将 MyLargeAsset.mp4 资产移至其自己的资产包中,我们可以在 MyProject.csproj 中使用以下内容。请注意,我们使用 Update 而不是 include,因为 .NET Android 支持资产的自动导入。
csharp
<ItemGroup>
<AndroidAsset Update="Assets/MyLargeAsset.mp4" AssetPack="myassets" />
</ItemGroup>
这将导致 .NET Android 构建系统创建一个名为 com.foo.myproject.myassets 的新资产包,并将 MyLargeAsset.mp4 包含在该包中。此资产包将自动包含在最终的 .aab 文件中。根本不需要手动创建此包。由于 DeliveryType 的默认值默认为 InstallTime,因此在这种情况下不需要额外的操作。
更完整的示例
现在可能存在一些需要控制哪些资产放入包、哪些不放入包的用例。但您也有数百个资产。在这种情况下,您可能希望使用通配符来更新自动导入的项目。
csharp
<ItemGroup>
<AndroidAsset Update="Assets/*" AssetPack="myassets" DeliveryType="FastFollow" />
</ItemGroup>
上面的代码片段将使所有从 Assets 文件夹导入的资源都进入 myassets 资源包,且该资源包的交付类型为 FastFollow(快速跟随)。但是,如果您有一两个需要放入主程序包中的资产,因为资产包可能在需要时未被安装,我们需要一种方法来实现这一点。
这就是前面提到的 AssetPack base 值的由来。
csharp
<ItemGroup>
<AndroidAsset Update="Assets/*" AssetPack="myassets" DeliveryType="FastFollow" />
<AndroidAsset Update="Assets/myimportantfile.json" AssetPack="base" />
</ItemGroup>
在上述更新的示例中,myimportantfile.json 现在将出现在主应用程序包中,而不是 myassets 资产包中。这是一种控制哪些特定资产最终出现在特定位置的好方法。
检查 FastFollow 资产包的状态
如果您使用的是基于 FastFollow 的资产包,则需要在尝试访问其内容之前检查它是否已安装。首先将 PackageReference 添加到 Xamarin.Google.Android.Play.Asset.Delivery NuGet 包。
csharp
<ItemGroup>
<PackageReference Include="Xamarin.Google.Android.Play.Asset.Delivery" Version="2.0.5.0" />
</ItemGroup>
这将引入与资产包配合使用所需的 API。
接下来,我们需要使用 Google.Play.Core.Assets.AssetPackManager 来查询 FastFollow 资产包的位置。我们可以使用 GetPackLocation 方法来执行此操作。如果它对返回的 AssetPackLocation 上的 AssetsPath 方法返回 null,则表示该包尚未安装。如果它返回任何其他内容,则表示包的安装位置。
csharp
var assetPackManager = AssetPackManagerFactory.GetInstance (this);
AssetPackLocation assetPackPath = assetPackManager.GetPackLocation("myfastfollowpack");
string assetsFolderPath = assetPackPath?.AssetsPath() ?? null;
if (assetsFolderPath is null) {
// FastFollow Pack 未安装。
}
下载 OnDemand 资产包
如果您想使用 OnDemand 包,则需要手动下载这些包。为此,您必须使用 Google.Play.Core.Assets.IAssetPackManager。为了监控下载进度,您需要使用 AssetPackStateUpdateListener 类型。但是,由于此类型是 Java 通用类型,因此您不能直接使用它。为了解决这个问题,.NET Binding 提供了一个 AssetPackStateUpdateListenerWrapper 类,可用于连接事件来监控进度。
首先我们需要声明我们需要的字段。
csharp
// 这是底层的 IAssetPackManager
IAssetPackManager assetPackManager;
//这是 AssetPackStateUpdateListener 的包装器,它允许我们提供事件来从 IAssetPackManager 获取更新。
AssetPackStateUpdateListenerWrapper listener;
接下来,我们声明要用来监控下载的委托。请注意,AssetPackStateEventArgs 是 AssetPackStateUpdateListenerWrapper 的嵌套类。您可以在 Android 文档中了解可以使用哪些 AssetPackStatus 值。
csharp
void Listener_StateUpdate(object sender, AssetPackStateUpdateListenerWrapper.AssetPackStateEventArgs e)
{
var status = e.State.Status();
switch (status)
{
case AssetPackStatus.Downloading:
long downloaded = e.State.BytesDownloaded();
long totalSize = e.State.TotalBytesToDownload ();
double percent = 100.0 * downloaded / totalSize;
Android.Util.Log.Info ("Listener_StateUpdate", $"Downloading {percent}");
break;
case AssetPackStatus.Completed:
break;
case AssetPackStatus.WaitingForWifi:
assetPackManager.ShowCellularDataConfirmation (this);
break;
}
}
接下来要做的是通过 AssetPackManagerFactory.GetInstance 方法创建一个IAssetPackManager 实例。然后我们需要创建一个AssetPackStateUpdateListenerWrapper 实例并连接 StateUpdate 委托。
csharp
assetPackManager = AssetPackManagerFactory.GetInstance (this);
// 创建我们的包装器并设置事件处理程序。
listener = new AssetPackStateUpdateListenerWrapper();
listener.StateUpdate += Listener_StateUpdate;
当应用程序恢复或暂停时,我们需要确保使用 assetPackManager RegisterListener 和 UnregisterListener 监听器,以确保能收到进度更新。
csharp
protected override void OnResume()
{
// 使用 SplitInstallManager 注册我们的 Listener Wrapper 以便我们得到反馈。
assetPackManager.RegisterListener(listener.Listener);
base.OnResume();
}
protected override void OnPause()
{
assetPackManager.UnregisterListener(listener.Listener);
base.OnPause();
}
在下载 OnDemand 资产包之前,我们需要先检查它是否已安装。与 FastFollow 资源包类似,如果 AssetsPath() 返回 null,则说明资源包尚未安装。您可以调用 assetPackManager.Fetch 来开始下载。您还可以使用 C# 扩展方法 AsAsync,它返回一个可以 await 的 Task。此扩展方法在 Android.Gms.Extensions 命名空间中可用。
csharp
// 尝试安装新功能。
var assetPackPath = assetPackManager.GetPackLocation ("myondemandpack");
string assetsFolderPath = assetPackPath?.AssetsPath() ?? null;
if (assetsFolderPath is null) {
await assetPackManager.Fetch(new string[] { "myondemandpack" }).AsAsync<AssetPackStates>();
}
从此时起,我们可以通过之前设置的 AssetPackStateUpdateListenerWrapper 来监控下载状态和进度。
调试和测试
若要在本地测试您的资产包,您需要确保正在使用 aab格式 AndroidPackageFormat。默认情况下,.NET Android 将使用 apk 进行调试。如果 AndroidPackageFormat 保留为 apk,则所有资产将像平常一样打包到 apk 中。
在您的csproj 中的设置以下选项,将启用调试资产包所需的设置。
csharp
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AndroidPackageFormat>aab</AndroidPackageFormat>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidBundleToolExtraArgs>--local-testing</AndroidBundleToolExtraArgs>
</PropertyGroup>
--local-testing 标志是本地设备上测试所必需的。它告诉 bundletool 应用程序,所有资产包都应安装在设备上的缓存位置。它还设置 IAssetPackManager 以使用将使用缓存的模拟下载器。这允许您在调试环境中测试安装 OnDemand 和 FastFollow 资产包。
我可以在我的 Maui 应用程序中使用它吗?
是的,当然!在为 Android 构建 Maui 应用程序时,您使用的是 .NET Android SDK。因此,Maui 开发人员可以使用 .NET Android 用户可用的所有功能。Maui 确实有自己的定义资产的方式,即通过 MauiAsset 构建操作。向 MauiAsset 项目添加额外的 AssetPack 和 DeliveryType 元数据将产生相同的结果。其他平台将忽略额外的元数据。
csharp
<MauiAsset
Include="Resources\Raw\**"
LogicalName="%(RecursiveDir)%(Filename)%(Extension)"
AssetPack="myassetpack"
/>
如果您有想要放置在 Asset Pack 中的特定项目,则可以使用 Update 方法来定义 AssetPack 元数据。
csharp
<MauiAsset Update="Resources\Raw\MyLargeAsset.txt" AssetPack="myassetpack" />
结论
Asset Packs 提供了一种很好的方式,可以在不影响随应用程序媒体或数据量的情况下增加应用程序的大小。将部分或全部资产移入资产包,可以让您将更多的基础包大小用于代码和用户界面。请务必阅读有关 .NET 9 中 .NET MAUI 的新增功能的更多信息。