Androidの実行環境について

高島友里氏:AndroidのLINE開発に携わっているtakasyこと高島です。よろしくお願いします。今回の発表では「APKファイルはどのように作られているのか」について、流れを見ていきたいと思います。かなり初心者向けの内容になっていますので、気軽に聞いていただければと思います。

この発表での目標は、どういう流れで何がなんのために動いているのかをざっくり理解することです。公式資料のこの図をベースにしながら順を追って見ていくことにしますが、まずはその前にAndroidの実行環境の話に触れておきます。この青枠で囲ったDEX Fileがなぜ必要になるかを把握したほうが流れが追いやすいと思うからです。

Androidの特徴はいくつかあると思うんですが、1つとしてさまざまな端末があるということが挙げられると思います。端末によって使われているCPUも違いますし、ARM64やx86のようにCPUアーキテクチャも種類があって、それに全部対応するようなコードをコンパイルするのは現実的ではありません。

そこで登場するのがJVM、Java仮想マシンになります。Javaコードはjavacコンパイラによって.classファイル、バイトコードにコンパイルされてJVM上で実行されます。

JVMを使うことでCPUアーキテクチャへの対応の問題はなくなったんですが、そもそもJVMがほぼ無制限のストレージやバッテリーを備えた端末向けに設計されているので、メモリもバッテリー容量も少ないAndroidだと……最近はそうでもないかもしれないんですけど、ちょっと無理があります。そのためGoogleではDalvikと呼ばれるAndroid JVMを採用していました。

今はDalvikではなくAndroidランタイムというARTに移行しています。この実行環境では.classバイトコードではなくて.dex、Dalvikバイトコードを実行します。dexはDalvik Excutableの意味です。

Androidのバイトコード実行環境はアプリ起動やインストール、ひいてはOSの更新時のアプリの再インストールなどにも関わってくるかなり大事な部分になっていて、けっこう変遷があっておもしろいので、興味がある方は、右下のリンクに後ほど資料が共有されると思うのでご覧いただければと思います。

各ファイルやライブラリの説明

では、なぜDEX Fileが登場するかもわかったので、ベースの図に戻って順を追って見ていきます。コンパイラはコンパイルするものによって使うものが変わっていきますが、まず挙げられている対象物をざっくり見ていこうと思います。けっこうみなさんご存知で「当たり前だろ」という気持ちになるかもしれないんですけど復習がてらに聞いてみてください。

ソースコードとリソースファイルに関しては言わずもがな。モジュールのsrcフォルダに入っている.javaファイルと.ktファイル。リソースファイルはresフォルダに入っているものが対象になります。AIDLはクライアントが別のアプリからマルチスレッド対応のサービスにアクセスしてプロセス間通信をするときに必要になるファイルで、Java言語で書かれています。これはあまり使うことがないかもしれません。

ライブラリモジュールは……すみません。何を指してライブラリモジュールなのかいまいちピンと来ないので、これは一旦置いておいて、AARを見ていきます。AARはAndroidライブラリで、モジュールと構造は同じになります。コンパイルするとAPKではなくてAARが作られる点が違います。JARはJavaのライブラリで、AARと違ってAndroidのリソースとマニフェストを含められません。

ざっくり紹介したんですけど、この中でリソースファイルおよびAAR内にあるリソースファイルは、aapt2によってコンパイルされて、それ以外のソースコードの.javaファイル、.aidlとか.jarや.aar内の.javaファイルはJavaコンパイラであるjavac。.ktファイルや.aar内の.ktファイルはkotlincというKotlinコンパイラでコンパイルされます。

このコンパイルされることによって.classファイルに変換されます。一旦ソースコードは置いて、リソース側のコンパイラを見ていきましょう。

リソースファイルのコンパイル

aapt2は、AndroidManifestとリソースファイルをコンパイルして、1つのAPKにパッケージ化します。このときにコンパイルとリンクの2つのステップに分かれていて、1つずつコンパイルして最後にリンクしてコンパイルしたものをまとめるようになります。そのため、変更が1つのファイルだったとしたら、再コンパイルが必要なのはそのファイルだけで済むといったインクリメンタルコンパイルができるようになっています。

