ぴーさんログ

だいたいXamarin.Formsのブログ

.NET for iOS / AndroidのSDKバージョンを固定する

.NET for iOS / Android、およびMAUI SDKのバージョンを制御する方法というテーマで調べた事をつらつら書いていきます。

問題

  • .NET 6以降のiOS/Android SDK管理は、基本的に最新へ上げ続ける事を要求する。
  • ついでに、Xamarin.Formsが.NET MAUIになってバージョンを下げられなくなった。

特に.NET for iOS SDKのバージョンが上がると、Xcodeのバージョンを上げないとビルドすらできなくなる場合もあり大変困ります。

主な動機

  • ビルドパイプラインのために毎回同じ環境を一から構築できるようにする。
  • セットアップ時期の違うマシンで開発環境を揃える。
  • バグのあるバージョンを避けてSDKをダウングレードする。

SDK? Workload?

さて、今回扱うSDKは、XamarinでいうとXamarin.iOS / Android SDKに当たるものです。 つまり、C#のコードをiOS/Androidアプリにするためのビルドツールや、ネイティブAPIC#ラッパーの事です。 ネイティブのAndroid SDKXcodeは含みません。(簡単のためC#前提で進めます)

.NET 6以降でこれらのSDKWorkload という仕組みでインストールされます。 Visual StudioSDKの更新サイクルを分離して、個別にアップデート出来るようにするのが大きな目的です。

dotnet workload コマンドが用意されており、例えばMAUI SDKをインストールする場合は dotnet workload install maui を実行します。 dpotnet install では常に最新のWorkloadがインストールされ、 dpotnet update でインストール済みのWorkloadを全て最新に更新します。バージョン指定するオプションはありません。

いくら何でも強気すぎますね!?

ついでに、Xamarin.FormsはただのライブラリだったのでNuGetで任意のバージョンをインストールできましたが、後継の.NET MAUIは.NET SDKの一部になってしまい開発環境で特定のバージョンを使うことになりました。 (複数のプロジェクトで違うバージョンで開発したい場合に困りますね)

アンドキュメントな方法でバージョンを指定する

実は workload installworkload update には --from-rollback-file という隠しパラメータが存在していて、所定のフォーマットのjsonファイルを与えるとバージョン指定が出来ます。

ロールバックファイルの中身はこんな感じ。

{
  "microsoft.net.sdk.maui": "7.0.52",
}

コマンド実行する時はこんな感じになります。

workload install maui --from-rollback-file rollback.json

先行事例に倣って"microsoft.net.sdk.maui"と指定しましたが、実験したところ代わりにWorkload IDの"maui"を使っても大丈夫でした。

参考

ドキュメント化しないと明言されているissue

Add workload install --from-rollback-file documentation · Issue #28226 · dotnet/docs

MAUIのバージョンを固定したいと相談しているissue

How do I Pin a Maui App to a specific version? · Issue #8985 · dotnet/maui

ロールバックファイルで指定する内容

ロールバックファイルで指定するバージョンをいくつにしたら良いか?

大抵の場合は安定環境にインストールされているメモして、それを利用するのが良さそうです。

WindowsVisual Studioをインストールした環境で dotnet workload list を実行すると以下のような結果となりました。

インストール済みワークロードの ID      マニフェストのバージョン           インストール ソース
----------------------------------------------------------------
maui-android            7.0.52/7.0.100         VS 17.4.33213.308
android                 33.0.4/7.0.100         VS 17.4.33213.308
maui-windows            7.0.52/7.0.100         VS 17.4.33213.308
maui-maccatalyst        7.0.52/7.0.100         VS 17.4.33213.308
maccatalyst             16.1.1477/7.0.100      VS 17.4.33213.308
maui-ios                7.0.52/7.0.100         VS 17.4.33213.308
ios                     16.1.1477/7.0.100      VS 17.4.33213.308

消せないWorkload

WindowsVisual Studioをインストールしている環境では関連するWorkloadをアンインストールできなくなるようです。

「インストールソース」で頭に"VS"が付いてるものをアンインストールしようとすると、見つからない(実際にはインストールされている)扱いとなるので特別扱いされます。

"ロールバックファイルで指定する内容"の状態からmauiiosのWorkloadをインストールするとこのようになります。

インストール済みワークロードの ID      マニフェストのバージョン           インストール ソース
-----------------------------------------------------------------------------
ios                     16.2.1024/7.0.100      SDK 7.0.100, VS 17.4.33213.308
maui                    7.0.59/7.0.100         SDK 7.0.100

