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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With