まずコンパイルのフェーズで1つずつファイルをコンパイルして中間ファイルの.flat、拡張子のバイナリXMLファイルを出力します。リンクフェーズではコンパイルフェーズで生成された中間ファイルをすべてマージして1つの.apkファイルを出力します。このときR.javaやproguard-rulesも生成できます。

出力された.apkファイルはもちろんDEXファイルが入ってないのでDEXファイルは含まないですし、署名もしていないので実行できないAPKになっています。このAPKには、AndroidManifestとバイナリXMLファイルたちとresouces.arscが入っています。

このresouces.arscというのは、リソースに関するすべてのmeta情報が含まれていて、パッケージ内のすべてのリソースのインデックスなどを持っています。バイナリファイルになっていて、実際の実行できるAPK、みなさんがよくビルドして実行しているAPKには非圧縮で保存されていてメモリ上に展開されるだけで使えるようになります。

ここに関してもう少し詳細が知りたい方は、また右下のSpeaker DeckのURLを参照してみてください。

APKと一緒に出力されると言ったR.javaは、一意のIDが割り当てられているため、コンパイル中にJavaコードからリソースを使用できるようになります。arscはアプリ実行時に使われるリソースのインデックスなので注意してください。

proguardのルールも出力されると言ったんですが、出力されるルールはproguardが後述するR8によってレイアウト内でのみ参照されるなど、使われていないと判断されて削除されないようにしてくれます。これでリソースファイルはコンパイルできたので、次に進みます。

ソースコードのコンパイル

次に、ソースコードのコンパイルを、javacやkotlincで.classファイルが生成されたあとから見ていきます。

コードのコンパイルにはR8が使われます。Jakeさんのブログで「R8とは最適化もするD8のバージョン」と言われていました。D8はクラスファイルをDEXファイルに変換するdexerの役割と、Java8の機能をAndroidでも実行可能なバイトコードに変換するdesugarの役割を担っています。

なので、R8に.classファイルを持たせてコンパイルをすると、圧縮しつつ難読化や最適化、desugar、DEX変換とDEXのマージをしてくれて、最終的にすべてのDEXファイルがまとめられたclasses.dexが出力されます。

R8ではproguard-rulesファイルを通じてアプリのコードへのエントリーポイントとして機能するクラスなど、そのアプリの構造を把握できるようになります。

リソースの圧縮の項目がこのコンパイラ中にあると思うんですが、この項目のときに先に話したaapt2で出力されたproguardのルールが使用されて、圧縮時に必要以上に削除されないようになっています。

コンパイルの中でやっていることを1つずつ見ていくと、まずこのツリーシェイキング、圧縮のところは、静的解析で到達できなかったりインスタンス化されないオブジェクトなど未使用のコードと構造を削除します。

難読化は、クラス、メソッド、フィールドの名前を短くしてアプリのサイズを小さくできます。難読化なので他にも利点があるんですが、目的はサイズを削減することにあります。

最適化は、不要なところを書き換えたりインライン化していくことでDEXファイルサイズを小さくするアプローチです。

最後にdesugarですが、これはD8の担当で、Desugaringすることでさっきも言ったようにJava8の便利な言語機能が使えるようになります。

R8が実際に何を行っているか

言葉だけで見てもわかりづらいかと思うので、去年のAndroid Dev Summitの動画でわかりやすくシュリンクしている例があったので拝借してきました。

ここではJavaHellowWorldというクラスをコンパイルしています。まずはトレースして、何を削減していいかを把握するところから始まります。

proguardのルールであるkeepルールから、既知のエントリーポイントであるmainは消したくないことがわかります。

エントリーポイントをたどってトレースしていきます。mainから入ってgreetingが呼ばれて終わり。ここでunusedが使われていないということがわかりました。

トレースが終わったので、ツリーシェイキングで使っていなかったunusedメソッドがまずは削除されます。次に、難読化でメソッド名がgreetingからaという名前に変わって短くリネームされます。最後に、最適化としてgreetingメソッドをmainの中にインライン化して完成します。

これでかなり短くなっているのがわかると思います。