この後、mauiiosのWorkloadをアンインストールできますが、mauiのそのままリストから消えるのに対し、iosの方は「インストールソース」の"SDK 7.0.100"部分だけ消えてWorkload自体は残ります。

CIなどを考える場合、WindowsではVisual Studioをインストールせずに.NET SDK、Workloadだけを直接インストールしたビルド専用マシンにするのが良さそうです。

Workload Manifest

WorkloadのIDやバージョンはWorkload Manifestに定義されています。 ManifestファイルはNuGetで配布されていて、dotnetコマンドをこれを自動的に取得して有効なIDか、新しいバージョンがあるかなどを判別しているようです。

nuget.orgで"Microsoft.NET.Sdk"と検索するとそれらしいのが沢山ヒットします。

命名規則は次のようになっているようで……

Microsoft.NET.Sdk.<プラットフォーム>.Manifest-<.NET SDKバージョン>

.NET 7.0.1xx系で利用可能なiOS開発用SDKの場合は Microsoft.NET.Sdk.iOS.Manifest-7.0.100 という具合ですね。

中身には WorkloadManifest.json という名前でマニフェストファイルの実体が入っています。

おわり

  • ビルド専用: ロールバックファイルを利用すれば固定SDKバージョンで再現性のある環境構築が出来そう。
  • 開発用(Windows): Visual StudioのせいでSDKバージョンをコントロール出来ないので、最新に追従し続ける事になりそう。
  • 開発用(Mac): ロールバックファイルでバージョン指定できるが、VS for Macが利用しているXamarin SDKとバージョンが乖離していくと問題が発生しそう。(どうやらデバッグ実行でiOSシミュレータ起動したりはXamarin SDK側の機能っぽい)
  • Xamarin用の開発環境と.NET 6+用の開発環境は分けた方が安全そう。

csprojファイル内で使用されるMSBuildプロパティのデフォルト値を調べる

前置き

.NET Coreと同じくらいの時期に登場したSDKスタイルと呼ばれるcsproj形式では、 明示的に記述しなくても多くのMSBuildプロパティに初期値が設定されるようになっています。

ここで Release ビルドの設定を少し変更した Release_Sandbox のようなビルド構成を 作る場合を考えます。

Configurationプロパティ(ビルド構成)が DebugReleaseかによって設定される初期値が変わってきますが、 独自のビルド構成ではこの条件に引っかかりません。

そのためRelease構成のバリエーションを作るためには Releaseの場合に設定される初期値を調べて再現する必要が出てきます。

方法

MSBuildコマンドの-pp(-preprocess)を使って、ビルド時にcsprojにインポートされる全てのファイルが展開された結果を出力します。

次のようなコマンドを実行します。(MSBuildの部分はdotnet buildでもOK)

MSBuild NetAndroidApp.csproj -pp:_NetAndroidApp.csproj.xml

実行すると画像左のcsprojから右のファイルが出力されます。

ここから '$(Configuration)' == 'Release' の条件が含まれる部分を調べていけば良さそうです。

最適化や……

Assemblyのトリミング設定に影響している事が分かりますね。

おわり

という訳でXamarin.Androidプロジェクトを.NET 6+に移行する際に調べたメモでした。

CIやコマンドラインビルドの場合はRelease構成指定にプロパティを上書きで問題ありませんが、 VSのGUI上ではビルド構成を追加しないと使い分けにくいんですよね。

どこかに「Release構成のプロパティを全て継承する」みたいな指定方法ありませんかね?

Xamarin.Forms Material Visualのバグ修正しました

お仕事で踏んだXamarin.Formsのバグ修正をしました。

この記事を書いている時点でPR承認済みなのでそのうち取り込まれてリリースされると思います。

[Bug] iOS Material Renderers can cause NRE. · Issue #15669 · xamarin/Xamarin.Forms

この問題はMaterial Visual(いわゆるマテリアルデザイン化するやつ)を使っていると、iOSでまれにNullReferenceExceptionが発生するというものです。

原因個所はレイアウト更新時のnullチェックで、タイミングが悪いとMaterialXxxxRendererがElement(Xamarin.Formsコントロール)付け替え中にそのプロパティを参照してNREになるようです。

今この問題を踏んで困っている人向けの回避策

MaterialXxxxRendererのカスタムRendererでApplyThemeIfNeededにnullチェックを追加して差し替えましょう。 (サンプルはFrameコントロールを差し替える例)

using System;
using Xamarin.Forms;
using Xamarin.Forms.Material.iOS;

[assembly: ExportRenderer(
    typeof(Xamarin.Forms.Frame),
    typeof(XFApp.iOS.MyMaterialFrameRenderer),
    new[] { typeof(VisualMarker.MaterialVisual) })]

