365連休

にわかのandroidとかの開発メモ。

【解決】Androidアプリをストアからインストールすると、Navigationのタイミングで、必ずjava.lang.RuntimeException Unknown animation name: xが発生し異常終了する。

前提条件

  • App Bundle使用
  • minifyEnabled true
  • shrinkResources true
  • compileSdkVersion 29
  • minSdkVersion 16
  • targetSdkVersion 29
  • multiDexEnabled true
  • androidx.navigation:navigation-fragment:2.3.2
  • androidx.navigation:navigation-ui:2.3.2
  • FragmentContainerView使用

 

発生状況

前提条件を全て適用したデバッグビルドをインストール->実機&エミュレータ問題無し

App Bundleをストアからインストール->必ず異常終了

 

Play Consoleのクラッシュログ

Play Consoleのクラッシュログは以下の通り。

java.lang.RuntimeException: Unknown animation name: x
  at android.view.animation.AnimationUtils.createAnimationFromXml (AnimationUtils.java:160)
  at android.view.animation.AnimationUtils.createAnimationFromXml (AnimationUtils.java:127)
  at android.view.animation.AnimationUtils.loadAnimation (AnimationUtils.java:108)
  at androidx.fragment.app.FragmentAnim.loadAnimation (FragmentAnim.java:2)
  at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState (FragmentManager.java:6)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java)
  at androidx.fragment.app.FragmentManager.executeOpsTogether (FragmentManager.java:19)
  at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute (FragmentManager.java)
  at androidx.fragment.app.FragmentManager.execPendingActions (FragmentManager.java:4)
  at androidx.fragment.app.FragmentManager$4.run (FragmentManager.java)
  at android.os.Handler.handleCallback (Handler.java:751)
  at android.os.Handler.dispatchMessage (Handler.java:95)
  at android.os.Looper.loop (Looper.java:159)
  at android.app.ActivityThread.main (ActivityThread.java:6139)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:886)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:776)

 

疑い

xは難読化によって化けたクラス名と思っていたが、調べてみるとanimation name: xというのはAnimation XMLリソース(Viewアニメーション)のルートタグであることが判明

以下AnimationUtils#createAnimationFromXmlのソース

    //前略
    if (name.equals("set")) {
        anim = new AnimationSet(c, attrs);
        createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
    } else if (name.equals("alpha")) {
        anim = new AlphaAnimation(c, attrs);
    } else if (name.equals("scale")) {
        anim = new ScaleAnimation(c, attrs);
    }  else if (name.equals("rotate")) {
        anim = new RotateAnimation(c, attrs);
    }  else if (name.equals("translate")) {
        anim = new TranslateAnimation(c, attrs);
    } else if (name.equals("cliprect")) {
        anim = new ClipRectAnimation(c, attrs);
    } else {
        throw new RuntimeException("Unknown animation name: " + parser.getName());
    }
    //後略

つまり、フラグメント切り替え時に、プロパティxを操作するアニメーションを行おうとして失敗している。

Viewアニメーションはset、alpha、scale、rotate、translateしか設定できないはずである。

  • ProGuardの設定ミス?
  • FragmentContainerViewのバグ? (←_ ←)アヤシイ
  • App Bundleのバグ?

 

解決

App Bundleで生成したapp-release.aabファイルをanalyzeに掛け、アニメーションリソースのルートタグがxになっているものを探してみると、

 

f:id:neet_rookie:20210116145323p:plain

一部アニメーションがxタグになっている
<?xml version="1.0" encoding="utf-8"?>
<x />

 

なんじゃこりゃ?

ファイルサイズが11byteになっているものは全て、ルートタグがxの空っぽのアニメーションファイル風になっている。

 

ちなみに元ファイルはこちら

 

f:id:neet_rookie:20210116145441p:plain

透明度を変化させるアニメーションが入っている

 

 

そこでアプリで設定しているProGuardを確認してみると

#NavigationのレイアウトにFragmentContainerViewを使用した場合、NavHostFragmentがClassNotFoundException
-keep class * extends androidx.fragment.app.Fragment{}
-keep class androidx.navigation.* {*;}

1行目にFragment継承クラスを全て保持するルールが書いてあるが、

これはFragmentContainerViewがリフレクションを使ってるせいでうまく遷移できなくなるのを防ぐルール。替わりに遷移先を全てkeepしても良い。

そして、2行目はなんかしらんけどどっかで見た、FragmentContainerViewを動かすためのルール。

とにかく、FragmentContainerViewはリフレクションを使いまくっているみたいで、世話が焼ける。

 

ここで、ふと

「2行目がandroidx.navigation.*になってるからサブパッケージが保持されていないじゃん!」

と啓示を得て、以下の通り変更。

 

#NavigationのレイアウトにFragmentContainerViewを使用した場合、NavHostFragmentがClassNotFoundException
-keep class * extends androidx.fragment.app.Fragment{}
#noinspection ShrinkerUnresolvedReference
-keep class androidx.navigation.** {*;}

 ※ *を**にするとLintがそんなルール無いっていうので、noninspectionしてる。

 

これで、サブパッケージまで全て保持するように変更できたので、App Bundleでビルドをしてみると…

 

f:id:neet_rookie:20210116145538p:plain

無事、アニメーションが保持された

 

わーい

これで枕を高くして寝れる!

 

 

まとめ

「FragmentContainerView」+「コード圧縮」の組み合わせには気を付けよう。

 

正常動作に成功したProGuardのルール

#NavigationのレイアウトにFragmentContainerViewを使用した場合、NavHostFragmentがClassNotFoundException
-keep class * extends androidx.fragment.app.Fragment{}
#noinspection ShrinkerUnresolvedReference
-keep class androidx.navigation.** {*;}

 

 

 

 

 

 

あとがき

Replace the <fragment> tag with FragmentContainerView.

 FragmentContainerViewを使えってLint警告してくるくせにポンコツ

 

 

Xタグによる置き換えはApp Bundleでのみ確認できた。

ファイルを削除する替わりに中身を抜いて未使用のファイルサイズを減らす試みだろうか?

 

っていうかApp BundleとAPKでリソースの中身変えるのをやめて欲しい。

ビルドオプションが同じなら同等のものを生成して欲しい。