Windows 10 UWP 24 of N: Build 2017 Streaming install for UWP app

  • 250
  • 0
  • UAP
  • 2021-04-30

UWP可以支援串流安裝啊!

今年MSFT在Build上介紹了Streaming install的功能,這個功能是為了降低安裝APP時大量的資源檔案需要下載所等待的時間,基本上很多遊戲都有這樣的功能!當然最佳化(全部資源下載並安裝完成)依舊需要看網路的流量狀況。

在Windows developer day中就有展示這個功能的特性,不論是原生UWP的App還是使用Desktop bridge或是iOS converter的App漸漸的App的大小都在成長!在Android以及iOS上還沒有這樣的特性所以在安裝App的時候依舊會需要較長的安裝、下載時間並且需要等完成後才能啟動App來進行App的操作。


概觀

使用Streaming install主要有個概念就是會把App分類成 Require以及 optional的類別。所以本身App的主要核心使用檔案都會是在Require的群組中~當你需要使用到那類群組的內容時在進行串流下載!

使用Streaming install的最低需求是 Windows insider preview build 15048 SDK之後的版本,如果使用的是Managed code撰寫的App只能串流"資源檔案"(如圖片、影片、音效檔...等)而非程式碼,這是因為manage code使用.Net native轉換所以這些轉換出來的程式碼將會被歸類在Require的類別內。
如果在Creator update之前的WIndows 10 中安裝使用了Streaming install的App將會使該App無法開啟!

接者要說明如何讓App內的檔案被區分為Require或是Automatic,這邊會在UWP的專案結構下使用一檔案名為 " AppxContentGroupMap.xml " ,這個檔案就是來標記那些檔案分別屬於那些類別。這個XML的範本格式如下

<?xml version="1.0" encoding="utf-8" ?>
<ContentGroupMap xmlns="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap" xmlns:s="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap">
  <Required>
    <ContentGroup Name="Required">
      <File Name="*"/>
      <File Name="WinMetadata\*"/>
      <File Name="Properties\*"/>
      <File Name="Assets\*scale*"/>
      <File Name="Assets\*Logo*"/>
    </ContentGroup>
  </Required>
  <Automatic>
    <ContentGroup Name="Group1">
      <File Name="Assets\Fish1.png"/>
      <File Name="Assets\Fish2.png"/>
    </ContentGroup>

    <ContentGroup Name="Group2">
      <File Name="Assets\Folder1\Pig1.png"/>
      <File Name="Assets\Folder1\Pig2.png"/>
    </ContentGroup>

    <ContentGroup Name="Group3">
      <File Name="Assets\Folder2\Rat.png"/>
    </ContentGroup>

  </Automatic>
</ContentGroupMap>

這邊就會看到區分為兩大類的ContentGroup,只要是ContentGroup的名稱設定成Required的就會被歸類在必要安裝的資源群組中!在ContentGroup的Node中可以放入多個File的Node;而在File的屬性會有個Name就是代表檔案的路徑。這時候就會出現一個問題....如果檔案有幾百個難道需要手動修改這個XML嗎?其實並不需要(當然也可以自己手動加入)這時候就會出現另外一個檔案來輔助這個情況這個檔案將會必須命名為"  SourceAppxContentGroupMap.xml "使用SourceAppxContentGroup.xml的方法非常簡單!而且在基本的Schema跟 AppxContentGroup.xml是完全一樣的,但是還支援了檔案的篩選語法!使用 * (星號)的方式來使用檔案篩選的。所以上面使用到的XML就可以修改如下

<?xml version="1.0" encoding="utf-8" ?>
<ContentGroupMap xmlns="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap" xmlns:s="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap">
  <Required>
    <ContentGroup Name="Required">
      <File Name="*"/>
      <File Name="WinMetadata\*"/>
      <File Name="Properties\*"/>
      <File Name="Assets\*scale*"/>
      <File Name="Assets\*Logo*"/>
    </ContentGroup>
  </Required>
  <Automatic>
    <ContentGroup Name="Group1">
      <File Name="Assets\*Fish*"/>
    </ContentGroup>

    <ContentGroup Name="Group2">
      <File Name="Assets\Folder1\*"/>
    </ContentGroup>

    <ContentGroup Name="Group3">
      <File Name="Assets\Folder2\*Rat*"/>
    </ContentGroup>

  </Automatic>
