AndroidアプリのAPKサイズを圧縮しようと試みて敗れる話【Xamarin.Forms / Linker / R8 Shrinker】

2021/03/13

目次 [隠す]

xamarin logo

本記事の概要

『Xamarin.Formsで開発したAndroidアプリのパッケージサイズを圧縮しようと「Linker」「d8/r8」コンパイラを駆使したが、敗北する』、です。

アプリのパッケージサイズが気になる

先日、初めて個人開発したスマホアプリ(Android版のみ)をリリースしました。

アラフォー初心者だけどスマホアプリを開発~リリースまでがんばってみた【Android・Xamarin.Forms】 | neputa note

この度、素人ながらスマホアプリ開発に挑戦してみました。 今回の記事では概要と経緯について書き綴ってみたいと思います。 実際に行った作業の詳細は、今後それぞれ記事を書き、こちらにリンクを追記します。作っ

blog card

こちらでインストールできます
※アンドロイド版のみです。iPhoneユーザの方すみません。
Google Play で手に入れよう

色々と不具合問題で話題となっている「COCOA - 新型コロナウイルス接触確認アプリ」の影響もあり、すっかり悪いイメージがついた「Xamarin.Forms」で開発しました!!

Xamarin.Forms は直接Android、iOSのAPIを叩いて実行するアプリを作れるので、ネイティブ開発と比べて特別に劣るということも無いとは思うのです。

思うのですが、monoランタイムを抱えていることもあり、パッケージサイズが大きくなりがちです。

Xamarinの基盤「Mono」のmonoランタイムとクラスライブラリ - Build Insider検索

インサイドXamarin(3)。Xamarinにおけるソフトウェアの基盤であるMonoを深く理解すれば、Xamarin製品の理解はもっと深まる。今回はmonoランタイムと、Monoのクラスライブラリに

blog card

現在リリースしているバージョン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

サイズを小さくする方法はいくつかあります。

1.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

blog card

2.Linker を使用する
Android でのリンク - Microsoft Docs

3.AOT&LLVM コンパイラを使用する
アプリケーションを保護する - Microsoft Docs

この中で、3番目のAOT&LLVMは、Visual StudioのEnterpriseエディションライセンスが必要なので、わたしは残念ながら利用できません。

では、最初の2つを使用すればいいじゃないかとなりますが、そうは簡単にはいかないのですね。

まずは、「R8 Shrinker」と「Linker」について、調べてみたことを簡単にまとめたいと思います。

R8 Shrinker

これは、Javaバイトコードを対象に未使用コードを削除してくれる機能です。
(ネイティブ開発と異なり、Xamarin.Forms では難読化の恩恵は得ることができません)

r8 shrinker

※ちなみにこれを書いている2021/3/12時点では、「R8」の他に「ProGuardを有効にする」という選択肢があります。 R8はProGuardを置き換える目的で開発されたもので、ProGuardを選択するとビルドで「R8」を使えと怒られます。

軽量化に役立つなら使えばいいじゃない、そう思うことでしょう。

なんの備えもなくこいつを選ぶとあら不思議、わたしのアプリは見事クラッシュします。

いろいろと対処をいないと使えないことがわかったので、必要事項を後述します。

Linker

これは、静的解析により不要と判断したコードをばっさり切り捨てることで軽量化を図る機能です。

オプションとしては、「一切使用しない(なし)」「SDKのみ対象とする(SDKアセンブリのみ)」「すべて対象(SDKおよびユーザアセンブリ)」の3つがあります。

linker

現在は、SDKのみを対象としており、わたしが書いたコードおよび追加したNuget Packageについては対象外となっています。

で、SDKとユーザアセンブリすべてを安易に選ぶとあら不思議、わたしのアプリは見事クラッシュします。

R8 Shrinker、Linkerどちらも何もせずに使えるわけではなく、導入するにはそれなりの準備が必要となります。

R8 Shrinker を使うために行った作業

まずは、Visual Studioのツール→Android→Android Device Monitorで、クラッシュ原因を見てみましょう。

crash

「FATAL」があるあたりを見てみると、こんなメッセージがあります。
java.lang.ClassNotFoundException: Didn't find class "com.google.android.gms.ads.MobileAdsInitProvider"

わたしのアプリには「Google AdMob」という広告表示用のプラグインがあるのですが、起動時にそんなもん見つかんねーよと言われておるのですね。

つまり、「R8 Shrinker」さん、「軽量化するため余計なコードぶった切ったけど、お前が追加したパッケージとかも切り捨ててやったよー」ということでしょうか。

R8 Shrinkerを使用していると、Androidプロジェクトフォルダ配下の「obj\Release\XXX\proguard」に「.cfg」拡張子のファイルがあります。(XXXは、お使いのエミュレータのバージョンが入ります)

これらのファイルを見てみると、「-keep class XXXX」という記述がずらりと並んでいます。

これは、コンパイル時に切り捨てずにキープ対象となるライブラリ名がずらりと書かれているのですね。

obj配下にあるファイルは自動生成されたものです。

これとは別に自分が追加したパッケージ等をkeepするために設定ファイルを用意する必要があります。

例えば「my_proguard_xamarin.cfg」というファイルをAndroidプロジェクトに追加し、「ビルドアクション」を「ProguardConfiguration」にしておきます。

こうすることで、ビルド時にこの設定ファイルを読んでくれるようになります。

ProGuard - Xamarin | Microsoft Docs

