NET、大規模アプリケーション向けのApplication StartupManager起動プロセスフレームワークにアクセス

NET、大規模アプリケーション向けのApplication StartupManager起動プロセスフレームワークにアクセス

ユーザーがデスクトップアイコンをダブルクリックしたとしますが、アプリケーションが起動するまで数分待ってから、ユーザーの次のステップはアンインストールをクリックすることです。起動プロセスにおける大規模なアプリケーションのバランスを取るためには、複雑な起動ロジックを実行する必要があり、起動パフォーマンスに注意を払う必要があり、このプロセスのためのフレームワークを作成することは完全に合理的なことです。

最后更新 2022/04/10 8:38
林德熙
预计阅读 20 分钟
分类
.NET
标签
.NET C#

对于大型的应用软件,特别是客户端应用软件,应用启动过程中,需要执行大量的逻辑,包括各个模块的初始化和注册等等逻辑。大型应用软件的启动过程都是非常复杂的,而客户端应用软件是对应用的启动性能有所要求的,不同于服务端的应用软件。设想,用户双击了桌面图标,然而等待几分钟,应用才启动完毕,那用户下一步会不会就是点击卸载了。为了权衡大型应用软件在启动过程,既需要执行复杂的启动逻辑,又需要关注启动性能,为此过程造一个框架是一个完全合理的事情。我所在的团队为启动过程造的库,就是本文将要和大家介绍我所在团队开源的 dotnetCampus.ApplicationStartupManager 启动流程框架的库

背景は

このライブラリの起源は、Visual Studioチームの話を聞いたことでした。彼のチームは、Visual Studioの起動パフォーマンスを最適化するために、アプリケーションの起動時にCPUとメモリとディスクをフルにするという興味深い方向性を開発したと言いました。もちろん、これは冗談であり、本来の意味は、Visual Studioアプリケーションが起動するときにコンピュータのパフォーマンスを十分に圧迫する必要があるということです。たまたま、私のチームには大きなアプリケーションがたくさんあり、MergeRequestコードの数は1000以上のアプリケーションがあります。これらのアプリケーションの論理的複雑性は非常に高く、単一のスレッドでしか実行できないため、モジュール間の依存関係の複雑さによって引き起こされるピットを低減します。しかし、アプリケーションの起動性能を最適化するために、マルチスレッド方式を含むマシンパフォーマンスの圧縮戦略を考慮した。

しかし、マルチスレッドを開くと、当然多くのスレッド関連の問題が発生します。最大の問題は、各起動モジュール間の依存関係をどのように処理するかです。処理するためのより良いフレームワークがなく、開発者の個人的な能力だけに依存している場合、このリファクタリングは完全に信頼できない、またはこれが遠くない場合、おそらくこのバージョンは最適化できますが、次のバージョンはどうでしょうか。

また、個々のスタートアップ項目の時間を分析するなど、スタートアップパフォーマンスを監視する方法も非常に重要です。起動モジュールごとのパフォーマンス最適化を行う前に、起動モジュールのパフォーマンス測定を行う必要がある。興味深いことに、起動モジュールは悪魔のユーザー環境に非常に関連しています。つまり、実験室での測定結果は、実際のユーザーが使用している結果とは大きく異なります。これは、起動プロセスフレームワークに重要な要件をもたらします。それは、各起動モジュールのパフォーマンス測定と監視を容易にサポートすることです。

複数のプロジェクトがスタートアップフローフレームワークにアクセスすることを期待するため、スタートアップフローフレームワークは十分に抽象化され、単一のプロジェクトを結合する機能を持たないことが望ましい。

約1年の開発期間を経て、Launch Process Frameworkは2019 年に正式に使用される予定です。現在、1000万台近いデバイスで稼働中のスタートアップフローフレームワークのロジック

当前此启动流程框架的库在 GitHub 上,基于最友好的 MIT 协议,也就是大家可以随便用的协议进行开源,开源地址: https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager

機能性は

我所在的团队开源的 ApplicationStartupManager 启动流程框架的库提供了如下的卖点

  • スタートアップフローチャートの自動構築
  • 高パフォーマンスの非同期マルチスレッドの起動タスクアイテム実行をサポート
  • UIスレッドの自動スケジューリング論理のサポート
  • 起動タスクリソースの動的な割り当て
  • 事前コンパイル済みフレームワークのサポート
  • すべての. NETアプリケーションをサポート
  • プロセスの開始にかかる時間の監視

スタートアップフロー図

各起動タスク項目間には、論理やモジュールの初期化、サービスの登録、実行タイミングなど、明示的または暗黙的な依存関係が存在しなければなりません。開発者が依存関係を整理した後、各起動タスクアイテムの依存関係を決定し、その依存関係に基づいて起動フローチャートを構築できます。

次のような起動タスクアイテムがあり、起動タスクアイテム間に相互依存関係があると仮定します。次の図のように、矢印を使用して依存関係を示します。

  • 起動タスク項目A最初に開始される起動タスク項目(ログやコンテナの初期化起動タスク項目など)
  • スタートアップタスクアイテムB :一部の基本サービスがあるが、実行にはAスタートアップタスクアイテムに依存する。
  • タスクCの依存Bタスクの実行完了
  • スタートタスクアイテムD :別のスタンドアロンモジュールで、B C Eスタートタスクアイテムとは無関係ですが、Aスタートタスクアイテムの完了にも依存します。
  • 起動タスク項目E B Cの起動タスク項目の完了も依存する
  • 起動タスク·アイテムF起動タスク·アイテムの完了はA Dにも依存します。

