Now in REALITY Tech #65 Unity2022.2にアップデートした話とアップデートを行うにあたってのTips

こんにちは、REALITY の Unity チームの虹ゴリラです。

REALITYアプリでは 2023/3/20 にリリースされた 23.12.0 より、内部で使われている Unity バージョンを 2020.3 (2020.3.33f1) から 2022.2 (2022.2.5f1) にアップデートしました。

このアップデートのおかげで C# 9.0 や .NET Standard 2.1、MemoryProfiler 1.0 と言った新機能が使えるようになった他、2020.3 から 2022.2 にかけて導入されているビルドやインポート周りの高速化の恩恵を受けたり、他にも Editor の AppleSilicon 対応によるイテレーションの向上などで開発体験を上げることが出来るようになりました!

ただ、実際にアップデートをするにあたってはそれなりに課題も出てきており、中には今まで動いていた箇所がアップデート起因でエラーが出て動かなくなると言った現象もチラホラと発生しました。

そこで今回はこれらの問題をどう解決していったのか?と言ったお話の他、アップデートを行うにあたって実施した幾つかのTipsについてお話できればと思います。

アップデート起因の不具合が発生した際の調査方法

前提としてアップデート作業で不具合が発生した場合には、先ずは原因の特定を進め、次に設定の変更やワークアラウンドなどで回避可能か?を模索していく流れになります。
(Unity に BugReport を送って修正を待つのは全体を通すとそれなりに時間を要するかと思われるので最終手段です)

ここではこれらを進めていくにあたっての調査すべき観点について解説します。

Forum や IssueTracker などを調べてみる

ビルド/コンパイルエラーや実行時エラーなど含め、何か発生した際には先ずは既知の問題であるか調べてみます。運が良ければ原因や公式見解、ワークアラウンドの手法などを得ることが出来ます。

ただ、アップデート対象の Unity が新しいと Forum や IssueTracker でヒットしなかったり、中にはエラーの情報が抽象的で「なんて調べたら良いのか分からない」と言った状況になることもあります。
この場合には次に解説する手順で調査していきます。

エラーが出ている場合には Stacktrace など参考にしつつ、問題となる箇所を特定する

例えば「アップデート前のバージョンでは動いていたのに、アップデート後のバージョンだと動かなくなる」と言った現象が起こっている場合には、何かしらの UnityAPI の内部挙動が変わってエラーが発生している可能性があります。

この場合にはエラーログや Stacktrace、更にはブレークポイントなどを活用して問題が起こっている API などを特定していきます。
この段階である程度問題に対する解像度が上がってきていたら、再度 Forum や IssueTracker などを調べ直してみると何かヒントが見つかるかもしれません。

不明な場合には UnityCsReference で実装を覗いてみる

Unity には UnityCsRefence と言うリポジトリがあり、C# レイヤーまでの実装であればこちらから中身を追うことが可能です。

機能によってはエンジン部分(C++ レイヤー)の呼び出しが主であり、 C# 側ではあまり実装を持たないものもありますが、中には C# 側で大きく実装を持っている物もあるので、もし Forum や IssueTracker などでヒントが見つからない場合には、こちらを参考にアップデート前とアップデート後のバージョンのコード差分などを読み比べてみるとヒントが見つかることがあります。
(各バージョンごとのブランチが切られているので、こちらを切り替える形で確認することが出来ます)

◆ 具体例
この手順で解消した不具合の実例を1つ挙げると、既存コードの中に WWWForm を生成し、その後に AddBinaryData を呼び出してから data プロパティにアクセスしているコードがあったのですが、2022.2 からは AddBinaryData の param3 である `fileName` に空文字を渡した状態でアクセスすると、`ArgumentNullException: Array cannot be null.`が投げられてしまう現象を確認しました。

var wwwForm = new WWWForm();
wwwForm.AddBinaryData(fieldName, bytes, "");

// 2022.2 だと param3 の `fileName` に空文字を渡した状態で `data` プロパティにアクセスすると、
// `ArgumentNullException: Array cannot be null.` が投げられてしまう
Debug.Log(wwwForm.data);

