深刻理解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,只會從新安裝更新的部分

參考

相關文章
相關標籤/搜索