上記の起動タスク項目は、各起動タスク項目に独自の先行または後続を持つことができる有向ループレス起動フローチャートを形成することができます。なぜ無循環が必要なのでしょう?2つの起動タスクアイテムが相互に依存している場合、自然に正常に起動することはできません。次の図のように、3つの起動タスクアイテムが相互に依存しています。つまり、どの起動タスクアイテムが最初に起動しても、期待に沿っていません。最初に起動した起動タスクアイテムの先行が満たされていないため、起動プロセスは論理的に先行依存があります。

スタートアップフローチャートをより良く構築するために、論理的には2つの仮想ノード、すなわちスタートポイントとエンドポイントが追加されます。スタートアップタスクアイテムに関係なく、仮想スタートアップポイントに依存し、エンドポイントに従います。

さらに、特定のビジネス当事者は、独自の関連する起動プロセス、すなわち事前設定された起動ノードを定義し、キー起動プロセスポイントは各起動項目に依存するため、手動で起動プロセスを複数の段階に分割することができます。

例えば起動過程は以下のような段階に分けられる.

  • 起動ポイント:起動フローチャートを構築するためのアプリケーション起動を表す仮想ノード。
  • インフラストラクチャ:ログの初期化、コンテナの初期化など、基盤となるサービスを開始する前に行う必要があるロジックを表します。他のスタートアップタスク項目はインフラストラクチャに依存することができ、インフラストラクチャの後に実行されるスタートアップタスク項目はインフラストラクチャが完了する準備ができていると見なされます
  • ウィンドウ起動クライアントプログラムのウィンドウが初期化される前に、UIの準備ロジック、例えばスタイルリソースや必要なデータ準備、あるいはViewModelの注入などを完了する必要がある。ウィンドウが起動すると、UI要素に対してロジックを実行したり、UI強い相関ロジックを登録したりできます。または、ウィンドウが起動した後に、メインインターフェイスが表示される前に実行する必要のない起動タスク項目を実行して、メインインターフェイスの表示パフォーマンスを向上させることもできます。
  • アプリケーションの起動:起動ロジックが完了すると、アプリケーションの起動後の起動タスク項目は、ログファイルのクリーンアップなど、アプリケーションの自動更新をトリガーするなど、ゆっくりと実行できるロジックに属します。
  • エンドポイント:アプリケーションの起動プロセスが完了したことを示す仮想ノードで、起動フローチャートを構築します。

図に示すように、各起動タスクアイテムは、特定の起動タスクアイテムに依存するか、重要な起動プロセスポイントに依存するかを選択できます。

このロジックにより、その後の最適化に備え、上位レベルのビジネス開発者がビジネスレベルのスタートアップタスクを開発することができます。上級レベルのビジネス開発者は、新しく書いた起動タスクアイテムをどこに置くべきかを明確に理解でき、各モジュールの起動タスクアイテムの依存関係をデバッグし、循環依存ロジックがあるかどうかを確認できます。

高パフォーマンス非同期マルチスレッドの起動タスクアイテムの実行

プレスマシンのパフォーマンスを向上させるには、マルチスレッドブートが必要です。スタートアップフローチャートの構築が完了すると、スタートアップタスクアイテムをツリーに描画することができ、マルチスレッドスケジューリングが容易になります。. NETベースのタスクスケジューリングは、マルチスレッドの非同期待機を実現し、マルチスレッドの状況下での複数の起動タスクアイテムの依存性を解決することができます。

スレッドプールのタスクスケジューリングを使用すると、異なる開始タスクアイテムの開始タスクチェーンを論理的に異なるスレッドに分割することができます。実際のスレッドの実行はスレッドプールスケジューリングに依存しており、実際の実行でもスレッドプールは2つの実際のスレッドを使用して実行されます。

アプリケーションの起動プロセスでは、. NETスレッドプールのスケジューリングメカニズムを理解せずにマルチスレッドを有効にすることは少し論争になります。コア論争は、アプリケーションの起動プロセスがCPUリソースでいっぱいになった場合、ユーザーのコンピュータカードが動かないようにするかどうかです。実は、上記の質問は答えが良くないので、疑いがある場合は、私の分析を聞いてください。最初のポイントは問題自体であり、最初に質問自体に質問してください。スレッドを起動するだけであれば、ユーザーのコンピュータカードも移動できませんか?答えははい、完全にユーザーのコンピュータに依存し、コンピュータの構成とコンピュータの悪魔環境、一緒にいくつかの国内のウイルス対策ソフトウェアとスラグ機器など、コンピュータの設定とコンピュータの悪魔環境を含む、その後、アプリケーションの起動時には、実行中のウイルス対策作業の多数があり、自然にカードを移動することはできません。また、コンピュータカードが動かない、CPUがいっぱいになっていることは必然的な関係ですか?答えは全くありません、アプリケーションの起動プロセスは、DLLロードプロセス、特にコールドブートプロセスのアプリケーション、多数のファイルの読み書き、一部の機械ディスクのために、ディスクの読み書きでいっぱいになり、自然にコンピュータカードを移動することができないようにすることができます、このプロセスとマルチスレッドを開くかどうか、実際には、関係は非常に小さいです。結局のところ、機械ディスクとCPUの間のパフォーマンスはここにあります。2つ目は、カードの時間が重要かどうかです。例えば、アプリケーションがマルチスレッドで500ミリ秒かかりますが、アプリケーションがシングルスレッドで起動すると4 x 500ミリ秒= 2秒かかる場合、マルチスレッドはうまくいきますか?これはトレードオフの必要性であり、異なるアプリケーションロジックは生産性ツールなど、自然に異なりますが、私はもともとVisual Studioツールでコードを書くなど、このツールを使用するために起動しましたが、私はアプリケーションを開き、プロセスは自然に他の同期要件はありません。最後の問題は、. NETマルチスレッドを開くことはCPUリソースを完全に占有することに等しくないことです。