</ContentGroupMap>
Group1就可以使用 *Fish*的方式找到在Assets下的檔案的名稱有包含Fish的所有檔案。
Group2使用的就是所有在Folder1下的所有檔案。
Group3就是把上面兩個規則合併使用。

Map檔案的使用限制

接者這邊還要說到ContentGroupMap的限制不論是在AppxContentGroupMap或是SourceAppxContentGroupMap上都適用。

  1. 檔案路徑都是對應到App package的根目錄
  2. 設定檔案(AppxBlockMap.xml, AppxContentGroupMapxml, AppxManifest.xml, AppxSignature.p7x, CodeIdetegrity.cat, resources.pri)不須加入,系統會自動幫這些檔案歸類在Reqired。
  3. SplashScreen和Suqare44x44Logo的圖片檔案已經被宣告在manifest中。
  4. 同一檔案可以被歸類在不同的ContentGroup中
  5. 所有在Map中的檔案都必須是在封裝檔中。
  6. 可以有多組ContentGroup。
  7. ContentGroup的名稱是獨一無二的而且具有大小寫區分。
  8. 在automatice下的ContentGroup不能被命名為Required。

而在AppxContentGroupMap.xml還有附加一下限制

  1. 沒有*(星號)模式的支援
  2. 不能有Required的Node。

而在SourceAppxContentGroupMap.xml還有附加以下限制

  1. Required的Node下的ContentGroup只能有一個。
  2. Required下的ContentGroup必須命名為Required。
  3. 檔案無法同時在Required以及automatic的ContentGroup內。
  4. Visual assets(視覺資產)是必須定義在required的ContentGroup內。
  5. 所有檔案在package中必須出現在map中一次,也就是所有檔案在封裝檔中至少會被對應到一次。
  6. * (星號)的模式支援。

在SourceAppxContentGroupMap.xml的 * (星號)規則如下

CGM Entry          File Layout (relative to project root) Result
* Assets\…
Extra.content\…
System.Core.dll
System.Runtime.dll
Microsoft.CSharp.dll
App.exe
Succeed conversion with System.Core.dll, System.Runtime.dll, Microsoft.CSharp.dll, and App.exe in the content group
** N/A Fail conversion, cannot double wildcard at the root of the project to include every file in the project.
*CSharp* Assets\…
Extra.content\…
System.CSharp\…
System.Core.dll
System.Runtime.dll
Microsoft.CSharp.dll
RandomCSharpfile.txt
App.exe
Succeed conversion with Microsoft.CSharp.dll and RandomCSharpfile.txt in the content group
Assets\*Level2* Assets\Level2title.png
Level2StartMusic.mp3
Succeed conversion with Assets\Level2title.png in the content group
Assets\* Assets\Hammer.png
Assets\Items\Gun.png
App.exe
Succeed conversion with Hammer.png in the content group
Assets\** Assets\Hammer.png
Assets\Items\Gun.png
App.exe
Succeed conversion with Hammer.png and Gun.png in the content group
*.dll Assets\…
Something.dll\…
System.Core.dll
System.Runtime.dll
App.exe
Succeed conversion with System.Core.dll and System.Runtime.dll in the content group
*.dll Assets\…
App.exe
Conversion succeeds but warning will appear alerting that no file matched the wildcard specification
System.*.dll Assets\…
System.something.something.dll\…
System.Core.dll
System.Runtime.dll
Microsoft.CSharp.dll
App.exe
Succeed conversion with System.Core.dll and System.Runtime.dll in the content group
System.Core.dll* System.Core.dll Succeed conversion with System.Core.dll in the content group

實際Demo

Demo的操作環境如下

  • Visual Studio 2017 lastest update with 15063 SDK
  • Windows 10 Creator Update lastest build