namespace XFApp.iOS
{
    public class MyMaterialFrameRenderer : MaterialFrameRenderer
    {
        protected override void ApplyThemeIfNeeded()
        {
            // まれにElement差し替えタイミングで走ることがあるのでnullチェックする。
            if (Element == null) return;
            base.ApplyThemeIfNeeded();
        }
    }
}

艦これ二期のスクリーンショットを撮るChrome拡張(ver.1.5)

やったのもう結構前なんですが、艦これのスクショを撮るChrome拡張がストアのポリシー変更に対応してなくて取り下げられちゃってたので対応しました。

chrome.google.com

ポリシー違反関係の対応自体は申請ページの作業なんですが、バックグラウンド処理のやり方が変わってservice worker必須になっていたのでその対応もしてます。

github.com

Xamarin.iOSのメモリ管理

2つのメモリ管理

Xamarin.iOS(.NET 6も同様)では、C#(.NET)側とiOS側(Objective-C, Swift)でそれぞれ独立したメモリ管理をしています。

C#(.NET)では、ガベージコレクタ(GC)を採用しています。 コード上ではメモリ管理を行わず、GC不定期にどこからも参照されていないインスタンスを検査してメモリを解放します。

一方、iOS(Objective-C, Swift)では参照カウンタを採用しています。 歴史的にMRC(Manual Reference Counting)とARC(Automatic Reference Counting)があり、ARCの方が新しい方式です。MRCではコードでメモリ管理を行い、ARCではコンパイラがメモリ管理のコードを自動生成してくれるというイメージです。

Xamarin.iOSでは内部的にMCRを利用してiOS側オブジェクトの参照カウンタ操作を行います。

インスタンス生成~解放

NSObjectなどのiOSの世界のクラスをC#から生成して操作することが出来ますが、この時C#側のインスタンスiOS側のインスタンスがペアで生成されています。

NSObjectクラスを例にして単純なケースを考えると次のような感じです。

C#側からのインスタンス生成

  • C#new NSObject() 実行したとき、このコンストラクタ内部でObjective-C[[NSObject alloc] init] がP/Invoke経由で実行されます。
    • (参照カウンタ 0 → 1)
  • C#側のNSObjectは、生成されたiOS側NSObjectのポインタをHandleプロパティに格納します。

C#側からのインスタンス解放

  • C#側NSObjectが何処からも参照されなくなるとGCに回収され、後始末としてデストラクタが呼ばれる。
  • デストラクタから呼ばれたDisposeがP/Invoke経由で [NSObject release] を実行する。
    • (参照カウンタ N → N-1、この時1 → 0ならiOS側でもメモリ解放)

まとめ

実際にはもちろん、先に作られたiOSインスタンスを元にC#インスタンスが作られるケースもありますし、monoランタイムに絡む制御も複雑で実際に起こる事をトレースしていくのは至難の技です。 (私はそこまで出来てません)

そこまで行かなくとも、C#側とiOS側のメモリ管理とその連携がイメージできると、メモリリーク対策やバグフィックスに役立つと思います。

オマケ

実際の処理が気になる人はxamarin-maciosリポジトリNSObject2.cs を起点に色々見てみましょう。

個人的には、iOS側から(強制的に?)ネイティブオブジェクトを解放されてしまうと検知出来ないのを、Associated Objectを使ってフックして解決してる所なんか面白かったです。 (アタッチ先と一緒に死ぬのでdeinitに処理を仕込めば引っかけられる)

読んでいたソース

デスクトップPC更新

自宅のデスクトップPCを更新しました。

いつも構成を忘れるので備忘録としてここに残します。

買ったもの

旧PCはCore i7 3770K(2012年発売)だったので6年ぶりの更新。ストレージとグラボは旧PCから移植。

写真

f:id:ticktack623:20181208142141j:plain

f:id:ticktack623:20181208142011j:plain 作業開始。


f:id:ticktack623:20181208164413j:plain クソデカCPUクーラー君。


f:id:ticktack623:20181209134422j:plain 組み立て完了、マザーボード側。


f:id:ticktack623:20181209134259j:plain 組み立て完了、裏配線。

艦これ二期のスクリーンショットを撮るChrome拡張(ver.1.4)

chrome.google.com

前回の問題点を改善して、高DPIの環境だったり、スクロールやズームをしていても原寸大のスクリーンショットが撮れるようになりました。

ついでにオプションで保存形式を選択できるようにして

申し訳程度のアイコンを設定しました。

f:id:ticktack623:20180824202442j:plain