もちろん、アプリケーションフローにアクセスできる開発者は初心者ではなく、スレッドの知識を理解していると信じて、起動タスク項目を実行する適切な方法を選択します。これはまた、このスタートアップフローフレームワークのライブラリアクセスに一定のしきい値があることを示しています。

UIスレッドの自動スケジューリング論理のサポート

クライアントアプリケーションでは、当然、特別なスレッドはUIスレッドであり、起動プロセスであり、UIスレッドで実行する必要があるロジックがたくさんあります。. NET系のアプリケーションフレームワークごとにUIスレッドのスケジューリングが異なるため,フローフレームワークを起動して一定量の適応を行う必要がある.

特定の起動タスクアイテムに現在の起動タスクアイテムをマークすると、フレームワークは自動的に起動タスクアイテムをUIスレッドにスケジュールします。

設計上、デフォルトでは起動タスクアイテムはUI以外のスレッドにスケジュールされます。

起動タスクリソースの動的な割り当て

ほとんどの場合、クライアント側での個々のタスク項目の開始時間と、開発マシンとテストマシンのラボでのテスト結果は、かなり異なります。起動タスクアイテムを固定された順序で実行すると、自然に多くの起動時間が空白の待機になります。本起動フローフレームワークライブラリは、起動プロセス中に、各起動タスク項目の時間に応じて、自動的に動的にスケジューリングすることをサポートします。

コアメソッドは、各タスクの待機ロジックをサポートする起動フローチャートを構築し、タスク待機メカニズムに基づいて、動的にスケジューリング待機ロジックを実行することができ、起動タスクアイテムを動的に配置し、コンパクトな時間で複数のスレッドを起動タスクの実行でいっぱいにすることができます。対応する上位レベルのビジネス開発者が、非同期待機などのタスクメカニズムを正しく使用すれば、起動プロセス中に大きな隠蔽を達成することができます。

事前コンパイル済みフレームワークのサポート

启动过程是属于性能敏感的部分,各个模块的启动任务项如何收集是一个很大的问题。启动部分属于性能敏感部分,不合适采用反射的机制。好在 dotnet campus 里面有技术储备,在 2018 年的时候就开源了 SourceFusion 预编译框架,后面在 2020 年时吸取了原有 SourceFusion 的挖坑经验,重新开源了 dotnetCampus.Telescope 预编译框架,新开源的 dotnetCampus.Telescope 也放在 SourceFusion 仓库中

ApplicationStartupManager 启动流程框架开发之初就考虑了对接预编译框架,通过预编译提供了无须反射即可完成启动任务项收集的能力,可以极大减少因为启动过程中反射程序集的性能损耗

プリコンパイルフレームワークにドッキングすると、クライアント側で実行する必要があるロジックに相当する時間が、開発者コンパイル時に移動し、開発者コンパイル時にクライアント側で実行する必要があるロジックを実行します。これにより、クライアント側のロジック実行時間を短縮できます。

プリコンパイル済みフレームワークにアクセスすると、開発者がコンパイルするときに、すべてのプロジェクトのスタートアップ項目(スタートアップ項目タイプ、スタートアップ項目の委任作成、スタートアップ項目の属性属性など)を収集できます。

プロセスの開始にかかる時間の監視

大規模なアプリケーションでは、ユーザー側での動作に焦点を当てることが重要です。スタートアッププロセスでは、モニタリングが非常に重要です。監視の主な意義は:

まず、ユーザーデバイス上での各起動タスクアイテムの実際の実行時間を理解し、その後のバージョンのパフォーマンス最適化をサポートするためのデータを提供します。そうでなければ、開発またはテスト側の限られたデバイスでは、真のパフォーマンスボトルネックを実行することは困難です。ユーザーのデバイス上の95行の起動分布に注意を払うだけでなく、いわゆる95行は、ユーザーの起動時間の95%の分布であるだけでなく、95行から99行のユーザーの起動分布に注意を払うこともできます。

第二に、バージョンの比較と早期警告ができます。大規模なアプリケーションでは、基本的にグレーリリースとプレリリースのメカニズムがあり、グレーリリースプロセス中に起動時間を監視することで、特定の起動タスクの時間が増加したときに開発者に通知する早期警告メカニズムをドッキングすることができます。プロジェクトの長期的な発展に役立ちます。

最後のポイントは、起動が遅い、どのステップで遅いかをユーザーに伝えることができます。このメカニズムは、オープン性を提供することに焦点を当てています。例えば、Visual Studioは、起動が遅いプラグインの原因を常に教えてくれます。

使用方法の使用

