2

I'm working on an Android application with pluggable .jar modules.

My problem is, when I load two different .jar files, the classes from the first .jar file cannot "see" (Class.forName()) classes from the second .jar file and vice-versa. I load external .jars from the main application with DexClassLoader.

=== Here is an example situation ===

We have two modules:

  • First.jar (with classes FirstA and FirstB, packaged in classes.dex)
  • Second.jar (with class SecondA, again in classes.dex)

By using DexClassLoader I load First.jar. Everthing is OK. Then again by using DexClassLoader I load Second.jar and SecondA cannot "see" FirstA or FirstB with Class.forName(). I get java.lang.ClassNotFoundException. When I check for FirstA from SecondA with Class.forName() I'm not setting the classloader parameter.

=== End of example situation ===

I was thinking that when a class is loaded into the dalvik/jvm, it will be visible/accessible from all other classes in that same dalvik/jvm. This is not the case or I'm totally wrong? What I have to do in order to make SecondA to see classes from First.jar (Class.forName())?

Any suggestions or resources I can learn from are welcome! I'm stuck guys!

EDIT:

  • All modules are loaded runtime.
  • Every module is loaded by different instance of DexClassLoader because I'm required to provide path to the .jar file and I cannot pack all modules in one jar.
Android5360
  • 33
  • 1
  • 5

3 Answers3

4

This is standard Java behavior; for class A to be visible to class B, class A has to be loaded by class B's loader or one of its parents. In fact, different class loaders can have distinct JVM classes with identical names.

You should generally use the same class loader for all your classes/jars unless you have a specific reason not to. If you do have a good reason, you need to ensure that you have the appropriate parent/child relationships between the loaders.

The semantic model for Java class loading is in chapter 5 of the JVM spec. Dalvik generally follows the semantics, though I'm not well-informed on the differences.

chrylis -cautiouslyoptimistic-
  • 75,269
  • 21
  • 115
  • 152
  • It sounds like you are implying that the poster is probably creating a unique class loader for each jar, and those siblings will not have the necessary relationship, while if they used a single class loader for the DVM lifetime, it would work as everything would either be in the same loader, or one with a parent/child relationship? – Chris Stratton Feb 14 '14 at 18:34
  • DexClassLoader's constructor requires a path to the target .jar/.apk file and I have multiple modules in different .jar files, so I cannot use only one DexClassLoader globally in the whole app. I was thinking about the parent/child relationships but won't the visibility principle be a problem? From what I've been reading I know that parent class loader cannot see classes loaded by its child. I will definatelly read every bit of the paper you provided. Thanks! – Android5360 Feb 14 '14 at 18:44
  • @ChrisStratton The OP stated that explicitly. – chrylis -cautiouslyoptimistic- Feb 14 '14 at 20:08
  • @Android5360 I haven't gotten into any significant Android development, but I believe the idea behind "library jars" is that the ADK compiles them all into one single `apk` for deployment. – chrylis -cautiouslyoptimistic- Feb 14 '14 at 20:09
  • @chrylis - it sounds to me like these are runtime plugins, so the SDK build system doesn't have an opportunity to unify them. Incidentally, "ADK" usually refers to something else entirely - the (hardware) accessory development kit. – Chris Stratton Feb 14 '14 at 20:14
  • 2
    @Android5360: The `DexClassLoader` constructor takes a `dexPath` argument, which is "the list of jar/apk files containing classes and resources, delimited by File.pathSeparator, which defaults to ":" on Android". So you *can* specify multiple jar files. – fadden Feb 14 '14 at 21:03
  • 1
    @fadden - that appears to mean they'd all have to be specified at once, right? So if the collection of (mutual-visibility) plugins changes, some sort of restart/reload cycle would be needed? – Chris Stratton Feb 14 '14 at 21:21
  • @ChrisStratton: yes, all the plugin jars get (re-)loaded together. Because Dalvik doesn't support class unloading, you either have to waste memory or restart the app when you want to update plugins. If class unloading were supported you could simply drop all references to all classes in all plugins and let the VM clean up, but Dalvik can't do that. – fadden Feb 14 '14 at 22:53
  • @chrylis: I don't know what is ADK? Maybe "Android SDK"? In the context of Android, you can release your libraries packed in .apk, .jar, or simply classes.dex (but without manifest) and load them on the target devices with the default tools provided by the Android SDK. But in my case I have multiples external libraries (I call them modules). These modules can depend on each other or only on the main application in which they are loaded into. I have multiple modules (say 30+) which I load on multiple occasions (on install, on update, etc). I cannot have only one .apk and pack everything in it. – Android5360 Feb 15 '14 at 08:07
  • @ChrisStratton: yes, these modules are loaded runtime. I've edited the question to reflect these details. – Android5360 Feb 15 '14 at 08:16
  • @fadden: I cannot specify all modules at once because of many reasons but the main is that I don't know about them in advance. When the app was released there were about 10 modules. Now, more than 30. Most probably, more will be developed. – Android5360 Feb 15 '14 at 08:20
  • 1
    Do they all implement common interfaces? If their public methods are all implementations of interfaces defined in the app APK, the plugins can treat their fellow objects as instances of that interface rather than a concrete class loaded in another loader. The app can see the interfaces because they're defined in the app APK, and all the plugins can see them because the app APK's class loader is the parent to the plugins' loader. (If they don't share a common interface then we can have a pedantic argument over whether these are really "plugins".) – fadden Feb 15 '14 at 16:32
  • That's the solution that's standard with OSGi (which goes to great lengths to separate bundles into different class loaders). – chrylis -cautiouslyoptimistic- Feb 15 '14 at 16:42
  • @fadden: I like your point! Yes, all plugins are represented by a number of files but there is always one file (implements IModule) which is the "entry point" of the module and has initialize(), start(), stop(), etc. Actually, in this case I don't care whether the plugin is instance of IModule and I don't want to treat the plugin as such instance. What I want, is to check whether that absolute class name exists in the JVM without instantiating another class loader (like in my ugly solution). – Android5360 Feb 19 '14 at 20:05
  • Currently there is no answer, or at least qualitative enough to be accepted as such. What should I do now guys? You tried to help me and I appreciate this. – Android5360 Feb 19 '14 at 20:07
