バグ?と回避方法の紹介です。
- アプリがクラッシュする状況
- 回避策
- 推定原因
- まとめ
1. アプリがクラッシュする状況
本記事を読んでいる方と似ているかもしれません。 まずはざっくりとした経緯から。
- クロスプラットフォームアプリ用に
Xamarin.Forms
を導入 - 3DCGを表示したくなったので
UrhoSharp
を導入 UrhoSurface
上に色々シーンを作る都合でリファクタリング- ページ遷移のテストしたらいきなりクラッシュ
そして私の場合、アプリのページ構成としては最上位にNavigationPage
を使い、
その下でContentPage
を遷移させるようにしていました。
MainPage : NavigationPage
StartPage
: ここではUrhoSurface
不使用AppPage
: 画面の一部としてUrhoSurface
を使用
起動時にStartPage
を表示し、StartPage
とAppPage
を行き来できる構成です。
そしてアプリをクラッシュさせる手順と挙動がこちら。
- アプリを起動→
StartPage
が表示 AppPage
に移動→問題なくUrhoSurface
にシーン表示StartPage
に戻る- 再度
AppPage
に移動→アプリがクラッシュ
実機Android(Oreo)で確認しました。 iOSやUWPでどうなるかは分かりません。
とくに困った点は、try-catch
をすり抜けることです。
私はXamarin
のデバッガの限界がよく分かってないので内部挙動もピンと来ず、
「Urho
内部のネイティブコード中でSEGVしてる?」と憶測が立った程度でした。
2. 回避策
結論としては、Urho
のApplication.ResourceCache
の参照を他クラスで保持しないようにしたら回避できました。
以下はクラッシュするコードの抜粋です。 xamarin/urho-sampleをもとに作っています。
//アプリケーション本体クラス public class Charts : Application { Scene scene; Node plotNode; List<Bar> bars; BarFactory barFactory; [Preserve] public Charts(ApplicationOptions options = null) : base(options) { //NG: ここでResourceCacheを渡して参照保持させる barFactory = new BarFactory(ResourceCache); //OK: ResourceCacheはあとで必要なときに渡す //barFactory = new BarFactory(null); } protected override void Start () { base.Start (); CreateScene (); //... } async void CreateScene () { //... scene = new Scene (); plotNode = scene.CreateChild(); //... int size = 3; bars = new List<Bar>(size * size); //NG: ここではResourceCacheを渡さない(コンストラクタで渡しておいて使わせる) barFactory.AddBarsToScene(plotNode, bars, size); //OK: この時点でResourceCacheを渡す barFactory.AddBarsToScene(plotNode, bars, size, ResourceCache); //... } } //シーンの一部をつくる別クラス internal class BarFactory { public BarFactory(ResourceCache resources) { _resources = resources; } private readonly ResourceCache _resources; //NG: 実行時にはResourceCacheを渡さない(コンストラクタで渡されたものを使う) public void AddBarsToScene(Node plotNode, Listbars, int size) { //リファクタリングされたコード //※ここでテクスチャ等を得るためにResourceCacheを使ってる想定 for (var i = 0f; i < size * 1.5f; i += 1.5f) { for (var j = 0f; j < size * 1.5f; j += 1.5f) { var boxNode = plotNode.CreateChild(); boxNode.Position = new Vector3(size / 2f - i, 0, size / 2f - j); var box = new Bar(new Color(RandomHelper.NextRandom(), RandomHelper.NextRandom(), RandomHelper.NextRandom(), 0.9f)); boxNode.AddComponent(box); box.SetValueWithAnimation((Math.Abs(i) + Math.Abs(j) + 1) / 2f); bars.Add(box); } } } //OK: 実行時にResourceCacheを渡す public void AddBarsToScene(Node plotNode, List bars, int size, ResourceCache resources) { //リファクタリングされたコード //※ここでテクスチャ等を得るためにResourceCacheを使ってる想定 for (var i = 0f; i < size * 1.5f; i += 1.5f) { for (var j = 0f; j < size * 1.5f; j += 1.5f) { var boxNode = plotNode.CreateChild(); boxNode.Position = new Vector3(size / 2f - i, 0, size / 2f - j); var box = new Bar(new Color(RandomHelper.NextRandom(), RandomHelper.NextRandom(), RandomHelper.NextRandom(), 0.9f)); boxNode.AddComponent(box); box.SetValueWithAnimation((Math.Abs(i) + Math.Abs(j) + 1) / 2f); bars.Add(box); } } } }
動くコードが欲しい方はGitHubへ。 上記コードに相当するのはChart.csとBarFactory.csです。 クラッシュの有無はAndroidでのみ確認しています。
蛇足ですが、今回のトラブルを起こす「リソースキャッシュの参照保持」は リファクタリングのつもりでやった変更で、裏目に出たのはやや残念です…。
3. 推定原因
回避策が分かったところで原因推定です。 Urhoのソースは読んでないので、単なる当てずっぽうです。 興味ある方は調べてみてください。
- マネージ: 画面をアンロードする
- マネージ: ユーザーコードから
UrhoSurface.OnDestroy()
を実行1 - ネイティブ:
OnDestroy
の実行時点でシーン破棄し、リソースキャッシュを削除 - マネージ:
Application
と関係ない所にResourceCache
の参照がまだあるので、リソースキャッシュの削除に気づかない - (いったん別ページに遷移)
- マネージ: ふたたび
UrhoSurface
を使う画面をロード - マネージ:
Application
を立ち上げなおしてResourceCache
を取りに行くが、ここでリソースキャッシュがまだ生きていると思い込んでるので、前回確保したリソースキャッシュを使おうとする - ネイティブ: 解放済みのメモリを叩かされてSEGV
4. まとめ
…いやまとめ以前に「issueで上げろよ」って話なんですが。 どうもページ遷移クラッシュの話題はスルーされてそうだし、 個人的には回避策で割と納得しちゃったのでモチベが無いというか…。
他力本願になりますが、やる気ある方は是非どうぞ。
-
抜粋したコードには含めていませんが、xamarin/urho-samplesで実装を見ると
Page
のOnDisappearing
でUrhoSurface.OnDestroy()
を呼んでいます。 ↩