各プロジェクトのカスタマイズ要件を取り除いた後、開始プロセスフレームワークのライブラリはコアロジックのみを持ちます。つまり、使用する際には、特定のビジネス側が初期化ロジックとビジネス固有のロジックを追加する必要があります。言い換えれば、アクセス開始フローフレームワークは、単にライブラリをインストールしてAPIを呼び出すのではなく、アプリケーションのビジネスニーズに応じてドッキング作業の一部を行う必要があります。幸いなことに、スタートアップフローフレームワークは大規模なプロジェクトまたは大規模なプロジェクトにのみ適用でき、大規模なアプリケーションの他のロジックと比較して、スタートアップフローフレームワークのコード量は基本的に無視できます。小規模なプロジェクトや複数人でのコラボレーションではないプロジェクトには、当然不適切です。

整个 ApplicationStartupManager 启动流程框架设计上是高性能的,减少各个部分的性能内损。但是在上启动流程框架本身就存在一定的框架性能损耗,如果对应的只是小项目或非多人协作的项目,假设可以自己编排启动任务项,那自然自己编排启动任务项如此做是能达到性能最高的

应用 ApplicationStartupManager 启动流程框架能解决的矛盾点在于项目的复杂度加上多人协作的沟通,与启动性能之间的矛盾。接入启动流程框架可以让上层业务开发者屏蔽对启动过程细节的干扰,方便上层业务开发者根据业务需求加入启动任务项,方便启动模块维护者定位和处理启动任务项的性能

慣習により、. NETライブラリを使用する最初のステップは、NuGet経由でライブラリをインストールすることです。

NuGetでApplication StartupManagerライブラリをインストールします。プロジェクトがSDKスタイルのプロジェクトファイル形式を使用している場合は、csprojプロジェクトファイルに以下のコードを追加してインストールできます。

<ItemGroup>
    <PackageReference Include="dotnetCampus.ApplicationStartupManager" Version="0.0.1-alpha01" />
</ItemGroup>

为了方便让大家看到 ApplicationStartupManager 启动流程框架库的效果,我采用了放在 https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager 里的例子代码来作为例子

以下の3つのプロジェクトを新規に作成する

  • WPFDemo. Lib 1:基礎となる個々のコンポーネントライブラリ、特にビジネスコンポーネントを表す。
  • WPFDemo.Api:プロセスを開始するためのフレームワークロジックがデプロイされるアプリケーションのAPI層のアセンブリ。
  • WPFDemo.App:アプリケーションの最上位レベル、すなわちMain関数が存在するアセンブリで、起動ロジックがトリガーされます。

一般的な抽象化後のアプリケーションのモデルアーキテクチャは以下の通りですが、デモンストレーションのためにBusiness層とApp層を統合し、多くのLibコンポーネントをLib1プロジェクトに統合します。

新しいプロジェクトを完了し、NuGetパッケージをインストールしたら、APIレイヤーでアプリケーションに関連する起動フレームワークロジックの構築を開始できます。NuGetパッケージのインストール後にAPIが追加のロジックを必要とするのはなぜですか?各アプリケーションには独自のロジックがあり、各アプリケーションの起動タスク項目に必要なパラメータは同じではなく、各アプリケーションのロギング方法も同じではなく、アプリケーションの異なるタイプの起動ノードも同じではないため、これらはすべてアプリケーション関連のカスタマイズを行う必要があります。

関連するプリセットを適用するスタートアップノードを最初に定義する

/// <summary>
/// 包含预设的启动节点。
/// </summary>
public class StartupNodes
{
    /// <summary>
    /// 基础服务(日志、异常处理、容器、生命周期管理等)请在此节点之前启动,其他业务请在此之后启动。
    /// </summary>
    public const string Foundation = "Foundation";

    /// <summary>
    /// 需要在任何一个 Window 创建之前启动的任务请在此节点之前。
    /// 此节点之后将开始启动 UI。
    /// </summary>
    public const string CoreUI = "CoreUI";

    /// <summary>
    /// 需要在主 <see cref="Window"/> 创建之后启动的任务请在此节点之后。
    /// 此节点完成则代表主要 UI 已经初始化完毕(但不一定已显示)。
    /// </summary>
    public const string UI = "UI";

    /// <summary>
    /// 应用程序已完成启动。如果应该显示一个窗口,则此窗口已布局、渲染完毕,对用户完全可见,可开始交互。
    /// 不被其他业务依赖的模块可在此节点之后启动。
    /// </summary>
    public const string AppReady = "AppReady";

    /// <summary>
    /// 任何不关心何时启动的启动任务应该设定为在此节点之前完成。
    /// </summary>
    public const string StartupCompleted = "StartupCompleted";
}

定義が完了すると,起動プロセスは次のように分割できる.

アプリケーションビジネスに関連するログタイプを定義すると、アプリケーションによってログを記録する方法はほとんど異なり、使用される基礎となるログレコードも異なります。

/// <summary>
/// 和项目关联的日志
/// </summary>
public class StartupLogger : StartupLoggerBase
{
    public void LogInfo(string message)
    {
        Debug.WriteLine(message);
    }

    public override void ReportResult(IReadOnlyList<IStartupTaskWrapper> wrappers)
    {
        var stringBuilder = new StringBuilder();
        foreach (var keyValuePair in MilestoneDictionary)
        {
            stringBuilder.AppendLine($"{keyValuePair.Key} - [{keyValuePair.Value.threadName}] Start:{keyValuePair.Value.start} Elapsed:{keyValuePair.Value.elapsed}");
        }

        Debug.WriteLine(stringBuilder.ToString());
    }
}

