Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ClassCastException while dynamically loading a class in Android

I have a thread that loads different classes for the resources it needs depending on the specific implementation of the system. My implementation is on Android and I have a class that returns the specific classes needed by my implementation. I seem to be able to load the class fine, but when I try to assign it to the object in my main thread, it gives me a ClassCastException. Here are the snippets:

In my main thread, I do:

    try {
        grammarProcessor = config.loadObject(GrammarProcessor.class);

which gives me this stacktrace:

    E/AndroidRuntime(6682): FATAL EXCEPTION: JVoiceXmlMain
    E/AndroidRuntime(6682): java.lang.ClassCastException: org.jvoicexml.android.JVoiceXmlGrammarProcessor
    E/AndroidRuntime(6682):     at org.jvoicexml.JVoiceXmlMain.run(JVoiceXmlMain.java:321)

GrammarProcessor is an interface and JVoiceXmlGrammarProcessor is the class that I load and implements that interface. The loading code is as follows:

else if(baseClass == GrammarProcessor.class){
        String packageName = "org.jvoicexml.android";
        String className = "org.jvoicexml.android.JVoiceXmlGrammarProcessor";           
        String apkName = null;
        Class<?> handler = null;
        T b = null;

        try {
            PackageManager manager = callManagerContext.getPackageManager();
            ApplicationInfo info= manager.getApplicationInfo(packageName, 0);
            apkName= info.sourceDir;
        } catch (NameNotFoundException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
            return null;
        }
        PathClassLoader myClassLoader =
            new dalvik.system.PathClassLoader(
                    apkName,
                    ClassLoader.getSystemClassLoader());
        try {
            handler = Class.forName(className, true, myClassLoader);
            return (T) handler.newInstance();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        }           
        catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        }
}

When debugging, I check what's returning from the load method and it is an object with an id number. If I click on it, it'll say org.jvoicexml.android.JVoiceXmlGrammarProcessor@40565820, and the dropdown will show the two private fields that a JVoiceXmlGrammarProcessor should have, so it looks like it's well loaded. Any ideas?

like image 486
Marakatu Avatar asked May 29 '12 01:05

Marakatu


1 Answers

I think I understand what's happening here but I have to make an assumption that org.jvoicexml.android is not your package, i.e., you're loading from a different apk (as the bounty seems to suggest).

With that in mind, this is impossible and for a good reason.

Let's start with your own app - you have the type GrammarProcessor available from your own classes.dex and into your default ClassLoader (the PathClassLoader that you get when the zygote forks your process). Let's call this type GP1. Any class in your own application that implements GrammarProcessor actually has GP1 in their interface list.

Then, you instantiate a new classloader. If you look at the source, you'll see that PathClassLoader is just a thin wrapper around BaseDexClassLoader which in turn delegates to a DexPathList, which in turn delegates to DexFile objects which in turn do the loading in native code. Phew.

There's a subtle part of BaseDexClassLoader that's the cause of your troubles but if you haven't seen it before, you might miss it:

this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

and a bit further down:

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class c = pathList.findClass(name);
    if (c == null) {
        ...
    }
    return c;
}

BaseDexClassLoader does not check with its parent first!

.. and that in short is your problem.

More precisely, the DexPathList and DexFile inside it load all the classes from the other dex and never look into the classes already loaded in the VM.

So, you end up with two different loaded versions of GrammarProcessor. Then, the object you're instantiating is referring to the new GP2 class, while you're trying to cast it to GP1. Obviously impossible.

Is there a solution to this?

There's one that's been done before, but you won't like it. Facebook use it in their app to load a bunch of dex files with strong relationships between them. (It's there, before all the messing about with LinearAlloc):

we examined the Android source code and used Java reflection to directly modify some of its internal structures

I'm 90% sure they get the PathClassLoader that you're given (getSystemClassLoader()), get the DexPathList and override the dexElements private field to have an extra Element with the other dex file (apk in your case). Hacky as hell and I would advise against it.

It just occurred to me that if you don't want to use the newly loaded classes in a way that the framework sees them, you could extend from BaseDexClassLoader and implement the proper look-in-parent-before-trying-to-load behaviour. I haven't done it, so I can't promise it will work.

My advice? Just use remote services. This is what Binder is meant for. Alternatively, rethink your apk separation.

like image 98
Delyan Avatar answered Oct 04 '22 09:10

Delyan