Bakulog

獏の夢日記的な何か。

Xamarin.Forms+UrhoでResourceCacheを下手に取り扱うとクラッシュする

バグ?と回避方法の紹介です。

  1. アプリがクラッシュする状況
  2. 回避策
  3. 推定原因
  4. まとめ

1. アプリがクラッシュする状況

本記事を読んでいる方と似ているかもしれません。 まずはざっくりとした経緯から。

そして私の場合、アプリのページ構成としては最上位にNavigationPageを使い、 その下でContentPageを遷移させるようにしていました。

  • MainPage : NavigationPage
    • StartPage: ここではUrhoSurface不使用
    • AppPage: 画面の一部としてUrhoSurfaceを使用

起動時にStartPageを表示し、StartPageAppPageを行き来できる構成です。

そしてアプリをクラッシュさせる手順と挙動がこちら。

  • アプリを起動→StartPageが表示
  • AppPageに移動→問題なくUrhoSurfaceにシーン表示
  • StartPageに戻る
  • 再度AppPageに移動→アプリがクラッシュ

実機Android(Oreo)で確認しました。 iOSやUWPでどうなるかは分かりません。

とくに困った点は、try-catchをすり抜けることです。 私はXamarinのデバッガの限界がよく分かってないので内部挙動もピンと来ず、 「Urho内部のネイティブコード中でSEGVしてる?」と憶測が立った程度でした。

2. 回避策

結論としては、UrhoApplication.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, List bars, 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.csBarFactory.csです。 クラッシュの有無はAndroidでのみ確認しています。

蛇足ですが、今回のトラブルを起こす「リソースキャッシュの参照保持」は リファクタリングのつもりでやった変更で、裏目に出たのはやや残念です…。

3. 推定原因

回避策が分かったところで原因推定です。 Urhoのソースは読んでないので、単なる当てずっぽうです。 興味ある方は調べてみてください。

  • マネージ: 画面をアンロードする
  • マネージ: ユーザーコードからUrhoSurface.OnDestroy()を実行1
  • ネイティブ: OnDestroyの実行時点でシーン破棄し、リソースキャッシュを削除
  • マネージ: Applicationと関係ない所にResourceCacheの参照がまだあるので、リソースキャッシュの削除に気づかない
  • (いったん別ページに遷移)
  • マネージ: ふたたびUrhoSurfaceを使う画面をロード
  • マネージ: Applicationを立ち上げなおしてResourceCacheを取りに行くが、ここでリソースキャッシュがまだ生きていると思い込んでるので、前回確保したリソースキャッシュを使おうとする
  • ネイティブ: 解放済みのメモリを叩かされてSEGV

4. まとめ

…いやまとめ以前に「issueで上げろよ」って話なんですが。 どうもページ遷移クラッシュの話題はスルーされてそうだし、 個人的には回避策で割と納得しちゃったのでモチベが無いというか…。

他力本願になりますが、やる気ある方は是非どうぞ。


  1. 抜粋したコードには含めていませんが、xamarin/urho-samplesで実装を見るとPageOnDisappearingUrhoSurface.OnDestroy()を呼んでいます。