如例子上的日志就是记录到 Debug.WriteLine 输出,同时日志里也添加了 LogInfo 方法

继续定制应用业务相关的启动任务项的参数,如例子代码的项目就用到了 dotnetCampus.CommandLine 提供的命令行参数解析,各个启动任务项也许会用到命令行参数,因此也就需要带入到启动任务项的参数里面,作为一个属性。例子代码的项目也用到了 dotnetCampus.Configurations 高性能配置文件库 提供的应用软件配置功能,也是各个启动任务项所需要的,放入到启动任务项的参数

適用業務に関する属性を加えた起動タスクアイテムのパラメータは以下のように定義する.

public class StartupContext : IStartupContext
{
    public StartupContext(IStartupContext startupContext, CommandLine commandLine, StartupLogger logger, FileConfigurationRepo configuration, IAppConfigurator configs)
    {
        _startupContext = startupContext;
        Logger = logger;
        Configuration = configuration;
        Configs = configs;
        CommandLine = commandLine;
        CommandLineOptions = CommandLine.As<Options>();
    }

    public StartupLogger Logger { get; }

    public CommandLine CommandLine { get; }

    public Options CommandLineOptions { get; }

    public FileConfigurationRepo Configuration { get; }

    public IAppConfigurator Configs { get; }

    public Task<string> ReadCacheAsync(string key, string @default = "")
    {
        return Configuration.TryReadAsync(key, @default);
    }

    private readonly IStartupContext _startupContext;
    public Task WaitStartupTaskAsync(string startupKey)
    {
        return _startupContext.WaitStartupTaskAsync(startupKey);
    }
}

为了继续承接 WaitStartupTaskAsync 的功能,于是构造函数依然带上 IStartupContext 用于获取框架里默认提供的启动任务项的参数。上面代码的 ConfigurationConfigs 两个属性都是 dotnetCampus.Configurations 高性能配置文件库 提供的功能,可以使用 COIN 格式进行配置文件的读写

スタートアップタスクアイテムのパラメータ定義が完了したら、特定のアプリケーションのスタートアップタスクアイテムのベースタイプをカスタマイズできます。起動タスクアイテムの基本型は起動タスクアイテムのパラメータに関連している必要があり、起動タスクアイテムのパラメータはアプリケーションごとに異なるため、起動タスクアイテムの基本型も異なります。レベルが異なる場合でも、タスクアイテムを起動するパラメータだけで、コードレベルはパンフォームを使用して解決できますが、パンフォームはビジネスレベルのコード量を多くするため、アプリケーションで再定義する方が良いでしょう。

/// <summary>
/// 表示一个和当前业务强相关的启动任务
/// </summary>
public class StartupTask : StartupTaskBase
{
    protected sealed override Task RunAsync(IStartupContext context)
    {
        return RunAsync((StartupContext) context);
    }

    protected virtual Task RunAsync(StartupContext context)
    {
        return CompletedTask;
    }
}

上記のコードでは、アプリケーションのすべてのビジネス側は、スタートアップタスクアイテムのベースクラスとしてStartupTaskを継承する必要があります。継承後もRunAsyncメソッドをオーバーライドし、ビジネスロジックを実行します。

RunAsyncを抽象的なメソッドではなく仮想メソッドとして設計したのは、アプリケーションビジネスには少しピットを必要とするスタートアップタスク項目があり、実際のロジック機能はなく、スタートアッププロセスのオーケストレーションを最適化するために追加されるだけであるためです。もう一つの重要な点は、同期のみのロジックを書くときに、RunAsyncタスクを返す方法がわからないという問題を解決することができ、上層のビジネス開発者が自然にbase.RunAsyncメソッドの結果を返すことができ、タスクを返す奇妙な方法を減らすことができることです。

カスタム起動タスクベース型が完了したら、StartupManagerBaseに基づいてアプリケーション関連のStartupManager型を記述する必要があります。ここでのロジックには、特定の起動タスクアイテムを起動する方法のロジックが含まれます。

/// <summary>
/// 和项目关联的启动管理器,用来注入业务相关的逻辑
/// </summary>
public class StartupManager : StartupManagerBase
{
    public StartupManager(CommandLine commandLine, FileConfigurationRepo configuration, Func<Exception, Task> fastFailAction, IMainThreadDispatcher mainThreadDispatcher) : base(new StartupLogger(), fastFailAction, mainThreadDispatcher)
    {
        var appConfigurator = configuration.CreateAppConfigurator();
        Context = new StartupContext(StartupContext, commandLine, (StartupLogger) Logger, configuration, appConfigurator);
    }

    private StartupContext Context { get; }

    protected override Task<string> ExecuteStartupTaskAsync(StartupTaskBase startupTask, IStartupContext context, bool uiOnly)
    {
        return base.ExecuteStartupTaskAsync(startupTask, Context, uiOnly);
    }
}

上記のコードでは、ExecuteStartupTaskAsyncメソッドをオーバーライドして、特定のスタートアップタスクアイテムを呼び出したときにビジネス関連のStartupContextパラメータを渡します。