直接建立一Blank專案,並且調整Mininal和Target的版本為15063 Creator Update,然後DEMO專案的檔案結構如下圖所示

這邊我在Assets下加入Folder名為Companies、Landmarks、Starts

然後再SourceAppxContentGroupMap.xml修改成如下XML code

<?xml version="1.0" encoding="utf-8" ?>
<ContentGroupMap xmlns="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap" xmlns:s="http://schemas.microsoft.com/appx/2016/sourcecontentgroupmap">
  <Required>
    <ContentGroup Name="Required">
      <File Name="*"/>
      <File Name="WinMetadata\*"/>
      <File Name="Properties\*"/>
      <File Name="Assets\*scale*"/>
      <File Name="Assets\*Logo*"/>
      <!-- Company logos as required -->
      <File Name="Assets\Companies\*"/>
    </ContentGroup>
  </Required>
  <Automatic>
    <ContentGroup Name="Landmarks">
      <File Name="Assets\Landmarks\*"/>
    </ContentGroup>
    <ContentGroup Name="Starts">
      <File Name="Assets\Starts\*"/>
    </ContentGroup>
  </Automatic>
</ContentGroupMap>

這邊我只有定一個automatic content group。然後再把這個XML的建置屬性調整成 " AppxSourceContentGroupMap "。

然後再對專案的市集選項中的轉換選項,如下圖所示

轉換成功後會直接產生AppxContentGroupMap.xml的檔案!

這樣就是在專案設定上支援Streaming install了~~接下來來看看更加詳細的資訊。我們直接把App打包起來然後變更成Zip壓縮檔後再解壓縮看看檔案結構

這邊會發現定義到automatice content group的檔案還是會被封裝進去App!

接者來看看在C# Code當中如何使用Streaming install的功能八

public StreamingInstallHelper()
        {
            if (!Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3, 0))
            {
                throw new NotSupportedException("This helper need 14393 or later");
            }

            catalog = PackageCatalog.OpenForCurrentPackage();

            catalog.PackageContentGroupStaging += Catalog_PackageContentGroupStaging;
            catalog.PackageStaging += Catalog_PackageStaging;
            catalog.PackageUpdating += Catalog_PackageUpdating;
            catalog.PackageInstalling += Catalog_PackageInstalling;
            catalog.PackageUninstalling += Catalog_PackageUninstalling;
            catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
        }

這邊需要先Check是否有Anniversary update的Windows 10才能先抓取到Package的詳細資訊。 在PackageCatalog這個Class裡面有以下幾個重點的event

事件名稱 說明
PackageContentGroupStaging 當Package中的ContentGroup正在部屬。
PackageStaging 當Optional Package正在部屬。
PackageUpdating 當Optional Package正在更新。
PackageInstalling 當Optional Package正在安裝。
PackageUninstalling 當Optional Package正在解除安裝。
PackageStatusChanged 當Optional Package狀態改變。

這邊會使用到的是PackageContentGroup的事件、而抓取Streaming install的安裝的進度可以參考如下的Code

private void Catalog_PackageContentGroupStaging(PackageCatalog sender, PackageContentGroupStagingEventArgs args)
        {
            var message = $"{args.ContentGroupName}\t{args.Progress*100}";
            System.Diagnostics.Debug.WriteLine(message);
        }

取得App package中所有的ContentGroup的Sample Code如下

public async Task<IEnumerable<PackageContentGroup>> GetAllContentGroupAsync(bool isIncludeRequiredGroup = false)
        {
            if (!Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 4, 0))
            {
                throw new NotSupportedException("This function need 15063 or later");
            }

            var contentGroups = await Package.Current.GetContentGroupsAsync();
            if (isIncludeRequiredGroup)
            {
                return contentGroups;
            }
            else
            {
                return contentGroups.Where(cg => cg.IsRequired.Equals(isIncludeRequiredGroup));
            }
        }

透過Package來抓取當下既有的Content,並且可以透過Linq來塞選ContentGroup~這邊我選擇塞選是否為Require或是Automatic的ContentGroup。

