Android: Loading and executing code at runtime – DexClassLoader

By | January 27, 2015

Sometimes you may want to add functionality to an application dynamically by adding new jar libraries.
Custom classloaders can be used to execute code not installed as part of an application.
Examples can range from security (hiding code in resources), mocking/testing, Object-relational mapping, etc.

In Android, the DexClassLoader allows an application to load classes from jar and apk files.

In this post we will see how we can create apk files to be loaded into another application during runtime.

The used IDE was Android Studio.

1 Create the apk to be loaded in DexClassLoader

Start by creating a new Android project with no Activity. When using DexClassLoader, we need to know the package and class names of the classes we want to load. ClassLoaders do not provide any method to retrieve such a list of classes. So lets try to create a package name and class that should be present in all your apks to be loaded:

package com.registry;

public class Registry {

    public static ArrayList<Class<?>> _classes = new ArrayList<Class<?>>();

    static{
        _classes.add(ClassToBeImported.class);
        //more classes here
    } 
}

In this example we created a package called “com.registry” and a class named “Registry”. We added a static ArrayList with the classes we want to be able to load. Note that these classes are statically added into our ArrayList.

Now we just need to implement our “ClassToBeImported”:

package test.aknahs.com.dexapptest;

public class ClassToBeImported {

    public static ClassLoader method(){
        Log.v("ClassToBeImported", "called method of class " + ClassToBeImported.class.getName());
        return ClassToBeImported.class.getClassLoader();
    }
}

I implemented a Method called “method” with no arguments. This method returns a ClassLoader so we can compare the ClassLoader used by the loaded class with the ClassLoader of application that actually loads it.

You can create any number of methods you want. The decision to add static was just to simplify this example, as this way we won’t have to instantiate the “ClassToBeImported” through reflection but rather just get its method and invoke with a null instance.

Now in order to generate the apk file in Android Studio, open “Run > Edit Configurations…”. Press the plus sign on the upper left and pick “Gradle”. On the “Gradle project” field select the recently created project “build.gradle” present on the project root. In the “Tasks” field write “assemble”:

run-conf
Click ok and just run your project (Shift + F10).

You should now have an apk file in <project_root_folder>/app/build/outputs/apk. You might have to play with library dependencies before actually loading the application (Open Module Settings (F4) > app > Dependencies).

2 Create an app that uses DexClassLoader

So now we just need to add class loading capabilities to our application.

Create a new project with an Activity (just to see when it actually executes, its usage is up to you). And add a new function called “loadDexClasses”:

public static void loadDexClassses() {
   if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      Log.v("loadDexClassses", "LoadDexClasses is only available for ICS or up");
   }
   File[] files = new File("data/local/tmp/testjars/").listFiles();

   if (files == null) {
      Log.v("loadDexClasses", "There was no " + "data/local/tmp/testjars/");
      return;
   }

   Log.v("loadDexClasses", "Dex Preparing to loadDexClasses!");

   for (File file : files) {
      // following code here
   }
}

So, in the example we assume that the apk files will be placed in a folder called “data/local/tmp/testjars/”. Go on and create the “testjars” folder in your Android device:

$ adb shell
# mkdir data/local/tmp/testjars
# exit

Now place your apk file in that folder:

$ adb push <project_root_folder>/app/build/outputs/apk/app-debug.apk data/local/tmp/testjar/load.apk

Now lets try to create our new DexClassLoader:

for (File file : files) {
   //The following optDexFolder is an arbitrary folder name.
   //E.g., "dex-" + getApplicationContext().getPackageName().hashCode()
   //This way every app will have its own optimized dex folder.
   final File tmpDir = new File("data/local/tmp/optdexjars/" + optDexFolder + "/");

   tmpDir.mkdir();

   final DexClassLoader classloader = new DexClassLoader(
      file.getAbsolutePath(), tmpDir.getAbsolutePath(),
      "data/local/tmp/natives/",
      ClassLoader.getSystemClassLoader());

   Log.v("loadDexClasses", "Searching for class : "
      + "com.registry.Registry");

   Class<?> classToLoad = (Class<?>) classloader
      .loadClass("com.registry.Registry");

   Field classesField = classToLoad.getDeclaredField("_classes");

   ArrayList<Class<?>> classes = (ArrayList<Class<?>>) classesField.get(null);

   for(Class<?> cls : classes) {
      Log.v("loadDexClasses", "Class loaded " + cls.getName());
   }

Note that DexClassLoader takes path to the apk file to open, a folder to place the optimized dex files, a folder with the required native libraries and the parent ClassLoader.
While in this example we used an application created folder under the application UID (generally unique per application), Android documentation advises to use Context.getCodeCacheDir instead.
If your application depends on native libraries (e.g. .so files), you can place them in “data/local/tmp/natives” and they will be loaded with your apk.
Finally, we used the new ClassLoader to load the “com.registry.Registry”. Then using reflection we retrieve the “_classes” field that contains the ArrayList with all the Classes we wanted to load.

If you want to invoke the loaded class method, you can try as follows:

for (Class<?> cls : classes) {
   Log.v("loadDexClasses", "Class loaded " + cls.getName());
   if (cls.getName().contains("ClassToBeImported")) {
      Method m = cls.getMethod("method");

      ClassLoader xb = (ClassLoader) m.invoke(null);

      if (xb.equals(ClassLoader.getSystemClassLoader()))
         Log.v("loadDexClasses", "Same ClassLoader");
      else
         Log.v("loadDexClasses", "Different ClassLoader");
      }
}

Finally just invoke this method on your code, e.g. I placed it on the OnCreate method of my Activity, perhaps not the best place to put it:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadDexClassses();
    }