これらのことをR8がしてくれて、classes.dexという1つのDEXファイルが出力されます。Multidexを使用している場合はその限りではなく、複数出てきてしまうんですけど、とりあえずclasses.dexが作られます。

DEXファイルのフォーマットと課題

できたついでに、DEXファイルのフォーマットも確認しておきましょう。ファイル自体の情報は1番上のヘッダにあって、その下の緑で囲っているところはIDの集まりになります。すべてのファイル内の他の場所をこのIDが指しています。実際のバイトコードとデータは後ろにまとめられていて、このdataセクションのdataと書いてあるところですね。バイトコードの中ではクラス名や定数の名前はすべて上のIDで参照されます。

例えば、string_idsセクションでは、ファイルの先頭からこのアイテムの文字列までのオフセットであるstring_data_offを持つstring_id_itemが並んでいます。そのオフセットを元に、例えば左側の228バイトだとしたら、ファイルの先頭から228バイトのところにstring_id_itemがあって、文字列の長さが10だとわかっているので、その長さの文字列を取得してTestClass0が取得できます。

DEXファイルのフォーマットがざっくり理解できたとして、DEXファイルの課題があります。アプリが大きくなってくると起こる64K参照制限に引っ掛かったことがある人もいると思います。参照するライブラリも含めてアプリのメソッドが65,536個、64×1,024個を超えると、右のようにビルドエラーが起こります

なぜこれが起こるのか。IDのセクションは範囲が決まっています。メソッドIDの範囲は0から0xFFFFまで。つまり65,536個、通し番号でいうと0から65,535までしか参照できません。これが64Kを超えると起こるビルドエラーの原因でした。これを回避するためには、アプリの依存関係の見直しやR8を使用して未使用のコードを削除するなどが有用です。やむを得ない場合は、Multidexを使いましょう。

すべてのAPKで必要なデジタル署名

コンパイラが生成するものがわかったので、次は署名回りを見てみます。APKをデバイスにインストールしたり更新したりする前に、すべてのAPKで必要なのがデジタル署名です。といっても、このすべてのAPKということはデバッグも含まれているんですけど、デバッグのときに署名をした覚えがある人はあまりいないと思います。

署名の設定をしていなくてもデバッグができているのは、プロジェクトを実行したときにAndroid StudioがAndroid SDKツールで生成されたデバッグ証明書を使用して自動的にアプリに署名してくれている。デバッグキーストアとデバッグ証明書を$HOME/.android/debug.keystoreを自動的に作成してキーストアと鍵のパスワードを設定しています。

これも画面を見たほうが思い出せると思うので、左に画面を載せてみました。左がアップロード鍵とキーストアを作るときの画面です。作ったあとは右画面のようにアップロード鍵でAPKをビルドして署名すれば署名済みAPKができあがります。

最後に、キーストアも整ったところでAPKを仕上げるために実行されるのがapkbuilderとzipalignです。apkbuilderで今までの出力されたものをまとめます。そしてzipalignでAPKファイルを最適化するために4バイトアラインメントに揃えます。これをしておくと、アプリ実行時にリソースをメモリに展開するmmap()というシステムコールですべての部分にアクセスできるようになります。

また、アプリ実行時に無駄にメモリの確保がされなくなるためメモリ消費量が減ります。そのためAPKファイルを作成する前にはzipalignを実行する必要があります。

早いですけどまとめです。Androidに合うJVMにするために、まずDEXファイルを使用する必要があった。だからDEXファイルを内部では使っている。リソースファイルはaapt2がコンパイルして、APKファイルとR.java、proguardのルールを出力します。コードはクラスファイルにjavacとkotlincでコンパイルしてからR8がコンパイルして最適化した上でclasses.dexを出力します。

署名済みアプリをビルドするには、キーストアとアップロード鍵が必要です。apkbuilderでAPKファイルにしてzipalignで最後に最適化します。

aab(Android App Bundle)まで本当は調査をしたかったんですけど、ぜんぜん間に合わない上に、概要に書いている内容と違ってかなりフワッとした内容となってしまったことは、自分としてはすごく心残りなんですけど、自分のようにビルド回りに苦手意識を持っている方がもしいるなら、苦手意識を払拭できるきっかけになれば幸いです。

お聞きくださりありがとうございました。