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! 🙂

11 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

Leave a Reply