世界最小のapkを作る

以下は[Playing APK Golf](https://fractalwrench.co.uk/posts/playing-apk-golf-how-low-can-an-android-app-go/)の日本語訳です。

# Playing APK Golf

## Reducing an Android APK's size by 99.99%

ゴルフでは、最も得点の小さい者が勝利します。

この原則をAndroidにも適用しましょう。
apkのファイルサイズを減らし、Oreoで実行できる最小サイズのアプリを作成するのです。

## Measuring a Baseline

Android Studioで生成されたデフォルトのアプリから開始しましょう。
[キーストアを生成](https://developer.android.com/studio/publish/app-signing.html#generate-key)し、アプリに署名し、そして`stat -f%z $filename.`コマンドでファイルサイズをバイト単位で測定します。

また、Oreoが動いているNexus5にapkをインストールし、アプリが実際に動作することも確認します。

![01.png](https://qiita-image-store.s3.amazonaws.com/0/26088/fcd07a55-0eca-1885-54fa-b6a4abb1c7b7.png)

すばら！
apkのファイルサイズは約1.5メガバイトでした。

## APK Analyser

アプリの内容に対して、1.5メガバイトの容量はあまりに大きすぎます。
Android Studioが何を生成したのか、プロジェクトを詳しく調査していきましょう。

・`AppCompatActivity`を継承した`MainActivity`。
・`ConstraintLayout`で作られたレイアウトファイル。
・3つの色、1つの文字列、1つのテーマの入ったリソースファイル。
・`AppCompat`と`ConstraintLayout`のサポートライブラリ。
・`AndroidManifest.xml`
・ランチャーのアイコンpngファイル。

おそらく最も容易なターゲットはアイコン画像でしょう。
なにしろ`mipmap-anydpi-v26`の下に全部で15もの画像と2個のXMLファイルが存在します。
Android Studio付属の[APK Analyser](https://developer.android.com/studio/build/apk-analyzer.html)で確認してみます。

![02.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/bb28e322-bc63-364b-7c1d-329d9db24e46.jpeg)

予想に反して`classes.dex`ファイルが最も大きく、リソースはapkのわずか20%に過ぎませんでした。

| File | Size |
|---|---|---|
| classes.dex | 74% |
| res | 20% |
| resources.arsc | 4% |
| META-INF | 2% |
| AndroidManifest.xml | <1% |

各ファイルがそれぞれ何をしているのか、順に見ていきましょう。

### Dex file

`classes.dex`は全容量の74%を占有している最大の犯人で、従って我々の最初のターゲットです。
ここには我々が書いたコードと、その他のAndroidフレームワークやサポートライブラリなどがまとめて[Dexフォーマット](https://source.android.com/devices/tech/dalvik/dex-format)で格納されています。

`android.support`パッケージは13000以上のメソッドに対応していますが、これは我々が作成した`Hello world`アプリには少々過剰なように思えます。

### Resources

`res`ディレクトリには、Android Studioでは表示されていなかった膨大な数のレイアウト、drawable、アニメーションファイル等が存在します。
これらはサポートライブラリから取り込まれたものであり、apkのサイズの20%を占めています。

![03.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/53fcc6ba-5cf2-0f65-d47b-7c0370dda600.jpeg)

ファイル`resources.arsc`は、これら各リソースの一覧ファイルです。

### Signing

`META-INF`ディレクトリには`CERT.SF`、`MANIFEST.MF`、および`CERT.RSA`が入っています。
これらは[v1 APK署名](https://source.android.com/security/apksigning/v2#v1-verification)であり、もし攻撃者がapkを改変した場合、apkと署名が一致しなくなります。
これにより、apkはマルウェアによる汚染から保護されることになります。

`MANIFEST.MF`はapk内のファイル一覧です。
`CERT.SF`はmanifestおよび各ファイルのダイジェストが入っています。
`CERT.RSA`には、`CERT.SF`の完全性を検証するための公開鍵が含まれています。

![04.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/a82b6031-e2fb-228f-ae5c-0df78b52a738.jpeg)

ここをどうにかするのは難しそうです。

### AndroidManifest

AndroidManifestはapk作成前のものとほとんど同じです。
唯一の違いは、文字列やdrawableなどのリソースが`0x7F`で始まるリソースIDに置き換えられていることです。

## Enable minification

最初にapkを作成したときは、`build.gradle`での最適化やリソースの縮小を有効にしていませんでした。
まずはこれを行ってみましょう。

```groovy
	android {
	    buildTypes {
	        release {
	            minifyEnabled true
	            shrinkResources true
	            proguardFiles getDefaultProguardFile(
	              'proguard-android.txt'), 'proguard-rules.pro'
	        }
	    }
	}
```

```bash
	-keep class com.fractalwrench.** { *; }
```

`minifyEnabled`は[Proguard](https://www.guardsquare.com/en/proguard)を有効にします。
これは未使用コードを削除します。
またシンボル名を難読化するので、アプリのリバースエンジニアリングが難しくなります。

`shrinkResources`は、apkから直接参照されないリソースを削除します。
リフレクション等を使って間接的にアクセスしていた場合は問題になりますが、今回のアプリでは使ってないので問題ありません。

## 786 Kb (50% reduction)

apkのサイズが半分になりました。
アプリの動作は特に影響ありません。

![05.png](https://qiita-image-store.s3.amazonaws.com/0/26088/fff2570f-b0eb-5fd0-9f41-a3c094d7085b.png)

もし作成しているアプリがあるのであれば、`minifyEnabled`と`shrinkResources`は真っ先に有効にしましょう。
たったそれだけで容量を簡単に数メガバイト減らすことができます。
この設定とテストにはほんの数時間しかかからないでしょう。

## AppCompat, we hardly knew ye

上記によって`classes.dex`はapkの57%にまで減りました。
Dexの容量の多くは`android.support`パッケージに属しているので、サポートライブラリを使わないようにします。

・`build.gradle`からdependenciesブロックを削除する。

```groovy
	dependencies {
	    implementation 'com.android.support:appcompat-v7:26.1.0'
	    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
	}
```

・`MainActivity`は`android.app.Activity`を直接継承する。

```java
	public class MainActivity extends Activity
```

・レイアウトはTextViewにする。

```xml
	<?xml version="1.0" encoding="utf-8"?>
	<TextView xmlns:android="http://schemas.android.com/apk/res/android"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent"
	    android:gravity="center"
	    android:text="Hello World!" />
```

・AndroidManifestの`application`要素から`styles.xml`と`android:theme`を削除する。 
・`colors.xml`も削除する。
・Gradleの[sync中](http://kittoworks.hateblo.jp/entry/2015/08/01/222500)に50回腕立て伏せする。

## 108 Kb bytes (87% reduction)

なんてこった！786Kbから108Kbまで、一気に約10倍の減量に成功してしまったぞ。
アプリ唯一の目に見える変化は、ツールバーの色がOSのデフォルトテーマに変わったところです。

![06.png](https://qiita-image-store.s3.amazonaws.com/0/26088/4965a184-4408-662a-96d8-a5a94949227a.png)

この時点で、`res`ディレクトリがapkサイズの95%を占めるまでになりました。

アイコンのPNGは、APIレベル15以上であればもっと効率の良い[WebP形式](https://developer.android.com/studio/write/convert-webp.html)に対応しています。

Googleはdrawableを自動的に最適化しますが、[ImageOptim](https://imageoptim.com/mac)を使ってpngファイルから不要なメタデータを削除することもできます。

ここではもっと効率の良い対応を行いましょう。
すなわち、`res/drawable`以下のアイコンファイルを1ピクセルの黒点画像に差し替えます。
この画像は67バイトでした。

## 6808 bytes (94% reduction)

ほぼ全てのリソースを取り除いたので、apkサイズが1/20になったとしても特に驚くことではありません。
`resources.arsc`は以下のファイルを参照しています。
・1つのレイアウトファイル
・1つの文字列リソース
・1つのアイコン

順に処理していきましょう。

### Layout file (6262 bytes, 9% reduction)

AndroidフレームワークはテンプレートのXMLファイルを自動的に`TextView`に[インフレート](https://developer.android.com/reference/android/view/LayoutInflater.html)し、`Activity`の`contentView`にセットします。

XMLファイルを削除し、`contentView`をプログラムから直接設定することで、このAndroidからの干渉をスキップできます。
XMLファイルを減らせたのでリソースサイズは減少しますが、そのかわりDexファイルの容量が増加します。
`TextView`を参照するソースを追加したためです。

```android
	TextView textView = new TextView(this);
	textView.setText("Hello World!");
	setContentView(textView);
```

このトレードオフはうまくいったように思われ、容量を5710バイトまで削ることができました。

## App Name (6034 bytes, 4% reduction)

`strings.xml`はいらないので削除して、AndroidManifestの`android:label`と書かれているところは直接'A'に入れ替えます。
これは小さな変更に見えますが、実際は`resources.arsc`からエントリが消え、AndroidManifestの文字列も減り、resディレクトリからファイル自体も削除されます。
これによって228バイトの削減に成功しました。

## Launcher icon (5300 bytes, 13% reduction)

[resources.arscのドキュメント](https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/README)によると、apkの各リソースは、`resources.arsc`によって整数のIDで管理されていることが示されています。
このIDは2個のネームスペースを持っています。

```text
	0x01: system resources (システムによって予めインストールされているもの)
	0x7f: application resources (アプリのapkでインストールされるもの)
```

つまり、`0x01`名前空間のリソースを参照することで、自前でアイコンを用意する必要がなくなり、ファイルサイズをさらに縮小することができます。

```xml
	android:icon="@android:drawable/btn_star"
```

![07.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/248c4ee1-7ec9-4df5-8713-53e4b0ed4c7c.jpeg)

言うまでもありませんが、プロダクションアプリではこのようなことをしてはいけません。
この手法はGoogle Playでの検証に失敗します。
また一部の端末では[仕様が勝手に変更されていることがある](https://www.reddit.com/r/androiddev/comments/71fpru/android_color_resources_not_safe/)ので、使用には注意してください。

## Manifest (5252 bytes, 1% reduction)

AndroidManifestの最適化には、まだろくに手を付けていません。

```xml
android:allowBackup="true"
android:supportsRtl="true"
```

不要な属性を削除して、48バイトを節約しました。

## Proguard hack (4984 bytes, 5% reduction)

Dexファイルの中に`BuildConfig`と`R`が未だ含まれているように見えます。

```
	-keep class com.fractalwrench.MainActivity { *; }
```

Proguardのルールを見直して、これら不要なクラスを削除しました。

## Obfuscation (4936 bytes, 1% reduction)

`Activity`にも難読化を行いましょう。
Proguardは通常のクラスに対してはデフォルトで行ってくれますが、`Activity`については、インテントで呼び出す都合上難読化しません。


```
	MainActivity -> c.java
	com.fractalwrench.apkgolf -> c.c
```

## META-INF (3307 bytes, 33% reduction)

これまで、apkの署名を[v1とv2](https://qiita.com/ntsk/items/ce655a804654dac33d73)の両方で行っていました。
v2はapk全体をハッシュすることによって、[優れた保護機能とパフォーマンス](https://source.android.com/security/apksigning/#apk-signing-schemes)を提供します。

v2の署名はapkファイル内にバイナリとして含まれているため、apkアナライザには表示されません。
v1の署名は`CERT.RSA`および`CERT.SF`という実体ファイルで存在します。

Android Studioでv1署名のチェックを外し、v2のみ署名されたapkを作成しましょう。
逆にv1のみのapkも作成してみます。

| Signature | Size |
|---|---|---|
| v1 | 3511 |
| v2 | 3307 |

v2のほうがサイズが小さいので、そちらを採用することにします。

## Where we’re going, we don’t need IDEs

ついにIDEを窓から投げ捨てるときが来ました。
今後apkは手動で編集することにします。

```
	# 1. 署名のないapkを作成
	./gradlew assembleRelease

	# 2. apkを解凍する
	unzip app-release-unsigned.apk -d app

	# なにか編集

	# 3. zip圧縮する
	zip -r app app.zip

	# 4. zipalign
	zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk

	# 5. v2署名する
	apksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk

	# 6. 署名の確認
	apksigner verify signed-release.apk
```

署名の詳細については[こちら](https://developer.android.com/studio/publish/app-signing.html#sign-manually)を参照してください。
簡単にまとめると、gradleで署名のないapkを作成し、zipalignはリソースを整形してAndroidがapkを効率よく実行できるようにし、最後にapkに署名しています。

署名もzipalignもしていない状態のapkは1902バイトで、上記のプロセスでおよそ1キロバイトが上乗せされることになります。

## File-size discrepancy (2608 bytes, 21% reduction)

なんか、zipalignされてないapkを解凍して手動で署名すると、`META-INF`と`MANIFEST.MF`が消え去って543バイト浮くんだけど。
ちょっとどうしてこんなことがおこるのか知ってる人いたら教えてくだちい。

ここでapkはついに3ファイルだけになりました。
さらに言うと、リソースは全く使ってないので`resources.arsc`も消すことができます。

これによって、`AndroidManifest`と`classes.dex`の2ファイルだけが残されました。
両ファイルはだいたい同じ大きさです。

## Compression Hacks (2599 bytes, 0.5% reduction)

残ってる文字列を全て'c'に変更し、バージョンは26に固定して署名済apkを作成しましょう。

```groovy
	compileSdkVersion 26
	    buildToolsVersion "26.0.1"
	    defaultConfig {
	        applicationId "c.c"
	        minSdkVersion 26
	        targetSdkVersion 26
	        versionCode 26
	        versionName "26"
	    }
```

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">

    <application
        android:icon="@android:drawable/btn_star"
        android:label="c"
        >
        <activity android:name="c.c.c">
```

9バイト削れました。

ファイルの文字数は変わっていませんが、'c'の出てくる頻度が変わったため、圧縮アルゴリズムはサイズをより小さくしてくれます。

## Hello ADB (2462 bytes, 5% reduction)

`AndroidManifest`をさらに削減するため、`Activity`を起動するインテントを削除します。
これからはアプリの起動は以下のコマンドで行うことになります。

```
	adb shell am start -a android.intent.action.MAIN -n c.c/.c
```

`AndroidManifest`はこうなりました。

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">

    <application>
        <activity
            android:name="c"
            android:exported="true" />
    </application>
</manifest>
```

ランチャーアイコンも削除しました。

## Reducing method references (2179 bytes, 12% reduction)

当初の目標は、デバイスにインストール可能なapkを作成することでした。

現在のアプリは`TextView`、`Bundle`、そして`Activity`を使用しています。
直接`Application`を使うようにすることで、それらを参照する必要がなくなり、Dexファイルのサイズをさらに減らすことができます。
今やDexファイルが参照するのは、`Application`クラスのコンストラクタだけになりました。

```java
	package c.c;
	import android.app.Application;
	public class c extends Application {}
```

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="c.c">
    <application android:name=".c" />
</manifest>
```

adbでインストールが成功し、さらにapkがインストールに成功したことを設定アプリから確認することもできます。

![08.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/588f4201-a2e2-ab37-44be-7d9c7c365a8e.jpeg)

## Dex Optimisation (1961 bytes, 10% reduction)

ここで数時間を費やし[Dexファイルのフォーマット](https://source.android.com/devices/tech/dalvik/dex-format)を調査しました。
チェックサムやオフセットなど様々な仕組みが、Dexファイルの直接編集を困難なものにしていました。
長い長い道のりを省略して結論だけ言うと、apkインストールが成功するための唯一の条件は、`classes.dex`ファイルが存在していなければならない、ということでした。
従って、apkから元の`classes.dex`を削除し、ターミナルから`touch classes.dex`とするだけで、ファイルサイズが10%減少します。

時には、愚かとしか思えない策が最も適切な解だったりします。

## Understanding the Manifest (1961 bytes, 0% reduction)

未署名apkの`AndroidManifest`はバイナリのXMLですが、その形式についてのオフィシャルなドキュメントは見つかりませんでした。
[HexFiend](https://github.com/ridiculousfish/HexFiend)を使って中身を操作してみます。

ファイルヘッダにいささか興味深い項目を見付けることができました。
最初の4バイトには、Dexファイルと同じくtargetSdkVersion`38`がありました。
次の2バイトは`660`が入っており、これはファイルサイズを表しているようです。

そこでtargetSdkVersionを1に変更して、余った不要なバイトを削除し、次の2バイトを`659`にしてみました。
残念ながら、これは無効なapkとして拒否されてしまいました。

## Not understanding the Manifest (1777 bytes, 9% reduction)

`AndroidManifest`のファイル全体にダミー文字列を入力してから、ファイルサイズを変更せずにapkをインストールしてみます。
これによってチェックサムがあるか、また変更によってapkのオフセットが無効になったかを判断できます。

![09.jpg](https://qiita-image-store.s3.amazonaws.com/0/26088/2a4efecd-7c8a-3b0b-ca8d-a610f44f9977.jpeg)

驚いたことに、こんな`AndroidManifest`でもOreoのNexus5上では有効なapkと判断されました。
`BinaryXMLParser.java`の開発者はきっと枕に顔を埋めて足をバタバタさせて叫んでいることでしょう。

今後のために、ダミー文字列をnullバイトに置き換えます。
これによってHexFiendで意味のある部分を見やすくなります。

### UTF-8 Manifest

![10.png](https://qiita-image-store.s3.amazonaws.com/0/26088/0a8e8432-61bb-1bfd-4778-6c8663eb0686.png)

これらは`AndroidManifest`に最低限必要なものであり、何れかがなくなるとapkのインストールに失敗します。
manifestやpackageなど、すぐに分かるものが幾つかあります。
versionCodeとパッケージ名も含まれています。

### Hexadecimal Manifest

![11.png](https://qiita-image-store.s3.amazonaws.com/0/26088/6f6af8db-c181-cdc0-2406-66eb37cca5ea.png)

16進数で表示すると、ファイルサイズを示す`0x9402`など、いくつか意味のある値が出てきます。
また、文字列長が8バイトを超える場合、その長さが文字列の手前2バイトで示されているようです。

しかし、ここでファイルサイズを減らすのは難しそうです。

## Done? (1757 bytes, 1% reduction)

完成したapkを見てみましょう。

![12.png](https://qiita-image-store.s3.amazonaws.com/0/26088/a32def62-1587-219b-68f4-44a3503a28c3.png)

v2署名で自分の名前が入っていました。
圧縮アルゴリズムが効いてくれる名前に変更しておきます。

![13.png](https://qiita-image-store.s3.amazonaws.com/0/26088/a7a63e8a-e85b-4050-f5c6-f4a14d1dc1bb.png)

これで20バイト浮きました。

## Stage 5: Acceptance

最終的に1757バイトのapkが完成しました。
私が知るかぎり、これが実在する最小のapkです。

しかし間違いなく、Androidコミュニティにいる誰かが、この容量を上回るさらなる最適化ができると確信しています。
もし改善案を見付けたら、[GitHubリポジトリ](https://github.com/fractalwrench/ApkGolf)にPRを送るか、[Twitter](https://twitter.com/fractalwrench)で教えてください。

## Thank You

Android APKの仕組みを調べることで新たな知見が得られました。
ご意見・ご質問・新たな提案などありましたら、[Twitter](https://twitter.com/fractalwrench)に連絡してください。


# その後

その後は[GitHub](https://github.com/fractalwrench/ApkGolf)においてプルリクを受け付けており、2017/12/25現在ではなんと678バイトにまで減っています。
実物のapkはリポジトリの`signed-release.apk`から試すことができます。
みんなも挑戦してみよう。