アプリケーションがより多くの要件を持つ場合は、すべてのスタートアップアイテムをエクスポートするExportStartupTasksメソッドなど、StartupManagerBaseのメソッドをオーバーライドできます。これにより、アプリケーションがすべてのスタートアップタスクアイテムをエクスポートする方法を定義できます。AddStartupTaskMetadataCollectorメソッドをオーバーライドすると、アプリケーションが管理対象アセンブリに起動情報を追加する方法を定義できます。

上記のステップが完了した後、完了する必要があるもう一つのことは、新しく作成されたWPPFDemo.ApiプロジェクトはWPF依存関係を追加しておらず、アプリケーション内では、UIスレッドで実行する必要がある起動タスク項目があるため、WPF依存関係を追加したWPPFDemo.Appで定義を完了することです。

class MainThreadDispatcher : IMainThreadDispatcher
{
    public async Task InvokeAsync(Action action)
    {
        await Application.Current.Dispatcher.InvokeAsync(action);
    }
}

上記の基本が完了したら、Program.csのメイン関数でスタートアップフレームワークを実行し、WPFDemo.AppプロジェクトのProgram型に入り、メイン関数でコマンドラインを解析してから、Appを作成してから、スタートアップフレームワークを実行します。

[STAThread]
static void Main(string[] args)
{
    var commandLine = CommandLine.Parse(args);

    var app = new App();

    //开始启动任务
    StartStartupTasks(commandLine);

    app.Run();
}

StartStartupTasksメソッドでTask.Runメソッドを使用して、バックグラウンドスレッドでフレームワークを起動し、メインスレッドであるアプリケーションのUIスレッドが実行され始めます。

private static void StartStartupTasks(CommandLine commandLine)
{
    Task.Run(() =>
    {
        // 1. 读取应用配置
        // 应用将会根据配置决定启动的行为
        var configFilePath = "App.coin";
        var repo = ConfigurationFactory.FromFile(configFilePath);

        // 2. 对接预编译模块,获取启动任务项
        var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());

        // 3. 创建启动框架和跑起来
        var startupManager = new StartupManager(commandLine, repo, HandleShutdownError, new MainThreadDispatcher())
            // 3.1 导入预设的应用启动节点,这是必要的步骤,业务方的各个启动任务项将会根据此决定启动顺序
            .UseCriticalNodes
            (
                StartupNodes.Foundation,
                StartupNodes.CoreUI,
                StartupNodes.UI,
                StartupNodes.AppReady,
                StartupNodes.StartupCompleted
            )
            // 3.2 导出程序集的启动项
            .AddStartupTaskMetadataCollector(() =>
                // 这是预编译模块收集的应用的所有的启动任务项
                assemblyMetadataExporter.ExportStartupTasks());
        startupManager.Run();
    });
}

以上的例子应用里面,有业务是需要根据配置决定启动过程,因此需要先读取应用配置。应用配置选取 dotnetCampus.Configurations 高性能配置文件库 可以极大减少因为读取配置而占用太多启动时间。以上的例子里,还对接了预编译模块。预编译模块的功能是收集应用里的所有启动任务项,如此可以极大提升收集启动任务项的耗时,也不需要让上层业务开发者需要手工注册启动任务项

上記のコードは、Main関数の起動後にフレームワークを実行して起動します。しかし、アセンブラMetadataExporterのロジックである事前コンパイル済みモジュール関連のロジックがまだ完了していないため、上記のコードはコンパイルされません。

これは、ブートフレームワークがプリコンパイル済みモジュールに強く依存していることと等価ではなく、プリコンパイル済みモジュールにオプションでアクセスできることです。必要なロジックは、アプリケーションを取得するために必要な起動タスク項目を渡すAddStartupTaskMetadataCollectorメソッドに接続するだけです。反射などを含め、どんな方法でも構いません。パフォーマンスを最適化し、起動タスク項目の収集時間を短縮するためのコンパイル済みモジュールへのアクセス

次に、プリコンパイルされたモジュールのアクセスロジックです。この記事では、Telescopeプリコンパイルされたモジュールの原理部分を扱わず、アクセス方法のみを含みます。

和 .NET 的其他库一样,为了接入预编译模块,就需要先安装 NuGet 库。通过 NuGet 安装 dotnetCampus.Telescope 库,如果是新 SDK 风格的项目文件,可以编辑 csproj 项目文件,添加如下代码安装

<ItemGroup>
    <PackageReference Include="dotnetCampus.TelescopeSource" Version="1.0.0-alpha02" />
</ItemGroup>

不同于其他的库,由于 dotnetCampus.Telescope 预编译框架是对项目代码本身进行处理的,需要每个用到预编译都安装此库,因此需要为以上三个项目都安装,而不能靠引用依赖自动安装

インストールが完了したら、プロジェクトに新しいAssemblyInfo.csファイルを作成して、アセンブリに機能を追加します。慣習により、AssemblyInfo.csファイルはPropertiesフォルダに置かれます。このPropertiesフォルダは特別なフォルダであり、Visual Studioで新規作成すると、このフォルダのアイコンが他のフォルダとは異なることがわかります。

AssemblyInfo.csファイルに以下を追加します。

[assembly: dotnetCampus.Telescope.MarkExport(typeof(WPFDemo.Api.StartupTaskFramework.StartupTask), typeof(dotnetCampus.ApplicationStartupManager.StartupTaskAttribute))]

以上就是对接预编译框架的代码,十分简单。通过给程序集加上 dotnetCampus.Telescope.MarkExportAttribute 可以标记程序集的导出预编译的类型,传入的两个参数分别是导出的类型的基类型以及所继承的特性

