連載
» 2013年02月18日 18時00分 公開

Androidで動く携帯Javaアプリ作成入門(40):動的クラスローディングでAndroidアプリ“裏”開発 (2/3)

[緒方聡,イーフロー]

BaseDexClassLoaderを用いた動的クラスローディング

 今回のサンプルアプリの実装を見ていきましょう。すべてのボタンは、ほぼ同じように動作するのですが、最初にその動作について説明しておきます。

 ボタンのリスナは下記のように実装されています。

button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClassLoader loader = createClassLoader1(mDexPath);
        invoke(loader);
    }
});

 クラスローダを作成するcreateClassLoaderNメソッドを呼び出し、そのクラスローダからクラスをロードしてメソッドを実行するinvokeメソッドを呼び出します。

 「mDexPath」というのは/sdcard/Hello_dex.jarです。

 invokeメソッドは、以下のような実装になっています。

private void invoke(Object loaderOrClass) {
    ClassLoader loader = null;
    Class<?> clazz = null;
    if (loaderOrClass instanceof ClassLoader) {
        loader = (ClassLoader) loaderOrClass;
    } else {
        clazz = (Class<?>) loaderOrClass;
    }
    try {
        if (loader != null) {
            clazz = loader.loadClass("com.example.hello.Hello");
        }
        Object obj = clazz.newInstance();
        Method method = clazz.getMethod("sayHello");
        Object greeting = method.invoke(obj);
        append((String) greeting);
    } catch (Exception e) {
        append(e.getMessage());
    }
}

 このアプリのActivityをロードしたクラスローダと動的にロードしたクラスのクラスローダは異なるため、リフレクションを用いてメソッドを呼び出さなければなりません。動的にロードしたクラスが特定のinterfaceを実装しているか、特定のクラスのサブクラスになっているのであれば、それらにキャストして使えますし、そのようにすべきです。今回はサンプルなので、リフレクションを用いて簡単に実現しています。

 さて、この章の本題のクラスローダを生成する部分の実装です。

private ClassLoader createClassLoader1(String dexPath) {
    return new BaseDexClassLoader(dexPath, mDexOutputDir, null, getClassLoader());
}

 BaseDexClassLoaderの第1引数には、DEX形式のJARのフルパスを、第2引数には最適化されたDEXファイルの出力先をFileで、第3引数にはネイティブライブラリのパスを、第4引数には作成するクラスローダの親クラスローダを指定します。

 第2引数の出力先は、書き込みができる場所ならどこでもよいのですが、他のプロセスに書き換えられたり消されたりされると困るので、アプリ専用のディレクトリを指定するのがよいでしょう。今回は「Context#getDir(String, int)」を用いて「MODE_PRIVATE」で他のプロセスからアクセスできない専用ディレクトリを作成して使用しています。

 この使い方で、期待通りに動的クラスローディングが行えます。

DexClassLoaderを用いた動的クラスローディング

 BaseDexClassLoaderとは、クラスローダの作成方法のみが異なるので、そこだけ解説します。

private ClassLoader createClassLoader2(String dexPath) {
    return new DexClassLoader(dexPath, mDexOutputDir.getAbsolutePath(), null, getClassLoader());
}

 DexClassLoaderの第1引数には、DEX形式のJARのフルパスを、第2引数には最適化されたDEXファイルの出力先をStringで、第3引数にはネイティブライブラリのパスを、第4引数には作成するクラスローダの親クラスローダを指定します。

 BaseDexClassLoaderとは、第2引数がFileかStringかの違いしかありません。実装は以下のように単に継承して第2引数をStringからFileに変えているだけなので、DexClassLoaderを使う必要性は、現時点では特になさそうです。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

 筆者なら、Fileオブジェクトが手元にあればBaseDexClassLoaderを、Stringが手元にあればDexClassLoaderを使うようにすると思います。

 ちなみに「Android Developers Blog」の解説ではDexClassLoaderを使用しています。

PathClassLoaderでは動的クラスローディングは行えない

 PathClassLoaderは以下の要領で使用しますが、前述したとおりアプリからはPathClassLoaderで動的クラスローディングはできません。

private ClassLoader createClassLoader3(String dexPath) {
    return new PathClassLoader(dexPath, getClass().getClassLoader());
}

 その理由は、このクラスローダは最適化されたDEXファイルの出力先を指定できず、決め打ちで「/data/dalvik-cache」という場所に出力しようとしますが、通常のアプリはこのパスへの書き込み権限がなく、クラスをロードしようとするタイミングで例外が発生します。

 具体的には、クラスローダ自体は生成できますが、「loadClass(String)」を呼び出すと、最適化されたDEXファイルの出力に失敗し、ClassNotFoundExceptionが発生します。

DexFileを用いた動的クラスローディング

 「DexFile」は、DEXファイルを表すクラスで、上記クラスローダ内部で使用されているDEXファイルからクラスをロードする重要なクラスです。

private void createClassLoader4(String dexPath) {
    String filename = dexPath.substring(dexPath.lastIndexOf("/"));
    filename = filename.replace(".jar", ".dex");
    try {
        DexFile dexFile = DexFile.loadDex(dexPath,
            mDexOutputDir.getAbsolutePath() + filename, 0);
        Class<?> clazz = dexFile.loadClass("com.example.hello.Hello",
            getClassLoader());
        if (clazz != null) {
            invoke(clazz);
        } else {
            append("class is null");
        }
    } catch (IOException e) {
        append(e.getMessage());
    }
}

 DexFileはFileまたはStringを引数に持つコンストラクタが2つありますが、これらは最適化されたDEXファイルの出力先が指定できません。代わりに、「DexFile.loadDex(String, String, int)」を使用することで、出力先を「自由に」選択できます。

 「自由に」と強調しているのは、BaseDexClassLoaderやDexClassLoaderが出力先ディレクトリを指定するのに対し、DexFileは出力先ファイルを指定できるためです。

 上記クラスローダを使用すると、ファイル名が.jarを.dexに置き換えたものになります。DexFileなら自由にファイル名を決められるのですが、今回はBaseDexClassLoaderやDexClassLoaderと同じ命名規則で出力することにします。

 「DexFile.loadDex(String, String, int)」の第3引数は、将来のために予約されているものの、現時点では使用されていません。取りあえず「0」を指定しておきます。

 最も大事なのは「DexFile#loadClass(String, ClassLoader)」です。このメソッドは第1引数で指定されたクラスを第2引数のクラスローダに関連付けてロードします。DEXファイルからクラスをロードするのはDexFileですが、ロードされたクラスは第2引数のクラスローダに関連付けられます。

 これは、どういうことなのでしょうか。

getClassLoader()を用いた動的クラスローディング

 「Context#getClassLoader()」を使うと、アプリをロードしたクラスローダが取得できます。このクラスローダには、当然Helloクラスは含まれないので、以下のコードを実行すると、ClassNotFoundExceptionが発生します。

button5.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClassLoader loader = getClassLoader();
        invoke(loader);
    }
});

 しかし、「DexFile#loadClass(String, ClassLoader)」で、アプリのクラスローダとHelloクラスを関連付けた後なら、上記コードは正常にクラスをロードしメソッドを呼び出せるようになります。これはつまり、以下のようにコードが書かれていても、正常に実行できてしまうのです。

Hello hello = new Hello();
append(hello.sayHello());

 このように書くには、コンパイル時にHelloクラスが解決されていなければならないという制約こそありますが、面白いことができる可能性が秘められています。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。