Execute the application and check your logcat.

Can you guess if it will print “Same ClassLoader” or “Different ClassLoader”? How does this affect your application? Can you filter objects based on class type or can there be multiple definitions of the same class on different class loaders?

Hope you enjoyed this tutorial and good luck! 🙂

21 thoughts on “Android: Loading and executing code at runtime – DexClassLoader

  1. Michael Cohen

    Just curious – what’s the hashcode for? Looks like you are passing a hash code arg to loadDexClasses, but, further up it is defined as an argument-less method.

    Reply
    1. admin Post author

      Well spotted, I tried to extract this example from a project I did and simplify it.
      It is basically what I use as the optDexFolder so that every app using this technique will have its own folder with optimized dex.
      I will fix the example. Thanks 🙂

      Reply
  2. Angel

    First of all, thanks for this tutorial.
    I have tried do the same but when the classloader tries to find the class it throws a ClassNotFoundException. The apk file is in the correct path and the classname is the correct too.
    I need to use a specific a compile sdk version or some gradle configuration?
    What can be the issue?

    Reply
    1. admin Post author

      Have you checked the logcat for errors? More specifically when DexClassLoader is instantiated.
      Depending on the version of Android you might have errors related to r/w permissions for /data/local/tmp.
      Either do a chmod of that folder or use the sdcard instead (adding the write to external storage permission in the manifest).
      If that is not the case I would probably need to see your code and the error.
      Either post it here or in stack overflow and link here.

      Reply
      1. Angel

        Finally I resolved it. I saw that I could write to the internal storage, because my app created the correct directory to place the optimized dex files (the last level directory must be created by the app in order to the owner of this directory be the same that the user).

        I changed permission for my load.apk and it worked fine.

        To call a method, I use this code, where cls is the loaded class:

        Method m = cls.getMethod(“methodName”);
        m.invoke(cls.newInstance()));

        Thanks!

        Reply
        1. admin Post author

          Happy it worked for you! 🙂

          Out of curiosity, which Android version are you using? and feel free to drop any suggestions that might make this tutorial easier for others to follow.

          Cheers!

          Reply
          1. Angel

            I’m using an Android emulator with version 4.4.2 KitKat (API 19)

    1. admin Post author

      Indeed i tested with KitKat. Does targeting a lower SDK (<=23) work for you? I was using this to load functionality into closed source apps so I guess I will have to look into it as well at some point. Thanks for the heads up and do post if you find a work around!

      Reply
    1. admin Post author

      Your jar looks useful. Can you share the source? I am a bit reluctant to link to a “closed” jar and lazy to decompile it myself. Thanks for the comment and for the effort!!

      Reply
  3. Pingback: Reduce the size of your APK bullet point – Jacques Giraudel – Freelance Android Developer

  4. gajraj

    Does it slow down the application performance, It uses reflection, could you suggest

    Reply
    1. admin Post author

      It depends on the use case. Reflection will only affect performance if intensively used.
      In this case its just used to load the classes and execute them, if the heavy load is on the executed methods/constructors then answer is likely no.
      For most cases I would say performance overhead is negligible.

      Reply
      1. gajraj

        Ok, Thank you, I read on some link that DexClassLoader is different from Reflection, it is not so much slow like reflection, Is it true?

        Reply
  5. Arun

    I have to use a constructor to initialize my class in the app. For example, my ClassToBeImported has a constructor with a parameter of class type B. I used class B in the DexClassLoader app and compiled it successfully into an APK. I replicated the same class B in my app which loads the class. I am able to load it using your method, but when I try to instantiate it, it throws an “IllegalArugmentException” and says: “method com.registry.myapplication.ClassToBeLoaded. argument 1 has type com.registry.B, got com.registry.B.”

    Have you tried instantiating the class instead of calling a static method? I used cls.getConstructors() to verify that the constructor does exist and used constructor.newInstance(…) with the right parameters to try and instantiate it.

    Thank you!

    Reply
    1. Arun

      I tried changing ClassToBeLoaded’s constructor and passed in an android.content.Context as the only argument. I was able to instantiate the class successfully when I did that. But when I use a custom defined class as one of the parameters, then I can’t do it.

      Also, when I instantiate the class, I can’t cast it back to my custom class. I have to keep it an Object and use Method.invoke to call functions.

      Maybe these two things are related. It seems like when I use a custom class in one project, compile it into an APK, and then try to use that same class (which is also included in my second project), it doesn’t seem to work. I tried using the same class as a constructor parameter and to try and cast back the instantiated Object.

      Is it possible that DexClassLoader won’t recognize custom classes? Or, is this the point of the data/local/tmp/natives in your code?

      Reply
  6. Arun

    Got it! Both reasons were related to the Classloader. It turns out that in Java, a class is identified by it’s name, package *and the Classloader* that was used to load it. Although I used the same name and package in both projects, the loaded class was different than the one I already had.

    I changed the DexLoader code to this and it works fine now:

    final DexClassLoader classloader = new DexClassLoader(
    appfile.getAbsolutePath(), tmpDir.getAbsolutePath(),
    “data/local/tmp/natives/”,
    getClassLoader());

    This article was helpful to understand this: https://www.ibm.com/developerworks/java/library/j-dyn0429/index.html?ca=drs-

    Reply

Leave a Reply