AndroidアプリのAPKサイズを圧縮しようと試みて敗れる話
初稿:
更新:
- 13 min read -
本記事の概要
Xamarin.Formsで開発したAndroidアプリのパッケージサイズを圧縮しようと「Linker」「d8/r8」コンパイラを駆使したが、敗北した一連の顛末ををまとめた。
アプリのパッケージサイズが気になる
アラフォー初心者だけどスマホアプリを開発~リリースまでがんばってみた【Android・Xamarin.Forms】
あらすじ この度、素人ながらスマホアプリ開発に挑戦してみた。今回の記事では概要と経緯について書き綴ってみたい。実際に行った作業の詳細は、全7回に分けた記事を別途作成
インストールはこちらから。
色々と不具合問題で話題となっている「COCOA - 新型コロナウイルス接触確認アプリ」の影響もあり、すっかり悪いイメージがついた「Xamarin.Forms」で開発した。
Xamarin.Formsは直接Android、iOSのAPIを叩いて実行するアプリを作れる。よって、ネイティブ開発と比べて特別に劣るということもないとは思う。
思うが、monoランタイムを抱えていることもあり、パッケージサイズが大きくなりがち。
Xamarinの基盤「Mono」のmonoランタイムとクラスライブラリ
インサイドXamarin(3)。Xamarinにおけるソフトウェアの基盤であるMonoを深く理解すれば、Xamarin製品の理解はもっと深まる。今回はmonoランタイムと、Monoのクラスライブラリについて解説する。
現在リリースしているバージョン1.0.4の時点で、ダウンロードサイズは34MB。インストールしたアプリサイズは49.45MB。アプリの内容を考えると大きい。
これをどうにかできないかと悪戦苦闘し、敗れる(つまり未解決)という、残念な内容な記事とっている。
情報価値はゼロとは思うが、暇つぶしにお付き合いいただけたらうれしい。
Xamarin.Forms だってダイエットしたい
参考にしたサイトはこちら。
- Xamarin.Forms - Android App Performance and Package Size Reduction
- Reducing iOS and Android App Size in Xamarin
サイズを小さくする方法はいくつかある。
- R8 Shrinkerを使用する
Android's D8 dexer and R8 shrinker - Xamarin Blog
Learn more about Xamarin.Android’s D8 and R8 integration and deep dive on how R8 is being developed for Android and D8 as the next-generation DEX compiler.
- Linkerを使用する
Xamarin.Android アプリケーションを使用して、サポート情報を含むアプリケーションのサイズをリンカーを使って小さくする方法について説明します。
- AOT&LLVMコンパイラを使用する
リリースに向けてアプリケーションを準備する - Xamarin
アプリケーションのコードを記述し、テストをしたら、配布用にパッケージを用意する必要があります。 リリース用にアプリをビルドする手順について確認します。
この中で、3番目のAOT&LLVMは、Visual StudioのEnterpriseエディションライセンスが必要。よってわたしは残念ながら利用できない。
では、最初の2つを使用すればいいじゃないかとなるが、そうは簡単にはいかない。
まずは「R8 Shrinker」と「Linker」について、調べてみたことを簡単にまとめたい。
R8 Shrinker
これは、Javaバイトコードを対象に、未使用コードを削除してくれるもの。
ただし、ネイティブ開発と異なり、Xamarin.Formsでは難読化の恩恵は得ることができない。
軽量化に役立つなら使えばいいじゃない、そう思うことだろう。
なんの備えもなくこいつを選ぶとアプリは見事クラッシュする。
いろいろと対処をしないと使えないことがわかった。その理由と対処については後述する。
Linker
これは、静的解析により不要と判断したコードをばっさり切り捨てることで軽量化を図る機能。
オプションとして、「一切使用しない(なし)」「SDKのみ対象とする(SDKアセンブリのみ)」「すべて対象(SDKおよびユーザアセンブリ)」の3つがある。
現在は、SDKのみを対象としており、わたしが書いたコードおよび追加したNuget Packageについては対象外となっている。
で、「SDKとユーザアセンブリすべて」を安易に選ぶとアプリは見事クラッシュする。
R8 Shrinker、Linker、どちらも何もせずに使えるわけではなく、導入するにはそれなりの準備が必要なのだ。
R8 Shrinker を使うために行った作業
まずは、Visual Studioのツール→Android→Android Device Monitorで、クラッシュ原因を見てみる。
「FATAL」があるあたりを見てみると、こんなメッセージがある。
わたしのアプリには「Google AdMob」という広告表示用のプラグインがあるが、起動時にそんなもん無いと言われている。
つまり、「R8 Shrinker」は、私が追加したnugetパッケージを不要コードとみなし、バッサリ削ったのだ。
R8 Shrinkerを使用していると、Androidプロジェクトフォルダ配下の「obj\Release\XXX\proguard」に「.cfg」拡張子のファイルが作られる。(XXXは、お使いのエミュレータのバージョンが入る)
これらのファイルを見てみると、「-keep class XXXX」という記述がずらりと並んでいる。
これは、コンパイル時に切り捨てずキープ対象となるライブラリ名がずらりと書かれているのだ。
obj配下にあるファイルは自動生成されたもの。
これとは別に、自分が追加したパッケージ等をkeepするため、設定ファイルを用意する必要がある。
たとえば「my_proguard_xamarin.cfg」というファイルをAndroidプロジェクトに追加し、「ビルドアクション」を「ProguardConfiguration」にしておく。
こうすることで、ビルド時にこの設定ファイルを読んでくれるようになる。
Xamarin.Android ProGuard は、Java クラス ファイルのシュリンカー、オプティマイザー、および事前検証機能です。 これは、未使用のコードを検出して削除し、バイトコードの分析と最適化を行います。 このガイドでは、ProGuard がどのように機能するか、プロジェクトで有効にする方法、および設定方法について説明します。 また、ProGuard の設定例もいくつか示します。
あとはひたすら、トライ&エラー。
エラー原因となったライブラリを設定ファイルに追記し、Android Device Monitorで確認、また別のエラーが出たらそれを追記、そしてまた……。
わたしの場合、最初のAdMobに続いてAdMobに関連する「com.google.unity.ads.UnityAdListener」、そして「androidx.work」が原因ではじかれ、その都度ファイルにKeepを追加した。
一番厄介だったのが、Splash screenのファイルに問題があるとエラーが出て、いろいろ調べた結果「Calligraphy」をアップデートしろという情報を見つけ対応したこと。
R8を使用していなければとくに問題は起きていなかったため、R8に関連してCalligraphyのバージョンが問題となるのかいまいち原因ははっきりしなかった。
Crash on Android 10 (InflateException in layout/abc_screen_simple line #17)
My application works fine from Android 4.3 until Android 9 Pie, but my application doesn't work on Android 10 (Q API 29) and crashes. This is my logcat - why this is happening?java.lang.
Use custom fonts in Android! Port of https://github.com/InflationX/Calligraphy - nwestfall/Calligraphy.Xamarin
結果として以下のような .cfgファイルを作成し、何とかアプリが起動するところまで漕ぎつけた。
R8については以上となる。
Linkerでユーザアセンブリも対象にする
続いて、Linker。
もっともパッケージ圧縮の恩恵が大きいのは「SDKおよびユーザアセンブリ」を選択すること。
しかしこちらもR8 Shrinker同様、必要な設定を施さないと、わたしの場合はアプリが見事クラッシュした。
行う作業も同様で、Linkerで切り捨ててほしくないライブラリ等を設定ファイルに追加する。
このドキュメントでは、必要なコードがリンクされているアプリケーションから削除されないことを明示的に確認し、リンカーを構成するために使用できる XML ファイルについて説明します。
Linkerの設定はXMLファイルに記述する。
とりあえず「LinkerSettings.xml」という名前のファイルをAndroidプロジェクトに追加し、ファイルプロパティのビルドアクションを「LinkDescription」にしておく。
わたしの場合はこんな感じになった。
使用しているNuget Packageと、作成したプロジェクトアセンブリが対象となっている。
よし、これでしまいかと思いきや……。
見事にクラッシュする。
で、色々と調べていると、Linkerの対象から外すために「Preserve属性を追加せよ」という情報を見つけた。
たいへん面倒ではあるが、以下のように属性を付けて回ることにした。
- Androidプロジェクトのすべてのクラス
- [Android.Runtime.Preserve(AllMembers = true)]
- 共通プロジェクトのすべてのクラス
- [Xamarin.Forms.Internals.Preserve(AllMembers = true)]
ここまでやって、ようやく、ようやくアプリが起動した。
だが、これでは終わらない……
無事起動した。
しかし物語は常にハッピーエンドとは限らない。
動作確認をすると、CosmosDBの接続でエラーが出る、広告が表示されない、などいくつかの不具合が見つかった。
「ンあーーーーーーっ」と叫びたい気持ちを抑え、またひとつひとつ潰していくかと頭を切り替えようと思った。
だが冷静になり、この時点でどれほどパッケージサイズは小さくなっているのだろうと確認すると、わずか「3MB」……。
これだけやって、こんな程度か、とまず脱力。
そして、「エラーを潰す = 削除されたコードを残すようにする」わけだ。
ここからさらにパッケージサイズは大きくなる。
また将来的なことも考えてみる。
この先、きっと機能追加等でコードやNuGetを追加したりするだろう。
そのたびに、今回の作業を忘れず行う必要がある。
アプリサイズが少しでも小さいほうがユーザにとって良いこと。
だが、コストやリスクに対し、メリット少なすぎやしないか。
涙の結論
ということで、「R8 Shrinker」および「フルLinker」は、めっちゃ頑張り、すごく悔しいが、あきらめることとした。
ダウンロードしてくれるユーザの皆さまのギガを奪って申しわけない。
wi-fiがある場所でダウンロードしたりアップデートしてくれることを祈っている。
技術の話なのに最後は祈りだ。
Visual Studioのエンタープライズエディションゲットして「AOT&LLVM」使えば楽にちっさくなったりするのだろうか。
でも$250/月とか個人開発には厳しい。
ほかに何か良い方法は無いだろうか。
もしご存じな方がいらっしゃったら教えていただけると、朝晩そちらに向かって毎日かかさず感謝の祈りを捧げることをお約束する。
以上、プログラミングは祈り、の巻き。