深刻理解Instant Run——原理篇

前言

Instant-run是Android Studio 2.0开始引入的新特性,它的做用是使开发者在开发时的改动能够很快地被应用,节省开发者的时间。当改动了代码以后,不须要进行完整的构建过程生成新的apk而且从新安装,只是把涉及到改动的部分push到设备上,某些状况下甚至都不须要重启当前Activity,立刻就能够看到改动。牛逼啊,简直黑科技。java

hotfix(热更新)的使用场景相似instant-run,因此有些hotfix框架的实现也借鉴了instant-run的思想。android

使用

使用instant-run要求Android Studio版本不低于2.0、用于构建的Android gradle插件版本不低于2.0.0(就是build.gradle里的classpath 'com.android.tools.build:gradle:x.x.x')、minSdkVersion不低于21。架构

对AS和gradle构建插件有版本要求是由于instant-run的实现须要介入并修改原来的构建过程,对sdk有要求是由于加载patch的要求。app

知足环境要求后,在第一次点AS的run按钮完整安装app后,旁边会有一个闪电状的按钮,后面接着在工程里开发,随时能够按这个按钮应用instant-run。框架

run

instant-run加载更新有三种方式:hotswapcoldswapwarmswapsocket

hotswap

若是只是改动现有方法的实现逻辑,instant-run会自动应用hotswap,不须要重启就能够看到实际改动。ide

好比如今有以下一个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++;
    }
}
复制代码

整个界面就是一个简单的Textview,点击了会弹出一个toast。如今把按钮点击两次,会依次弹出toast:gradle

origin count: 0
origin count: 1
复制代码

而后简单地把toBeFix中的文案改掉:ui

private void toBeFix() {
  Toast.makeText(this, "change count: " + count, Toast.LENGTH_SHORT).show();
}
复制代码

应用instant-run,Activity没有任何变化,可是再点击按钮,弹出来的toast变成了:

change count: 2
复制代码

文案确实发生变成了改变后的文案,并且count在以前的基础上递增,说明Activity确实仍是以前的实例,没有重启数据也没有丢。这个行为就相似线上hotfix,在用户无感知的状况下替换掉实际的实现逻辑。

若是发现每次都重启了,参考这个回答关掉每次自动重启的设置

warmswap

当改变的不止是代码,还涉及到资源文件的变更,就作不到像hotswap同样在不影响当前Activity的状况下应用变更了。AS会生成一个新的resources._ap(相似正常构建过程当中资源的打包)推到设备上,而后重启当前Activity来使新的资源能生效。

coldswap

若是不符合上面hotswap和warmswap的条件,好比说增长或者删减了方法、修改了类的集成关系、修改了AndroidManifest等,就会应用coldswap。

coldswap也是会把改动部分推送到设备上,而后会重启整个app,才能看到变更。

原理

如下分析基于Android Studio 3.二、Android gradle插件3.2.1

概述

insatnu-run的目的是使在代码或者资源改动以后,不用进行完整的编译和从新安装也能在设备上看到改动,为了实现这个目标,它主要作了下面几件事:

  • 介入构建过程,把instant-run框架的jar包打进咱们正在开发的应用的apk包里,目的是把instant-run的服务在app中跑起来
  • 打进apk中的instant-run.jar中有个contentProvider,它在咱们的应用中被启动后会打开一个LocalServerSocket并监听,等待AS进行通信
  • AS经过adb工具跟咱们app中上面提到的socket进行通信,发送实现定义好的各类消息,app会作出相应的动做。至关于一个Server/Client架构,server跑在咱们的app里,client跑在AS里
  • gradle插件把本次改动的对应产物生成后,AS中的client负责把产物经过adb推送到设备上,server根据本次类型是hot、warm仍是cold决定要不要hack当前应用的AssertManager以及重启Activity或者应用
  • 整个过程涉及Android的gradle插件、Android Studio中的instant-run client、打进咱们app中的instant-run runtime之间的同步和数据互传
  • 整个过程须要Android Studio深层参与,因此不像正常的build能够用./gradlew assembleDebug这个命令行的方式来进行,instant-run只能经过AS来执行

