2009-11-25

[Android] Dynamic-loading of classes in your Android app

I am writing an application that can load plug-ins developed by other programmers. The first thing that come to my mind is the use of Class.forName and methods in java.lang.reflective. I was very naive to use Class.forName() like this,

try {
         Class<?> handler = Class.forName(handlerClassName);
         Method m = handler.getDeclaredMethod(
                "addMyPreferences",
                Class.forName("android.content.Context"),
                Class.forName("android.preference.PreferenceCategory"),
                Class.forName("java.lang.String"));
         m.invoke(handler.newInstance(), this, mExtraSettingsCategory, defaultValue);
     } catch(ClassNotFoundException e) {
        // .....
     }
     // other exceptions that may be thrown by reflective API.

But, I got an ClassNotFoundException from this. Home come? A big question mark showed up in my head. I was wondering if any other developers have the same problem with me. So, I googled and found one document that is helpful to understand what's go on behind this ClassNotFoundException (See here)

In this document, it says the dynamic-loading model of Java basically is built upon parent-chaild hierarchy and ClassLoader class. It would be helpful to print out the class loader hierarchy. So I did it.

ClassLoader cl = this.getClass().getClassLoader();
while (cl != null) {
    Log.d(TAG, "====> class loader: " + cl.getClass());
    cl = cl.getParent();
}

Two class loaders were printed out on my screen, dalvik.system.PathClassLoader and java.lang.BootClassLoader. The interesting thing is the use of PathClassLoader. Look at its documentation, it accepts there different types of values as its path argument in the constructor. Just copied from documentation,

Creates a PathClassLoader that operates on two given lists of files and directories. The entries of the first list should be one of the following:

public PathClassLoader  (String path, ClassLoader parent)

Creates a PathClassLoader that operates on a given list of files and directories. This method is equivalent to calling PathClassLoader(String, String, ClassLoader) with a null value for the second argument (see description there).

    * Directories containing classes or resources.
    * JAR/ZIP/APK files, possibly containing a "classes.dex" file.
    * "classes.dex" files.

Hum, based on these clues, I guessed the currently using PathClassLoader was created with an apk filename this application is in was passed in, like

PathClassLoader("/data/app/org.startsmall.myapp.apk",
                ClassLoader.getSystemClassLoader());

Hence, if I want Class.forName() to see 3-rd party classes from other developers with class name given, I should construct a new PathClassLoader that loads classes from my apkl files and 3-rd party apk files. Here is the code,

final String apkFiles =
            "/data/app/org.startsmall.myapp.apk:" + // myself
            // handlers defined by other developers
            "/data/app/" + handlerClassName.substring(0, lastDotPos) + ".apk";

        dalvik.system.PathClassLoader myClassLoader =
            new dalvik.system.PathClassLoader(
                apkFiles,
                ClassLoader.getSystemClassLoader());

         // ...

        try {
            Class<?> handler =
                Class.forName(handlerClassName, true, classLoader);

            // Call reflective APIs.

       } catch (ClassNotFoundException e) {
            // .....
       }

After these of trial-and-errors, my code of using Class.forName(...) successfully gets 3-rd party classes loaded and methods defined in the classes are executed as expected. :)