以上代码表示导出所有继承 WPFDemo.Api.StartupTaskFramework.StartupTask 类型,且标记了 dotnetCampus.ApplicationStartupManager.StartupTaskAttribute 特性的类型

タグ付け後、コードを再構築すると、objフォルダにAttributedTypesExport.g.csビルドファイルがあります。この記事のサンプルプロジェクトのように、ビルドファイルのパスは次のとおりです。

C:\lindexi\Code\ApplicationStartupManager\demo\WPFDemo\WPFDemo.Api\obj\Debug\net6.0\TelescopeSource.GeneratedCodes\AttributedTypesExport.g.cs

次のように定義されたFoo 1 Startupというスタートアップタスクアイテムがあるとします。

[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTasks = StartupNodes.Foundation)]
public class Foo1Startup : StartupTask
{
    protected override Task RunAsync(StartupContext context)
    {
        context.Logger.LogInfo("Foo1 Startup");
        return base.RunAsync(context);
    }
}

AttributedTypesExport.g.csは以下のコードを含む。

using dotnetCampus.ApplicationStartupManager;
using dotnetCampus.Telescope;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WPFDemo.Api.StartupTaskFramework;

namespace dotnetCampus.Telescope
{
    public partial class __AttributedTypesExport__ : ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>
    {
        AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[] ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>.ExportAttributeTypes()
        {
            return new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[]
            {
                new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>(
                    typeof(WPFDemo.Api.Startup.Foo1Startup),
                    new StartupTaskAttribute()
                    {

                        BeforeTasks = StartupNodes.CoreUI,
                        AfterTasks = StartupNodes.Foundation
                    },
                    () => new WPFDemo.Api.Startup.Foo1Startup()
                ),
            };
        }
    }
}

つまり、アセンブリ内の起動項目を自動的に収集し、収集されたコードを生成する。

可以在启动框架模块里面,新建一个叫 AssemblyMetadataExporter 的类型来从 AttributedTypesExport.g.cs 拿到收集的类型。从 Telescope 拿到 __AttributedTypesExport__ 生成类型的方法是调用 AttributedTypes 的 FromAssembly 方法,代码如下

IEnumerable<AttributedTypeMetadata<StartupTask, StartupTaskAttribute>> collection = AttributedTypes.FromAssembly<StartupTask, StartupTaskAttribute>(_assemblies);

以上代码传入的 _assemblies 参数就是需要获取收集的启动任务项程序集列表,调用以上代码,将会从传入的各个程序集里获取预编译收集的类型

この収集された戻り値はStartupTaskMetadataとしてカプセル化され、スタートアップ·フレームワークに戻されます。

using System.Reflection;

using dotnetCampus.ApplicationStartupManager;
using dotnetCampus.Telescope;

namespace WPFDemo.Api.StartupTaskFramework
{
    public class AssemblyMetadataExporter
    {
        public AssemblyMetadataExporter(Assembly[] assemblies)
        {
            _assemblies = assemblies;
        }

        public IEnumerable<StartupTaskMetadata> ExportStartupTasks()
        {
            var collection = Export<StartupTask, StartupTaskAttribute>();
            return collection.Select(x => new StartupTaskMetadata(x.RealType.Name.Replace("Startup", ""), x.CreateInstance)
            {
                Scheduler = x.Attribute.Scheduler,
                BeforeTasks = x.Attribute.BeforeTasks,
                AfterTasks = x.Attribute.AfterTasks,
                //Categories = x.Attribute.Categories,
                CriticalLevel = x.Attribute.CriticalLevel,
            });
        }

        public IEnumerable<AttributedTypeMetadata<TBaseClassOrInterface, TAttribute>> Export<TBaseClassOrInterface, TAttribute>() where TAttribute : Attribute
        {
            return AttributedTypes.FromAssembly<TBaseClassOrInterface, TAttribute>(_assemblies);
        }

        private readonly Assembly[] _assemblies;
    }
}

Program.csに戻り、BuildStartupAssembliesメソッドを作成します。このメソッドでは、起動タスクアイテムを収集する必要があるアセンブリのリストをAssemblyMetadataExporterに渡します。

class Program
{
    private static void StartStartupTasks(CommandLine commandLine)
    {
        Task.Run(() =>
        {
            var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());

            // 忽略其他逻辑
        });
    }

    private static Assembly[] BuildStartupAssemblies()
    {
        // 初始化预编译收集的所有模块。
        return new Assembly[]
        {
            // WPFDemo.App
            typeof(Program).Assembly,
            // WPFDemo.Lib1
            typeof(Foo2Startup).Assembly,
            // WPFDemo.Api
            typeof(Foo1Startup).Assembly,
        };
    }
}

エクスポートされたスタートアップタスク項目は、StartupManagerのAddStartupTaskMetadataCollectorを使用してスタートアップ·フレームワークに追加できます。

var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());

var startupManager = new StartupManager(/*忽略代码*/)
// 导出程序集的启动项
.AddStartupTaskMetadataCollector(() => assemblyMetadataExporter.ExportStartupTasks());

startupManager.Run();

これにより、すべてのアプリケーションの起動フレームワークの構成ロジックが完了し、各ビジネスモジュールが起動ロジックを記述します。

個々のビジネスモジュールの起動タスクアイテムを追加して起動フレームワークの使用方法を示す