調査したところ、data プロパティ及び内部で呼び出されている SevenBitClean の実装が変わっているのを確認しており、こちら起因で投げられていることが判明したので、引数に空文字を渡さないようにする形で不具合を回避してます。

その他にも、Editor 拡張にて Reflection で internal なクラスに対してアクセスしている箇所が内部の実装変更の影響で動かなくなったと言った事例もあり、今回だと以下の GameView.selectedSizeIndex のアクセス修飾子が private から public に変わった影響で実行に失敗している箇所を特定しました。

とは言え、internal なクラスに対して Reflection でアクセスを行っている箇所については、こちら都合の不具合となるので、もし似た手法をとっている箇所がある場合には基本的にはアップデート時には気をつけておく必要があります。

◆ 補足: .NET Standard 2.1 に追従してそう?
上述したように、今回のアップデート対応でも 2020.32022.2 の差分を見比べる機会が幾つかあったのですが、2022.2 の方は .NET Standard 2.1 に対応していることもあってか、内部的に Span<T> が使われるなど、環境に追従して改修されている印象を受けました。

最小プロジェクトに切り出してみる

もし不具合の特定や再現フローに時間を要する場合には、一度最小のプロジェクトに切り出す形で再現できないか試してみることもオススメです。

そもそも切り出せるかどうかを試すことによって「プロジェクトや特定のアセットに依存している不具合かどうか?」の切り分けにも役立つ上に、もし切り出すことに成功したら規模が小さくなるので、調査イテレーションを早めることが出来ます。

あとは副次的なところとして、そのまま BugReport を出しやすくなると言った利点も出てきます。

クラッシュ発生時の調査について

REALITY では Unity は Cloud Diagnostics を導入し、ネイティブ側は Firebase Crashlytics を導入してます。
先ずはこちらを見て該当するログが無いか調査します。

その上で再現手順なども模索しつつ、確立してきたらローカルで Xcode や AndroidStudio を用いてデバッグ実行し、こちらからも情報を収集していきます。

ある程度クラッシュ時の情報が集まってきたら、Forum / IssueTracker で再度検索をしてみたり、見つからない場合には Stacktrace の情報から怪しいと思われる箇所の挙動を変えるなどして様子を見て仮説を立てていきます。

その上で、例えばもし「特定の API を呼び出し続けるとクラッシュする」と言った不具合を観測した際には、BugReport を提出しつつ、その API を呼び出さずに回避するワークアラウンドなどを検討していきます。

他にもまだ話せていない事例がチラホラと…

とりあえずは Tips として調査方法と幾つかの簡単な事例を解説しましたが、他に起こった事例と具体的な対処方法(どう調査して解決していったのか?)の観点で言えばまだ説明できていないところが幾つかあります..。

今回のBlogの内容を図で表すとこんなイメージ

これらについてはネイティブレイヤーも絡んでいて解説が難しいところもあり、別途説明の機会があればそちらでご紹介できればな…と考えておりますが、とりあえずは章の締めとして今回のアップデートで確認した事例と簡単な対処方法をザックリとだけ纏めておきます。

