.NET for iOS / AndroidのSDKバージョンを固定する
.NET for iOS / Android、およびMAUI SDKのバージョンを制御する方法というテーマで調べた事をつらつら書いていきます。
問題
特に.NET for iOS SDKのバージョンが上がると、Xcodeのバージョンを上げないとビルドすらできなくなる場合もあり大変困ります。
主な動機
- ビルドパイプラインのために毎回同じ環境を一から構築できるようにする。
- セットアップ時期の違うマシンで開発環境を揃える。
- バグのあるバージョンを避けてSDKをダウングレードする。
SDK? Workload?
さて、今回扱うSDKは、XamarinでいうとXamarin.iOS / Android SDKに当たるものです。 つまり、C#のコードをiOS/Androidアプリにするためのビルドツールや、ネイティブAPIのC#ラッパーの事です。 ネイティブのAndroid SDKやXcodeは含みません。(簡単のためC#前提で進めます)
.NET 6以降でこれらのSDKは Workload
という仕組みでインストールされます。
Visual StudioとSDKの更新サイクルを分離して、個別にアップデート出来るようにするのが大きな目的です。
dotnet workload
コマンドが用意されており、例えばMAUI SDKをインストールする場合は dotnet workload install maui
を実行します。
dpotnet install
では常に最新のWorkloadがインストールされ、 dpotnet update
でインストール済みのWorkloadを全て最新に更新します。バージョン指定するオプションはありません。
いくら何でも強気すぎますね!?
ついでに、Xamarin.FormsはただのライブラリだったのでNuGetで任意のバージョンをインストールできましたが、後継の.NET MAUIは.NET SDKの一部になってしまい開発環境で特定のバージョンを使うことになりました。 (複数のプロジェクトで違うバージョンで開発したい場合に困りますね)
アンドキュメントな方法でバージョンを指定する
実は workload install
と workload 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
ロールバックファイルで指定する内容
ロールバックファイルで指定するバージョンをいくつにしたら良いか?
大抵の場合は安定環境にインストールされているメモして、それを利用するのが良さそうです。
WindowsでVisual 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
WindowsでVisual Studioをインストールしている環境では関連するWorkloadをアンインストールできなくなるようです。
「インストールソース」で頭に"VS"が付いてるものをアンインストールしようとすると、見つからない(実際にはインストールされている)扱いとなるので特別扱いされます。
"ロールバックファイルで指定する内容"の状態からmaui
とios
の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
この後、maui
とios
の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
という名前でマニフェストファイルの実体が入っています。
おわり
csprojファイル内で使用されるMSBuildプロパティのデフォルト値を調べる
前置き
.NET Coreと同じくらいの時期に登場したSDKスタイルと呼ばれるcsproj形式では、 明示的に記述しなくても多くのMSBuildプロパティに初期値が設定されるようになっています。
ここで Release ビルドの設定を少し変更した Release_Sandbox のようなビルド構成を 作る場合を考えます。
Configurationプロパティ(ビルド構成)が
Debug
かRelease
かによって設定される初期値が変わってきますが、
独自のビルド構成ではこの条件に引っかかりません。
そのため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拡張がストアのポリシー変更に対応してなくて取り下げられちゃってたので対応しました。
ポリシー違反関係の対応自体は申請ページの作業なんですが、バックグラウンド処理のやり方が変わってservice worker必須になっていたのでその対応もしてます。
艦これスクショ撮影のChrome拡張を更新しました。
— ざまりん.ふぉーむずマン(ハワイ仕様) (@ticktackmobile) July 20, 2022
ポリシー違反で怒られなくなったのでよろしくお願いします。 (* ᴗ ᴗ)⁾⁾https://t.co/odH1sd3oRNhttps://t.co/3P3yZUyv6a
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を更新しました。
いつも構成を忘れるので備忘録としてここに残します。
買ったもの
- CPU: Intel Core i7 9700K
- メモリ:SanMax SMD4-U32G48M-26V-D (DDR4-2666 16GB2枚組)
- マザーボード:ASRock Z390 Phantom Gaming 6
- 電源:Seasonic Prime 850 Titanium
- ケース:Fractal Design Define R6
- CPUクーラー:Noctua NH D15
旧PCはCore i7 3770K(2012年発売)だったので6年ぶりの更新。ストレージとグラボは旧PCから移植。
写真
作業開始。
クソデカCPUクーラー君。
組み立て完了、マザーボード側。
組み立て完了、裏配線。
艦これ二期のスクリーンショットを撮るChrome拡張(ver.1.4)
前回の問題点を改善して、高DPIの環境だったり、スクロールやズームをしていても原寸大のスクリーンショットが撮れるようになりました。
スクロールしてようが高DPIだろうがちゃんとスクショ撮れるようになったぞい。https://t.co/7cuYUcrJ0G pic.twitter.com/DZ8IZSSNMj
— ざまりん.ふぉーむずのコントリビューター🚲 (@ticktackmobile) 2018年8月21日
ついでにオプションで保存形式を選択できるようにして
Storage APIを使って保存形式を選べるようにしてみた。 pic.twitter.com/0ex4uzNkVX
— ざまりん.ふぉーむずのコントリビューター🚲 (@ticktackmobile) 2018年8月21日
申し訳程度のアイコンを設定しました。