然後如果要優先下載ContentGroup(s)的Sample如下

public async Task PrioritizeCotentGroupAsync(IEnumerable<string> groupNames)
        {
            if (!Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 4, 0))
            {
                throw new NotSupportedException("This function need 15063 or later");
            }

            if (groupNames == null)
            {
                throw new ArgumentNullException(nameof(groupNames));
            }

            await Package.Current.StageContentGroupsAsync(groupNames, true);
        }

透過StageContentGroupAsync並且帶上true 讓這些contentgroup name的列舉提升到柱列的最前端。

如果發生中斷的情況(使用者可以在Store中暫停安裝或是網路中斷...等)的情況可以使用StageContentGroupAsync傳入任一個ContentGroup的Name讓Streaming install繼續下載並安裝!如果只是出現短暫的錯誤(網路不穩定)的話將會由系統自動處理後續下載安裝行為。 但是如果發生如上述的狀況就會讓優先下載的功能被取消,需要再次透過StageContentGroupAsync配上true的API重新讓特定的ContentGroup進行優先下載安裝流程。

接者來說明如何Debug和Test streaming install的功能八

目前MSFT提供兩個方式來做Deubg\Test

1. Powershell

2. Debugging API

在UWP的安裝會是使用Powershell的方式,預設的指令如下

Add-AppxPackage <package>

基本上這個指令和double click一樣喔。在15063之後的支援了RequiredContentGroupOnly的指令,這個語法會讓Streaming install的UWP app只安裝Require的ContentGroup。PowserShell指令會變成如下

Add-AppxPackage -RequiredContentGroupOnly <package>

實際操作畫面

透過指令安裝Required的ContentGroup只會有Companies的選項在左邊的選項。

透過ForceApplicationShutdown 的語法讓Appx取得Automatic 的ContentGroup,這邊有趣的是需要下兩次的Powershell指令才會讓App取得完整的Automatic contentgroups!只要下ForeceApplicationShutdown就會強制UWP的APP重新開啟但是只進行一次的指令並不會讓Automatic ContentGroup做刷新的動作,目前不知道是否為Bug。

上述有提到可以使用Debugg API,這邊可以直接使用MSFT 提供的Github link ( https://github.com/AppInstaller/StreamingInstallDebuggingApp )抓下來後直接使用或是可以自己寫一隻拉~需要使用Debugging package api的流程如下

在package.appxmanifest加入 restrict capabilities的宣告如下所示

<?xml version="1.0" encoding="utf-8"?>

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  IgnorableNamespaces="rescap uap mp">

<中間略過不需修改>

  <Capabilities>
    <Capability Name="internetClient" />

    <rescap:Capability Name="packageManagement"/>
  </Capabilities>
</Package>

然後就可以透過PackageManager的Class來做Debug的動作!如下的C# sample code就可以設定ContentGroup的狀態。

public async Task SetContentGroupStateAsync(string groupName, PackageContentGroupState state, double percentage)
        {
            if (!Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 4, 0))
            {
                throw new NotSupportedException("This helper need 14393 or later");
            }

            if (string.IsNullOrEmpty(groupName) || string.IsNullOrWhiteSpace(groupName))
            {
                throw new ArgumentNullException(nameof(groupName));
            }

            if (percentage < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(percentage));
            }

            try
            {
                var pm = new PackageManager();
                await pm.DebugSettings.SetContentGroupStateAsync(Package.Current, groupName, state, percentage);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }
這邊除了需要注意是否有在Manifest中宣告restrict capabilites以及是否是15063的SDK,PackageManager是從10240就開始支援的Class,所以配合ApiInformation來判斷可以避免一些版本檢查問題。

 

 


總結

Streaming install可以讓App的檔案資源較為有效率的調整且讓使用者優先體驗App主要的功能,其餘資源可以後續下載並安裝。

 

***以上Code以及說明都有可能隨著Windows 10 的版本以及Visual Studio 版本有所調整!***

參考資料 MSDN, UWP Windows app deployment, B8093_Building extensible, stream-able, componentized apps

下次再分享Windows 10 的新技術拉~