後はあくまでアップデート作業中に確認したものであり、物によっては後のバージョンで修正されている可能性がある点についてはご了承下さい。

  • AndroidJavaObject / AndroidJavaClass の解放を忘れた状態でインスタンスを生成し続けるとクラッシュ

    • 既存コードでこれらを頻繁に呼び出している箇所があったものの、上手く解放を行えていない不具合があった

    • 2020.3 では挙動としては問題なかったものの、2022.2 からは解放しないままインスタンスを生成し続けるとクラッシュする不具合を確認

      • ネイティブ側で `JNI ERROR (app bug): global reference table overflow (max=51200)global reference table dump:` と言うエラーが出ている

      • ちなみに 2020.3 で問題が無かったのは AndroidJavaObject / AndroidJavaClass の Finalizer から Dispose が呼び出されており、こちら経由でリークすることが無かったと思われるが、2022.2 からは挙動が変わったのか、Finalizer スレッドから Dispose が呼び出されると挙動が怪しそうだった?

    • とりあえずは不具合を修正し、ちゃんと Dispose を呼び出すようにして対処

  • 空の payload (0byte) を受け取ると downloadHandler.data が null になる

    • 前提としてREALITY では 一部の API は protobuf 形式で通信を行っており、2020.3 まではレスポンスで空の payload を受け取ると空の byte 配列を返す挙動となっていた箇所が null を返すようになっていた

    • 既存コードは byte 配列が返ってくることを期待した実装となっていたので、null が来ても動くように修正

  • 一部 iOS 端末で 60fps に設定するとクラッシュ

  • AssetBundleビルド時にメモリリークでフリーズ

    • Sprite Packer Mode が「Sprite Atlas V1 - Enabled For Builds」指定だとメモリリークっぽい現象を確認

      • 「Sprite Atlas V2 - Enabled」に移行することで回避できた

    • 最小プロジェクトに切り出そうとしても上手く再現できず…

      • ひょっとしたらプロジェクト上の複合的な要因が重なった結果、発生している不具合かもしれないが…参考程度に書き記しておく

  • UnityHub CLI から Unity 2022.2.5f1 をインストールすると Android ビルドが上手く行かない

    • CLI 経由で入れると PlaybackEngines/AndroidPlayer 以下にある Modules が古そうだった?

    • GUI 上からインストールする分には問題無さそうだったので、CLI を使わないことで回避

    • UnityHub のバージョンは `3.4.1` で確認

作業中のブランチ運用

Unity はメジャーアップデート規模になってくると、Texture や 3D Model、AudioClip と言った各種AssetImporter のシリアライズ形式に変更が入る可能性があり、もし仮に変更が入った場合にはアップデート時に該当の .meta ファイルに差分が発生します。

その上で REALITY ではプロジェクトのコードや運用中のアセットなどを1つのリポジトリで管理しているというのもあり、この状態で 1つのブランチで全てを管理すると大量の .meta ファイルの差分を含むこととなってしまうため、File changed の数が 5000 を余裕で超えてしまうことになります。

そこで今回は「機械的に発生する変更差分を管理するブランチ」と「不具合修正のコード差分などを管理するブランチ」を分けることにより、アップデートに伴う意図的な変更点を俯瞰しやすくする形式を取りました。

簡略化してますが、図にするとこのような形になります。

もう少し具体的に書くと次のようなものを各ブランチで取り扱います。
(ちなみにこれらは説明用に簡略化しており、実際にはもう少し違ったブランチ運用をしてます)

  • update_unity2022.2_meta

    • develop ブランチから派生

    • 主にアップデート時に機械的に発生する差分を管理

      • 各種アセットの自動更新される .meta 差分

      • ProjectVersion.txt や 自動更新される Packages の更新差分

      • アップデート時に自動で時間される UnityAPI のコードなど

  • update_unity2022.2_code

    • update_unity2022.2_meta から派生

    • 不具合修正に伴うコード差分や、ビルドに必要な Makefile やシェルスクリプトなどの更新差分

次のアップデートに備えてドキュメントを残しておく

アップデート時に得た知見や見ておくべき観点と言った情報は次のアップデートに備えてドキュメント化を行っております。

と言うのも、REALITY は大体1年ペースで Unity のアップデートを行っているのですが、恐らくは1年も経てば前回対応時に得た情報は明確に覚えきれていない可能性があります。

その上で特に担当が決まっているタスクでもないので、チームの誰がアサインされても対応を進めやすいようにフォローしておくと言った意図があります。

ドキュメントの冒頭部
(全部はお見せできませんが..)

おわりに

今回は簡単ではありますが、幾つかの Tips についてシェアさせていただきました。

とは言え、不具合調査のところに関しては抽象的な説明の部分が多く、まだ実例など交えつつ説明できていない部分も多々あるので、これらの情報についてはまたどこかの別の機会で説明できれば良いなと考えております。

ブランチ運用についてはプロジェクトによって運用が変わってくるかと思われるので、もし REALITY に近い運用をされている場合には一助となれば幸いです。

最後に、REALITYでは一緒に開発してくれるメンバーを募集しています。ご応募お待ちしております!