Structural Class Redefinition and Apply Changes

Alan Leung
Android Developers
Published in
4 min readSep 1, 2020

--

Introduction

Apply Changes is a feature in Android Studio that we introduced in Android Studio 3.5 to help you iterate quickly on changes that you’ve made to your app. Apply Changes relies on the JVMTI API to guide what changes can be applied in this manner. In Android 11, the Android Runtime (ART) introduces an extension called Structural Class Redefinition to the JVMTI API. This extension opens a category of use cases for Apply Changes utilizing an Android 11 device in development. More complex edits can now be quickly deployed with Apply Changes while the application is still running. This includes:

  • Adding methods (Android Studio 4.1)
  • Adding Resource files (Android Studio 4.2)
  • Adding static fields (Android Studio 4.2)

This allows you to reduce turn-around time during development and maximize productivity. In this post, we’ll explore how we implemented this in Android Studio.

Android Studio implementation

Apply Changes has been designed from the ground up to continuously evolve with each new iteration of the Android Runtime by automatically utilizing new features and capabilities.

In the case of Structural Class Redefinition, classes with added methods are sent to ART not unlike the previous Android version. A new API entry point has been added so you’ll need to upgrade your Android Studio to version 4.1 or later to take advantage of adding methods, both static and virtual, on-the-fly.

Adding variables, however, requires a new analysis done in Android Studio. When a new variable is added, ART does not make an attempt to determine what value should be assigned to it. (Stay tuned for a future Medium blog on ART’s implementation details of Structural Class Redefinition). Instead, added variables will only be initialized as the default primitive value or null and it will be up to Android Studio to determine how it should be initialized.

This process is nontrivial. Consider the case where you add a static long y to a class‒the initial assignment happens during class loading. Consider this:

public class example {   public final static long x = System.currentTimeMillis();   public final static long y = System.currentTimeMillis();}

Should this class be loaded, both x and y should have values very close to each other. In the case where y is added as an invocation of Apply Code Changes, the correct value of y is not easily computable. In fact, what the value of y should be assigned to is arguable for it to most closely mimic the program where y is initialized where the class is loaded. Since both currentTimeMillis()were invoked during static initialization (<clinit> method), Apply Changes will continue to start true to the policy of not re-running any parts of the <clinit> method. Therefore the added value y will obtain the value 0.

Fortunately, Apply Changes already uses D8 for DEX file analysis and as part of that process, in the latest version of Android Studio, Apply Changes is able to leverage D8’s newly introduced Inspector API. This lightweight inspection API is able to compute some extra information as part of the DEX comparison process with very little overhead added (only changed Java classes are inspected). A set of meta-information about newly added variables is attached to the Apply Changes request ProtoBuf to the device.

On the device, before Android Studio communicates our changes to the VM, the Java agent inspects the current loaded classes that are slated for replacement. By comparing fields of the soon-to-be-replaced classes and the newly compiled classes, a list of fields that are newly added and their respective initial value are computed. The agent then temporarily suspends all the other threads to prevent a thread from accessing any of the newly added non-initialized fields before performing the swap. Should the swap request be successful, it will then initialize the newly added fields with the appropriate variables.

Limitations and upcoming features

As of Android Studio 4.2 Canary 3, this feature is only available for cases where a new static primitive is added. As a byproduct, this facilitates adding values in R.class which gives Apply Changes the ability to add new resources.

One thing to keep in mind that is true to all usages of Apply Changes: the semantic of the program can never be the same as the one where you rebuild and restart the program. Think of the case where a constructor is changed; objects that have been constructed with the old constructor do not get reconstructed. This also applies to static variables as well since <clinits> will not be re-invoked.

We hope that everyone will be able to leverage this new addition to Android Studio to improve productivity. As always, we welcome everyone to provide us feedback on your usage of Apply Changes in our issue tracker and let us know what you’d like to see improved.

--

--

Alan Leung
Android Developers

Senior Software Engineer, Android Studio, Google Inc.