Superdry Memorandom :-p

旧「superdry memorandum :-D」です

Android Developer Blog の Custom Class Loading in Dalvik をてきとう翻訳してみたよ。

ちょっと興味深かったので勉強メモがてら、てきとう翻訳してみました。十分に理解してるとは言いがたいので訳に変なとこがあったらご指摘ください。

今回の記事ではFred Chungさんという方が書いているのですが、最後の方書くの飽きちゃったのかかなり投げ出した感がありました(笑)。Android Developer Blogって毎回書く人が違うんですが、毎回その人なりの個性がでててとても面白いです。

Dalvikでのカスタムクラスローディング

DalvikVMは開発者にカスタムクラスをロードできるようにしています。デフォルトの位置からDalvik実行可能ファイル(dexファイル)をロードするかわりに、アプリが内部ストレージまたはネットワーク越しからロードすることが可能です。

このテクニックは全てアプリケーションに役立てるものではありません。 実際ほとんどがこのテクニックなしでも問題ないです。しかし、カスタムクラスをローディングする状況で役に立ちます。ここに2つの利点を示します。

  • メソッド参照を64k以上含めることができます。これはdexファイルでサポートされる最大数です。この制限を逃れるため、開発者は複数のセカンダリdexファイルにプログラムを分割し、ランタイムでそれらをロードします。
  • ランタイムでロードされるダイナミックコードによって拡張可能なロジックに設計することが可能です。

クラスロードとdexファイルの分割についてデモ用サンプルアプリを作成しました。(下記に示される理由により、ADTのEclipseプラグインではサンプルアプリをビルドできません。中に含まれるantビルドのスクリプトを使います。詳細はReadme.txtを参照。)

このアプリには、Toastを表示するためにライブラリコンポーネントを呼び出す簡単なActivityがあります。 Activityとそのリソースはデフォルトのdexで保持されますが、ライブラリコードはAPKにバンドルされたセカンダリdexに格納されます。それには下記に詳細を示す通りビルドブロセスを変える必要があります。

ライブラリメソッドを呼び出し可能にする前に、アプリはセカンダリdexファイルをロードしなければなりません。 関連箇所の実装を見ていきましょう。

コードの構成

アプリは以下の3つのクラスで構成されています。

  • com.example.dex.MainActivity: ライブラリを起動するUIコンポーネント
  • com.example.dex.LibraryInterface: ライブラリのInterface定義クラス
  • com.example.dex.lib.LibraryProvider: ライブラリの実装

ライブラリはセカンダリdexでパッケージ化されます。残りのクラスはデフォルト(プライマリ)dexファイルに含まれます。下記の章の「ビルドプロセス」はこれを実現するための方法を示しています。もちろんパッケージングをどのようにするかは、開発者がどのような経緯で取り組んでいるか、その状況に依存します。

クラスローディングとメソッド呼び出し

LibraryProviderを含むセカンダリdexファイルはアプリケーションのassetとして保持されます。最初に、クラスローダーのパスを提供できるようストレージの場所をコピーする必要があります。サンプルアプリでは内部ストレージ領域を使っています。(技術的には外部ストレージでも可能ですが、そこにアプリケーションバイナリをおくことのセキュリティについて考える必要があります)

下記はMainActivityのスニペットです。標準的なファイルI/Oでコピーを実行しています。

  // DexClassLoaderがセカンダリdexファイルを処理する前に、
  // まずはじめに、assetリソースからストレージの場所をコピーします。
  File dexInternalStoragePath = new File(getDir("dex", Context.MODE_PRIVATE), SECONDARY_DEX_NAME);
  ...
  BufferedInputStream bis = null;
  OutputStream dexWriter = null;

  static final int BUF_SIZE = 8 * 1024;
  try {
      bis = new BufferedInputStream(getAssets().open(SECONDARY_DEX_NAME));
      dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
      byte[] buf = new byte[BUF_SIZE];
      int len;
      while((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
          dexWriter.write(buf, 0, len);
      }
      dexWriter.flush();
      dexWriter.close();
      bis.close();
      
  } catch (. . .) {...}

次に、セカンダリdexファイルからライブラリを抽出しロードするためにDexClassLoaderインスタンス化します。このようにロードされたクラスのメソッドを呼び出す方法は色々あります。今回の例では、クラスインスタンスをメソッドが直接呼ばれるインタフェースにキャストします。

もう一つの方法として、リフレクションAPIをつかってメソッドを呼び出す方法があります。リフレクションを使う利点は、どんなインタフェースを実装するのもセカンダリdexファイルを必要としないことです。 しかし、リフレクションは冗長で遅いことも意識しておかないといけません。

  // DexClassLoaderが最適化dexファイルを書き出す内部ストレージへのパス
  final File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);

  DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
                                         optimizedDexOutputPath.getAbsolutePath(),
                                         null,
                                         getClassLoader());
  Class libProviderClazz = null;
  try {
      // ライブラリをロード
      libProviderClazz = cl.loadClass("com.example.dex.lib.LibraryProvider");
      // インタフェースのメソッドを直接呼び出すことができるように、
      // 返されたオブジェクトをライブラリのインタフェースにキャスト
      // 別の方法として、リフレクションでメソッドを呼び出すことができる(ただし冗長になる)
      LibraryInterface lib = (LibraryInterface) libProviderClazz.newInstance();
      lib.showAwesomeToast(this, "hello");
  } catch (Exception e) { ... }
ビルドプロセス

2個の別々のdexファイルを作るためには、標準的なビルドプロセスを少し変える必要があります。それは、プロジェクト内のAnt用のファイルbuild.xmlのなかで、「-dex」ターゲットを変更するだけです。

以下の手順でターゲット「-dex」を変更します。

2つのデフォルトdexファイルとセカンダリdexファイルに変換される.classファイルを保存するために用意した2つのディレクトリを作成します。PROJECT_ROOT/bin/classesの中から選択した.classファイルを用意しておいたディレクトリにコピーします。

<!-- プライマリdexファイルにはライブラリ実装以外がふくまれる -->
<copy todir="${out.classes.absolute.dir}.1" >
    <fileset dir="${out.classes.absolute.dir}" >
        <exclude name="com/example/dex/lib/**" />
    </fileset>
</copy>
<!-- セカンダリdexには実際のライブラリ実装が含まれる -->
<copy todir="${out.classes.absolute.dir}.2" >
    <fileset dir="${out.classes.absolute.dir}" >
        <include name="com/example/dex/lib/**" />
    </fileset>
</copy>     

この2つのディレクトリの中の.classファイルを個別のdexファイルに変換します。セカンダリdexファイルをjarファイルに追加します(これはDexClassLoaderに対するインプットとなります)。最後にプロジェクトのassetsフォルダへjarファイルをおきます。

<!-- apkのassetsフォルダにアウトプットをパッケージ化しておいておく -->
    <jar destfile="${asset.absolute.dir}/secondary_dex.jar"
            basedir="${out.absolute.dir}/secondary_dex_dir"
            includes="classes.dex" />

ビルドを開始するために、プロジェクトのルートディレクトリでant debug(もしくは ant release)を実行します。

どうですか?正しい状況で、ダイナミッククラスローディングを役立ててください。