preface

Instant-run is a new feature introduced in Android Studio 2.0. It allows developers to quickly implement changes made during development, saving developers time. When you change the code, you don’t need to go through a full build process to generate a new APK and reinstall it, you just push the parts involved to the device, and in some cases you don’t even need to restart the current Activity to see the changes right away. This is awesome. This is really bad technology.

Hotfix (hot update) is used in scenarios similar to instant Run, so some implementations of hotfix frameworks also borrow ideas from instant Run.

use

Using Instant – Run requires Android Studio to be at least 2.0 and Android Gradle to be at least 2.0.0 ‘com. Android. Tools. Build: gradle: X.X.X’), minSdkVersion not less than 21.

AS and Gradle are required to build plug-ins because the implementation of instant-Run needs to intervene and modify the original construction process, and SDK is required to load patch.

After meeting the environmental requirements, click the RUN button of AS for the first time to install the app completely, and there will be a lightning-shaped button next to it. Then it will be developed in the project, and you can press this button to apply instant- Run at any time.

There are three methods for instant-run to load updates: Hotswap, coldswap, and warmswap

hotswap

If you just change the implementation logic of an existing method, instant-Run automatically applies Hotswap, and you don’t need to restart to see the actual change.

For example, you now have the following Activity:

public class MainActivity extends Activity implements View.OnClickListener  {
    private TextView mTv;
    private int count = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTv = new TextView(this);
        mTv.setText("click me!");
        mTv.setOnClickListener(this);
        setContentView(mTv);
    }

    private void toBeFix() {
        Toast.makeText(this, "origin count: " + count, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onClick(View v) {
        toBeFix();
        count++;
    }
}
Copy the code

The entire interface is a simple Textview, clicked will pop up a toast. Now click the button twice and toast will pop up one by one:

origin count: 0
origin count: 1
Copy the code

Then simply change the copy in toBeFix:

private void toBeFix() {
  Toast.makeText(this, "change count: " + count, Toast.LENGTH_SHORT).show();
}
Copy the code

When the instant-run application is applied, the Activity does not change, but when the button is clicked again, the toast pops up instead:

change count: 2
Copy the code

The copy does become the changed copy, and the count increases from the previous one, indicating that the Activity is indeed the original instance, with no restart and no data lost. This behavior is similar to an online hotfix that replaces the actual implementation logic without the user being aware of it.

If you find that you restart each time, follow this answer to turn off the automatic restart Settings for each time

warmswap

When changing not only code but also resource files, you can’t apply changes without affecting your current Activity, as HotSwap does. AS generates a new Resources._AP (similar to the packaging of resources during a normal build) and pushes it to the device, then restarts the current Activity for the new resources to take effect.

coldswap

Coldswap is applied if hotswap and Warmswap conditions are not met, such as adding or deleting methods, modifying the integration of classes, modifying AndroidManifest, etc.

Coldswap will also push the changes to the device, and then restart the entire app to see the changes.

The principle of

The following analysis is based on Android Studio 3.2 and Android Gradle plugin 3.2.1

An overview of the

Insatnu-run is designed to enable code or resource changes to be visible on the device without a full compilation and reinstallation. It does the following:

  • Get involved in the construction process and put the Jar package of the Instant-Run framework into the APK package of the application we are developing in order to run the instant-Run service in the app
  • There is a contentProvider in the instant-run.jar called into APK, which, when started in our application, opens a LocalServerSocket and listens, waiting for the AS to communicate
  • AS communicates with the socket mentioned above in our APP through ADB tools, sending various messages defined by the implementation, and app will make corresponding actions. It’s like a Server/Client architecture, where the Server runs in our app and the Client runs in AS
  • After the gradle plugin generates the corresponding product of this change, the client in AS is responsible for pushing the product to the device via ADB. The server decides whether to hack the AssertManager of the current application and restart the Activity or application depending on the type of the change
  • The whole process involves the synchronization and data exchange between Android Gradle plug-in, Android Studio’s Instant-Run client, and the Instant-Run Runtime in our APP
  • Android Studio is heavily involved in the process, so unlike normal builds that can be run from the./gradlew assembleDebug command line, instant-run can only be run via AS

The whole process

All of the things mentioned above can be summarized as follows:

The following two diagrams illustrate the modifications and injections to the Gradle plug-in build process:

The original build process is as follows:

The construction process after the introduction of instant-Run is as follows:

From Android Studio’s point of view, it is responsible for automatically analyzing the actions to be performed based on build artifacts and build-info.xml.

Working Principle of Patch

Reader: Ok, ok, ok, now that you’ve said so much, even if you push the changed code or resources to the device, then what? It doesn’t say how it works.

Indeed, how changes are pushed to devices is the foundation of the entire architecture. In general, three things were done to make the change work:

  • For resource changes, that is, warm swap, hack the current AssertManager
  • For simple code changes, namely Hot Swap, because staking is done in the very beginning of the build, a simple reflection is required
  • For Cold Swap, use ADB install-multiple -p for partial installation

Code staking and replacement

MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity MainActivity

Normally compiled class:

package com.example.wuyi.instantruntest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener {
    private TextView mTv;
    private int count = 0;

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.mTv = new TextView(this);
        this.mTv.setText("click me!");
        this.mTv.setOnClickListener(this);
        this.setContentView(this.mTv);
    }

    private void toBeFix() {
        Toast.makeText(this, "origin count: " + this.count, 0).show();
    }

    public void onClick(View v) {
        this.toBeFix();
        ++this.count;
    }
}
Copy the code

