ClassCastException while dynamically loading a class in Android
Asked Answered
A

1

8

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?

Arequipa answered 29/5, 2012 at 1:29 Comment(3)
grammarProcessor variable is of class GrammarProcessor.class??Homeo
Had you tried to use ClassLoader.getSystemClassLoader() instead of instantinating new PathClassLoader ?Physiotherapy
@Arequipa Wait, I'm super confused. What are you trying to do exactly? Return a Class<?> object by name? Use Class.forName. On Android, you only ever need to use new classloaders if you want to load classes from a separate .dex file (here's a good article). Please explain what you're trying to do. Edit: Are you loading from the same apk or a different one?Centenarian
C
12

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.

Centenarian answered 27/7, 2013 at 23:38 Comment(7)
That was extremely informative and good insides into the inner workings of the ClassLoader and the suggestion seem viable (not the FB hack). My situation, and maybe somewhat related to the OPs as well is that I have an application which I want to "sell" extensions but the extensions need to go beyond what AIDL is capable of doing, primarily working with non Parcelable objects. So what I would like to do is bring large parts of the application (such as full functioning fragments, which I have for the most part gotten to work with the exception of interacting with the current loaded classes).Tantalite
The issue now is: Caused by: java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementationTantalite
Ah, dexopt, I was afraid it was going to do something weird. Since you're essentially overwriting a class in the loaded dex, all references that have been pre-verified by the compiler have become invalid. It would take someone more knowledgeable than me to even begin getting around that (though I'm afraid it might be entirely impossible). My first suggestion is to try the FB hack and prepend the other dex, making sure to only ever load the foreign interface, not your own.Centenarian
Also, @J.Romero, thinking more about your fragments, the resources are the other huge barrier - how on earth are you going to access them from a different package?Centenarian
Thank you for everything dexopt seems to be the major blocker. Resources were actually the easier part so if the fragment was self dependent there would have not been a problem. Here's what I did, I might add a sample app to show the full implementation later: gist.github.com/jromero/6099470Tantalite
Ah, of course. Do you need shared user between the packages or does it work otherwise? There's also CONTEXT_INCLUDE_CODE, might want to see what that does - I think it might actually be relevant to your case.Centenarian
You can access Resources from another apk using the packageName (from Manifest) like: createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY).getResources(); Note that I don't know what kind of vulnerability IGNORE_SECURITY introduces and if you could make it work without that flag..Admass

© 2022 - 2024 — McMap. All rights reserved.