WPFDemo.AppにMainWindowStartupをメインウィンドウの起動に追加する

using System.Threading.Tasks;

using dotnetCampus.ApplicationStartupManager;

using WPFDemo.Api.StartupTaskFramework;

namespace WPFDemo.App.Startup
{
    [StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = StartupNodes.UI, Scheduler = StartupScheduler.UIOnly)]
    internal class MainWindowStartup : StartupTask
    {
        protected override Task RunAsync(StartupContext context)
        {
            var mainWindow = new MainWindow();
            mainWindow.Show();

            return CompletedTask;
        }
    }
}

上記のコードは、StartupTask機能により、AppReadyの前に実行する必要がある起動タスク項目、UIの後に実行する必要があることを示しており、メインスレッドにスケジュールする必要があります。メインウィンドウの表示では、ViewModel登録やスタイルディクショナリの初期化など、他のUI関連ロジックの実行が完了するまで待つ必要があります。AppReadyアプリケーションはメインウィンドウの準備が完了した後にのみ完了するため、起動タスク項目をこのように整理できます。

次に、ビジネス関連のスタートアップタスク項目を追加し、ビジネスを実装するためにビジネススタートアップを追加します。ビジネスはメインインターフェイスにボタンを追加する必要があります。したがって、必要に応じて、MainWindowStartupの実行が完了した後にBusinessStartupを起動する必要があります。

[StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = "MainWindowStartup", Scheduler = StartupScheduler.UIOnly)]
internal class BusinessStartup : StartupTask
{
    protected override Task RunAsync(StartupContext context)
    {
        if (Application.Current.MainWindow.Content is Grid grid)
        {
            grid.Children.Add(new Button()
            {
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Bottom,
                Margin = new Thickness(10, 10, 10, 10),
                Content = "Click"
            });
        }

        return CompletedTask;
    }
}

可以看到,在 BusinessStartup 里,通过 AfterTasks 设置了 MainWindowStartup 字符串,也就表示了需要在 MainWindowStartup 执行完成之后才能执行

さらに、依存関係は複数のプロジェクトにまたがることができます。例えば、インフラストラクチャ内のWPFDemo. Lib 1アセンブリを持つLibStartupは、インフラストラクチャに属するコンポーネントの初期化を表し、BeforeTasksを介してFoundationのプリセットスタートアップノードで開始するように指定します。

[StartupTask(BeforeTasks = StartupNodes.Foundation)]
class LibStartup : StartupTask
{
    protected override Task RunAsync(StartupContext context)
    {
        context.Logger.LogInfo("Lib Startup");
        return base.RunAsync(context);
    }
}

上記のように、このフレームワーク設計では、StartupTask型RunAsyncを仮想メソッドとして与え、ビジネスドッキングを容易にし、同期ロジックを行い、ベースクラスメソッドを呼び出すことでTaskオブジェクトを返すことができます。

上記のコードはBeforeTasksをマークし、AfterTasksをマークしない場合、デフォルトでAfterTasksを仮想スタートポイントとして割り当てます。つまり、他のスタートアップを待つ必要はありません。

WPFDemo.Apiアセンブリには、コマンドラインに基づいて実行するロジックを表すOptionStartupがあります。これもインフラストラクチャに属しますが、LibStartupの実行完了に依存します。

[StartupTask(BeforeTasks = StartupNodes.Foundation, AfterTasks = "LibStartup")]
class OptionStartup : StartupTask
{
    protected override Task RunAsync(StartupContext context)
    {
        context.Logger.LogInfo("Command " + context.CommandLineOptions.Name);

        return CompletedTask;
    }
}

OptionStartupをLibStartupの後、Foundationの前に実行することができる。

上記のコードの起動図は以下の通りです。LibStartupとOptionStartupはUIスレッドを必要とせず、デフォルトでスレッドプールにスケジュールされます。

BeforeTasksとAfterTasksの両方で、セミコロンで区切られた複数の異なるスタートアップ項目のリストを渡すことができます。代わりに、BeforeTaskListとAfterTaskListを使用して配列を使用することもできます。例えば、WPFDemo.Apiアセンブリを使用したFoo 1 StartupとWPFDemo.Lib 1のFoo 2 StartupとFoo 3 Startupスタートアップタスクアイテムです。Foo 3 StartupはFoo 1 StartupとFoo 2 Startupの実行完了に依存します。

[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTaskList = new[] { nameof(WPFDemo.Lib1.Startup.Foo2Startup), "Foo1Startup" })]
public class Foo3Startup : StartupTask
{
    protected override Task RunAsync(StartupContext context)
    {
        context.Logger.LogInfo("Foo3 Startup");
        return base.RunAsync(context);
    }
}

以上就是应用接入 ApplicationStartupManager 启动流程框架的方法,以及业务方编写启动任务项的例子。以上的代码放在 https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager 的例子项目

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/04/22

バージョン別の. NETサポート状況(250 7 0 7更新)

仮想マシンとテストマシンを使用して、各バージョンのオペレーティングシステムの. NETサポートをテストします。オペレーティングシステムのインストール後、対応するランタイムを測定し、スターダストエージェントをパスとして実行できます。

继续阅读
同分类 / 同标签 2026/02/07

AOTの使用経験

プロジェクトの最初から、新しい機能が追加されたり、新しい構文が使用されたりするたびに、AOTリリーステストを行うという良い習慣を身につける必要があります。

继续阅读