Xamarin.Android ProGuard は、Java クラス ファイルのシュリンカー、オプティマイザー、および事前検証機能です。 これは、未使用のコードを検出して削除し、バイトコードの分析と

blog card

ちなみに、自動生成された.cfgファイルをコピーして設定ファイルを作る場合、ファイル内のBOMがビルドエラーの原因となるので対応するエディタ等でBOMを削除する必要があります。

あとはひたすら、トライ&エラーです。

エラー原因となったライブラリを設定ファイルに追記し、Android Device Monitorで確認、また別のエラーが出たらそれを追記、そしてまた……。

わたしの場合、最初のAdMobに続いて、AdMobに関連する「com.google.unity.ads.UnityAdListener」、そして「androidx.work」、が原因ではじかれ、そのつど、ファイルにKeepを追加しました。

一番厄介だったのが、Splash screenのファイルに問題があるとエラーが出て、いろいろ調べた結果「Calligraphy」をアップデートしろという情報を見つけ対応したことです。

R8を使用していなければ特に問題は起きていなかったので、R8に関連してCalligraphyのバージョンが問題となるのかいまいち原因ははっきりせず。

Crash on Android 10 - stack overflow Calligraphy.Xamarin

結果として以下のような.cfgファイルを作成し、何とかアプリが起動するところまでたどり着きました。

-keep class com.google.unity.** {
  *;
}

-keep public class com.google.android.gms.ads.**{
public *;
}

-keep public class com.google.ads.**{
public *;
}

-keep class androidx.work.** { *; }

-keepattributes Annotation

R8については以上となります。

Linkerでユーザアセンブリも対象にする

続いて、Linker。

もっともパッケージ圧縮の恩恵が大きいのは「SDKおよびユーザアセンブリ」を選択すること。

しかしこちらもR8 Shrinker同様、必要な設定を施さないと、わたしの場合はアプリが見事にクラッシュしました。

行う作業も同様で、Linkerで切り捨ててほしくないライブラリ等を設定ファイルに追加していきます。

カスタム リンカーの構成 - Xamarin | Microsoft Docs

このドキュメントでは、必要なコードがリンクされているアプリケーションから削除されないことを明示的に確認し、リンカーを構成するために使用できる XML ファイルについて説明します。

blog card

Linkerの設定はXMLファイルに記述します。

とりあえず「LinkerSettings.xml」という名前のファイルをAndroidプロジェクトに追加し、ファイルプロパティのビルドアクションを「LinkDescription」にしておきます。

わたしの場合はこんな感じになりました。

使用しているNuget Packageと、作成したプロジェクトアセンブリが対象となっています。

<?xml version="1.0" encoding="utf-8" ?>
<linker>
  <!--
      For more information see the docs on creating custom Linker Settings
      https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/linker
  -->
  <assembly fullname="Essential.Interfaces">
    <type fullname="Xamarin.Essentials.Implementation.AppInfoImplementation">
      <method name=".ctor" />
    </type>
  </assembly>

  <assembly fullname="Prism.Forms">
    <type fullname="Prism.Common.ApplicationProvider" preserve="all" />
    <type fullname="Prism.Services.PageDialogService" preserve="all" />
    <type fullname="Prism.Services.DeviceService" preserve="all" />
    <type fullname="Prism.Ioc*" preserve="all" />
    <type fullname="Prism.Modularity*" preserve="all" />
    <type fullname="Prism.Navigation*" preserve="all" />
    <type fullname="Prism.Behaviors.PageBehaviorFactory" preserve="all">
      <method name=".ctor" />
    </type>
    <type fullname="Prism.Services.DependencyService" preserve="all">
      <method name=".ctor" />
    </type>
  </assembly>

  <assembly fullname="Prism">
    <type fullname="Prism.Navigation*" preserve="all" />
    <type fullname="Prism.Logging.EmptyLogger" preserve="all">
      <method name=".ctor" />
    </type>
  </assembly>

  <assembly fullname="Unity.Abstractions">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Unity.Container">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Prism.Unity.Forms">
    <type fullname="*" />
  </assembly>

  <assembly fullname="System">
    <type fullname="*" />
  </assembly>

  <assembly fullname="mscorlib">
    <type fullname="*" />
  </assembly>

  <assembly fullname="OneThird.Core">
    <type fullname="*" />
  </assembly>

  <assembly fullname="OneThird.Application">
    <type fullname="*" />
  </assembly>

  <assembly fullname="OneThird.Domain">
    <type fullname="*" />
  </assembly>

  <assembly fullname="OneThird.Infrastructure">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Microsoft.Identity.Client">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Realm">
    <type fullname="*" />
  </assembly>

  <assembly fullname="System.IdentityModel.Tokens.Jwt">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Xamarin.CommunityToolkit">
    <type fullname="*" />
  </assembly>

  <assembly fullname="Xamarin.GooglePlayServices.Ads" >
    <type fullname="*" />
  </assembly>

</linker>

よし、これでしまいかと思いきや……。

見事にクラッシュします。

で、色々と調べているとLinkerの対象から外すために「Preserve属性を追加せよ」という情報を目にします。
Using The Linker In Xamarin Projects

たいへん面倒ではありますが、以下のように属性を付けて回ることにします。

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/月とか無理っすよ……。

それとも何かいい方法があるのでしょうか。

もしご存じな親切な方いらっしゃいましたらコメントやTwitterなどで教えていただけますと、朝晩そちらに向かって毎日かかさず感謝の祈りを捧げたりします。

以上、プログラミングは祈りだ、の巻きでした。