Instant-run compiler class:

package com.example.wuyi.instantruntest; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; import android.widget.Toast; import com.android.tools.ir.runtime.IncrementalChange; import com.android.tools.ir.runtime.InstantReloadException; public class MainActivity extends Activity implements OnClickListener { private TextView mTv; private int count; public static final long serialVersionUID = -3671979505056694483L; public static volatile transient com.android.tools.ir.runtime.IncrementalChange $change; public MainActivity() { IncrementalChange var1 = $change; if (var1 ! = null) { Object[] var10001 = (Object[])var1.access$dispatch("init$args.([Lcom/example/wuyi/instantruntest/MainActivity; [Ljava/lang/Object;)Ljava/lang/Object;", new Object[]{null, new Object[0]}); Object[] var2 = (Object[])var10001[0];  this(var10001, (InstantReloadException)null); var2[0] = this;  var1.access$dispatch("init$body.(Lcom/example/wuyi/instantruntest/MainActivity;[Ljava/lang/Object;)V", var2);  } else { super(); this.count = 0;  } } public void onCreate(Bundle savedInstanceState) { IncrementalChange var2 = $change; if (var2 ! = null) { var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});  } else { super.onCreate(savedInstanceState); this.mTv = new TextView(this); this.mTv.setText("click me! "); this.mTv.setOnClickListener(this); this.setContentView(this.mTv);  } } private void toBeFix() { IncrementalChange var1 = $change; if (var1 ! = null) { var1.access$dispatch("toBeFix.()V", new Object[]{this}); } else { Toast.makeText(this, "origin count: " + this.count, 0).show(); } } public void onClick(View v) { IncrementalChange var2 = $change; if (var2 ! = null) { var2.access$dispatch("onClick.(Landroid/view/View;)V", new Object[]{this, v}); } else { this.toBeFix();  ++this.count; } } MainActivity(Object[] var1, InstantReloadException var2) { String var3 = (String)var1[1];  switch(var3.hashCode()) { case -1230767868: super(); return; case -669279916: this(); return; default: throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s", var3, var3.hashCode(), "com/example/wuyi/instantruntest/MainActivity")); } } }Copy the code

You can see how much more content is added to the class file after the instant-run injection. Again carefully, you will find the key in the new public static volatile transient com. Android. View the ir. The runtime. IncrementalChange $change; This property. The initial value of $change is null, and both class files behave the same. When $change is not null, all methods of MainActivity are proxied to the access$dispatch method of $change. At this point, if the implementation logic in $change is a code change in development, then virtually all the actual calls to the methods in MainActivity are proxyed to the changed logic, making the change effective.

$change is the IncrementalChange interface type, with only one access$Dispatch method defined. What about the implementation of $change that is actually assigned to MainActivity? Seeing the actual implementation will tell you if any new changes have been applied. As described above, change origin in the text in the toBeFix method of MainActivity into change to perform an actual instant-run hotswap.

Then you can in the app/intermediates/transforms/transforms/instantRun find the actual implementation.

package com.example.wuyi.instantruntest;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tools.ir.runtime.AndroidInstantRuntime;
import com.android.tools.ir.runtime.IncrementalChange;
import com.android.tools.ir.runtime.InstantReloadException;

public class MainActivity$override implements IncrementalChange {
    public MainActivity$override() {
    }

    public static Object init$args(MainActivity[] var0, Object[] var1) {
        Object[] var2 = new Object[]{new Object[]{var0, new Object[0]}, "android/app/Activity.()V"};
        return var2;
    }

    public static void init$body(MainActivity $this, Object[] var1) {
        AndroidInstantRuntime.setPrivateField($this, new Integer(0), MainActivity.class, "count");
    }

    public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
        Object[] var2 = new Object[]{savedInstanceState};
        MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
        AndroidInstantRuntime.setPrivateField($this, new TextView($this), MainActivity.class, "mTv");
        ((TextView)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "mTv")).setText("click me!");
        ((TextView)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "mTv")).setOnClickListener($this);
        $this.setContentView((TextView)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "mTv"));
    }

    public static void toBeFix(MainActivity $this) {
        Toast.makeText($this, "change count: " + ((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "count")).intValue(), 0).show();
    }

    public static void onClick(MainActivity $this, View v) {
        toBeFix($this);
        AndroidInstantRuntime.setPrivateField($this, new Integer(((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "count")).intValue() + 1), MainActivity.class, "count");
    }

    public Object access$dispatch(String var1, Object... var2) {
        switch(var1.hashCode()) {
        case -1912803358:
            onClick((MainActivity)var2[0], (View)var2[1]);
            return null;
        case -1441621120:
            return init$args((MainActivity[])var2[0], (Object[])var2[1]);
        case -909773794:
            toBeFix((MainActivity)var2[0]);
            return null;
        case -641568046:
            onCreate((MainActivity)var2[0], (Bundle)var2[1]);
            return null;
        case 942020946:
            init$body((MainActivity)var2[0], (Object[])var2[1]);
            return null;
        default:
            throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s", var1, var1.hashCode(), "com/example/wuyi/instantruntest/MainActivity"));
        }
    }
}
Copy the code