1

WARNING: VERY UGLY "SOLUTION"

Given these 2 modules loaded with different DexClassLoaders (on different occasions, by different threads, etc):

  • Module-1 (module-1.jar with class ClassModule1)
  • Module-2 (module-2.jar with class ClassModule2)

For ClassModule1 to "see" ClassModule2 instead of doing this:

Class class = Class.forName("com.example.app.ClassModule2", false, ClassModule1.class.getClassLoader());

One should use this:

ClassLoader dexClassLoader = new DexClassLoader(
    manager.getPluginsFolder() + "Module-2.jar",
    this.context.getApplicationContext().getFilesDir().getAbsolutePath(), 
    null, 
    ClassModule1.class.getClassLoader()
);

Class class = Class.forName("com.example.app.ClassModule2", true, dexClassLoader);

This is ugly because of few things:

  • we are loading the module once again for sure because of the 2nd arg on forName() "true"
  • we have to know the path to Module-2.jar which is not good because modules are being management by a ModuleManager and not by themselves (separation of responsibility)

At least Dalvik (DexClassLoader) is smart enough not to optimize the .jar before loading it, but use the cached version generated by previous a DexClassLoader instance.

I guess the problem lies in: "At run time, a class or interface is determined not by its name alone, but by a pair: its binary name (§4.2.1) and its defining class loader. Each such class or interface belongs to a single run-time package. The run-time package of a class or interface is determined by the package name and defining class loader of the class or interface." (reference: chapter 5 of the JVM spec at http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html).

Any help appreciated.

Android5360
  • 33
  • 1
  • 5
0

Another idea...

Create a data structure that holds the class loaders used to load each plugin (maybe just ArrayList<ClassLoader>). Instead of calling Class.forName("ClassIWant"), write a method that iterates through every class loader and calls ClassLoader#loadClass("ClassIWant").

I'm assuming that your code is responsible for loading each plugin, which means you have a reference to the class loader handy, and that each plugin can have multiple classes that you don't have a priori knowledge of. (If each plugin has a single externally-facing class whose name you know in advance, you just need a map from String to Class that you add elements to as the plugins are loaded.)

How you deal with duplicate entries, and whether you need to cache results for performance, is up to you.

fadden
  • 51,356
  • 5
  • 116
  • 166
  • Yeah, something like this should do the trick. If I'm about to do that I'll have to change the main .apk file because the ModuleManager is part of it. But another problem comes into play in this case... I have thousands of installations of my app and all of them are outside of Google Play. If I'm about to update the app it won't happen without user interaction. And I don't want to bother the user in any way (Google Play update notification is by far the most I can expect from the user to accept). – Android5360 Feb 23 '14 at 22:51
  • Also, as far as I know I cannot invisibly uninstall/install .apk files. And even if I could, that would be by using something under the hood (and not the SDK) and most probably would require a rooted device. I would like to stick to the standard utilities because I've seen enough Android compatibility issues - Android SDK combined with different manufacturers (especially the cheap Chinese ones) is like CSS and Internet Explorer :) Fadden, thank you for time! – Android5360 Feb 23 '14 at 22:51