总体流程

上面讲到的全部事情,能够用下图归纳:

build

下面两个图描述了针对gradle插件构建过程的修改和注入:

原始的构建过程以下:

引入instant-run后的构建过程以下:

从Android Studio的角度来看,它负责根据build的产物和build-info.xml等自动分析出要执行的动做,更详细的流程图以下:

patch生效原理

读者:好好好知道了,说了那么多,就算你如今把改动后的代码或者资源推送到设备上了,而后呢?没说怎么生效啊。

确实,改动push到设备后怎么生效,是整个架构的基础。整体来讲,作了三件事情使改动生效:

  • 针对资源改动,也就是warm swap,hack掉当前的AssertManager
  • 针对简单的代码改动,也就是hot swap,由于在最开始的构建中就作了代码插桩的工做,只须要作一个简单的反射就能够
  • 针对cold swap,使用adb install-multiple -p进行部分安装

代码插桩和替换

仍是用上面那个最简单的MainActivity的代码作示例,先回去看下java源码,再看下正常编译后生成的class文件和instant-run下生成的class文件的对比,就能看到端倪了:

正常编译的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;
    }
}
复制代码

instant-run编译的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"));
        }
    }
}
复制代码

能够看到通过instant-run的注入,class文件里多了好多内容。再仔细对比下,就会发现关键在新增的public static volatile transient com.android.tools.ir.runtime.IncrementalChange $change;这个属性。$change初始值为null,这时其实两个class文件的行为是同样的。当$change不为null时,MainActivity的全部方法都被代理到了$changeaccess$dispatch方法上。这个时候,若是$change中的实现逻辑是开发中的代码改动,那么实际上MainActivity中的全部方法的实际调用都被代理到改动后的逻辑了,实现了使改动生效的目的。

$changeIncrementalChange接口类型,里面只定义了一个access$dispatch方法。那实际上被赋值给MainActivity的$change的实现是怎么样的?看到实际的实现就知道有没有应用新的改动了。仍是按照上面的描述,把MainActivity的toBeFix方法里的文案中的origin改为change,来一次实际的instant-run hotswap。

而后能够在app/intermediates/transforms/transforms/instantRun下找到实际的实现。

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"));
        }
    }
}
复制代码

hotswap作的事情就是经过注入到app中的runtime用反射把MainActivity的 $change赋值了成MainActivity$override的一个实例。而后MainActivity的全部方法代理到access$dispatch方法后再根据方法签名分发给MainActivity$override中的对应方法。

有点绕。说白了MainActivity$override基本上就是MainActivity的副本,惟一改动的地方就是toBeFix方法中的文案。作的事情总共是三步完成狸猫换太子:

  • 在第一次完整编译的时候给全部的类插桩(字节码操做),使它们的方法能被代理
  • 代码改动后的增量编译中,经过gradle插件生成包含了改动代码的代理类
  • 经过app中的instant-run服务给代码被改动的类的$change字段复制,这样全部方法都转发到了代理类,而代理类里就是改动后的逻辑
  • done!

实际推送到设备的patch为app/intermediates/reload-dex/classes.dex,里面只有两个类,一个类是MainActivity$override,另外一个类实现了instant-run中的AbstractPatchesLoaderImpl,做用是指明哪一个类须要被patch,在这里就是MainActivity。

替换AssertManager

在warmswap时,构建一个新的AssertManager,经过反射调用它的addAssetPath方法把新push到设备上的改动资源的路径加进去,而后仍是经过反射把当前全部使用中的AssertManager替换成这个新的,再重启就能找到修改后的资源。

具体实现查看insatnt-run.jar中的MonkeyPatcher#monkeyPatchExistingResources()

实际推送到设备的patch为app/intermediates/instant-run-resources/resources-debug.ir.ap_

部分安装

这个就很少说了,就是adb提供的功能,不须要安装完整的apk,只会从新安装更新的部分

参考

相关文章
相关标签/搜索