What hotswap does is use reflection to assign MainActivity$change to an instance of MainActivity$Override by injecting it into the app runtime. All methods of MainActivity are then proxied to the Access $Dispatch method and dispatched to the corresponding method in MainActivity$Override based on the method signature.

A bit of a round. MainActivity$Override is basically a copy of MainActivity, with the only changes being the copy in the toBeFix method. There are three steps to complete:

  • All classes are staked (bytecode operations) on the first full compilation so that their methods can be propped
  • In incremental compilation of code changes, the Gradle plug-in generates proxy classes containing the changed code
  • The instant-run service in the app copies the $change field of the class whose code has been changed, so that all methods are forwarded to the proxy class, which contains the changed logic
  • Done!

The actual patch pushed to the device is app/intermediates/reload-dex/classes.dex. There are only two classes in the patch, one of which is MainActivity$override. Another class implements the instant – run the AbstractPatchesLoaderImpl role is pointed out which classes need to be patch, here is the MainActivity.

Replace AssertManager

In Warmswap, build a new AssertManager, call its addAssetPath method by reflection to add the path of the newly pushed resource to the device, and then replace all the assertManagers in use with the new one by reflection again. Restart to find the modified resource.

In the specific implementation view insatnt – run. Jar MonkeyPatcher# monkeyPatchExistingResources ()

Pushed to the actual equipment patch for app/intermediates/instant – run – resources/resources – debug. Ir. Ap_

Part of the installation

There is no need to install the full APK, only the updated parts will be re-installed

reference

  • Android.googlesource.com/platform/to…
  • Android.googlesource.com/platform/to…
  • Medium.com/google-deve…
  • Android.googlesource.com/platform/to…