從零開始分析lnstantrun源碼

背景

Android Studio 2.0 中引入的 Instant Run 是 RunDebug 命令的行為漩蟆,可以大幅縮短應用更新的時間。盡管首次構建可能需要花費較長的時間,Instant Run 在向應用推送后續(xù)更新時則無需構建新的 APK系宫,因此维费,這樣可以更快地看到更改种蝶,曾經是Android開發(fā)人員的開發(fā)的利器,現在已被廢棄靡羡,用Apply changes替代系洛。但我們仍然可以學習它的源碼,提升自己的開發(fā)技術略步。

使用

instant-run加載更新有三種方式hot swap碎罚,cold swap,warm swap,當然在不同gradle版本中不一定都有這個三個功能纳像。

Hot Swap(熱插拔)

hot swap是所有swap方式中效率最高的荆烈,不需要重新安裝和重啟,但是hot swap不會對程序中的對象重新進行初始化,也就是說某些場景需要重啟Activity才能看出具體的變更內容憔购,Android Studio對于hot swap這種情況默認是重啟Activity的宫峦,當然你也可以到設置中去改變這一默認行為,具體路徑是 Settings -> Build, Execution, Deployment -> Instant Run -> Restart activity on code changes玫鸟。Hot Swap適用的條件比較少导绷,只有一種情況會被視為hop swap類型,就是修改一個現有方法中的代碼屎飘。

Warm Swap(溫插拔)

只有一種情況會被Android Studio視為warm swap類型妥曲,就是修改或刪除一個現有的資源文件,要求必須重啟Activity

Cold Swap(冷插拔)

Android Studio會自動記錄我們項目的每次修改钦购,然后將修改的這部分內容打成一個dex文件發(fā)送到手機上檐盟,盡管這種swap類型仍然不需要去安裝一個全新的APK,但是為了加載這個新的dex文件押桃,整個應用程序必須進行重啟才行葵萎。另外,cold swap的工作原理是基于multidex機制來實現的唱凯,在不引入外部library的情況下羡忘,只有5.0及以上的設備才支持multidex,5.0以下只能重新安裝磕昼。該模式在3.0時候被廢棄卷雕。

cold swap的使用場景非常多,如下

  • 添加票从、刪除或修改一個注解爽蝴,字段,方法
  • 添加一個類
  • 修改一個類的繼承結構
  • 修改一個類的接口實現
  • 修改一個類的static修飾符
  • 涉及資源文件id的改動
使用時的注意點:
  1. 如果應用的minSdkVersion小于21纫骑,可能多數的Instant Run功能會掛掉蝎亚,這里提供一個解決方法,通過product flavor建立一個minSdkVersion大于21的新分支先馆,用來debug发框。

  2. Instant Run目前只能在主進程里運行,如果應用是多進程的煤墙,類似微信,把webView抽出來單獨一個進程仿野,那熱铣减、溫拔插會被降級為冷拔插。后面的版本好像就只能在主進程中了脚作,冷插拔都沒了

  3. 在Windows下葫哗,Windows Defender Real-Time Protection可能會導致Instant Run掛掉缔刹,可用通過添加白名單列表解決。

  4. 暫時不支持Jack compiler劣针,Instrumentation Tests校镐,或者同時部署到多臺設備。

Instant Run的設計需要Android構建工具和Android Studio的配合捺典,相關的源碼在兩個庫中鸟廓,這兩個庫都在AOSP的源碼中,Google使用基于git開發(fā)的版本管理工具repo進行管理襟己,全部開源的代碼及其龐大引谜,我們只需要下載相關的git倉庫就行。

配置代理

配置代理擎浴,我的代理使用的是藍燈员咽,有HTTP和SOCKS端口,使用HTTP速度只有幾百kb退客,而使用SOCKS真是快啊,這玩意這么快在下載一些源碼庫的時候非常有用

HTTP(S)代理服務器:127.0.0.1:54504
SOCKS代理服務器:127.0.0.1:54505
git config --global http.proxy http://127.0.0.1:1080(本機的端口)链嘀,可能會遇到Time out萌狂,使用下面那個
git config --global http.proxy socks5://127.0.0.1:54505

配置代理的常用命令
//設置git代理
git config --global http.proxy socks5://127.0.0.1:54505
//取消git代理:
git config --global --unset http.proxy
git config --global --unset https.proxy
//查看全局的所有代理
env | grep -i proxy
//只在當前shell窗口配置,也可以在配置文件中配置怀泊,那就是永久配置了
export http_proxy="http://127.0.0.1:1087"
//取消本機http代理
unset http_proxy

查看 端口所在線程 lsof -i:8080(端口)
查看mac終端端口命令 netstat -AaLlnW (相當于linux的 netstat -lntp)

查看端口是否被占用: sudo lsof -i :8080
結束占用端口的所有進程: lsof -P | grep ':8080' | awk '{print $2}' | xargs kill -9

獲取源碼

Instant Run的設計需要Android構建工具和Android Studio的配合茫藏,這個庫中有Android gradle插件的代碼,instant-run框架的代碼全部在其中的instant-run目錄中

git clone https://android.googlesource.com/platform/tools/base

在3.5版本的Android Studio之后霹琼,Google使用了新的apply change架構代替了instant run务傲,所以最新的代碼中看不到,需要切換到studio-3.2.1這個tag枣申,最新的apply change使用時有諸多限制

  • 您使用調試構建變體來構建應用的 APK售葡。
  • 您將應用部署到搭載 Android 8.0(API 級別 26)或更高版本的目標設備或模擬器上。
需要重啟應用(不是重啟Activity)才能實現的代碼更改

某些代碼和資源更改必須在重啟應用之后才能應用忠藤,其中包括以下更改:

  • 添加或刪除方法或字段
  • 更改方法簽名
  • 更改方法或類的修飾符
  • 更改類繼承行為
  • 更改枚舉中的值
  • 添加或移除資源
  • 更改應用清單
  • 更改原生庫(SO 文件)

所以我感覺這玩意以后可以用來在修改代碼邏輯的時候使用挟伙,使用的范圍非常有限。

知乎上也有人提問Android Studio3.5提供的Apply Changes是什么原理模孩?

這里引用weishu大佬的回答尖阔,“猜測是使用JVMTI實現的,JVMTI 的全稱是 JVM Tool Interface榨咐。它是 Java 虛擬機(ART)實現的一部分介却,包含了虛擬機中線程 / 內存 / 類 / 方法 / 變量 / 事件 / 定時器處理等等 20 多類功能。比如:內存控制和對象獲取块茁、線程和鎖齿坷、調試功能。
對這個「Apply Changes」來說,比較重要的應該是 ClassTransform 和 ClassRedefine胃夏;它允許虛擬機在運行時動態(tài)修改類(Redefine只在9.0上實現了)轴或。比如說 Activity 這個 class,你可以通過此接口在字節(jié)碼層面往里面直接添加方法/修改方法仰禀,然后虛擬機會為你重新加載這個類照雁,之后這個被改過的類就是原來那個貨真價值的 Activity 類。所以答恶,這個技術跟 Instant Run/Robust 編譯期字節(jié)碼編織 / ClassLoader 替換 / AndFix 方法替換那種動態(tài)修改完全不是一個層面的東西饺蚊,這是 運行時動態(tài)字節(jié)碼編織

另一個需要下載的庫中有Android Studio相關的源代碼悬嗓,其中可以看到AS是如何配合instant-run工作的污呼,需要切換到studio-3.2.1這個tag

git clone https://android.googlesource.com/platform/tools/adt/idea

我們可以在build.gradle中添加一行代碼,查看啟動gradle的命令和全部參數

println getGradle().getStartParameter()

我在3.4.2的Android Studio中看到 projectProperties={android.optional.compilation=INSTANT_DEV包竹,這里就表示開啟instant-run支持了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=F:\GitAndroid\RxDemo, searchUpwards=true, projectProperties={android.optional.compilation=INSTANT_DEV, android.injected.build.density=xxhdpi, android.injected.coldswap.mode=MULTIAPK, android.injected.build.api=28, android.injected.invoked.from.ide=true, android.injected.build.abi=arm64-v8a,armeabi-v7a,armeabi, android.injected.restrict.variant.name=debug, android.injected.restrict.variant.project=:app}, systemPropertiesArgs={}, gradleUserHomeDir=C:\Users\Jackie\.gradle, gradleHome=C:\Users\Jackie\.gradle\wrapper\dists\gradle-4.4-all\9br9xq1tocpiv8o6njlyu5op1\gradle-4.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=8, buildCacheEnabled=false, interactive=false}:app:buildInfoDebugLoader

在3.6.0的Android Studio中就看不到了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=/Users/jackie/Desktop/WorkPlace/AndroidWorkPlace/MyApplication2, searchUpwards=true, projectProperties={android.injected.build.density=xhdpi, android.injected.build.api=29, android.injected.invoked.from.ide=true, android.injected.build.abi=x86}, systemPropertiesArgs={idea.active=true, idea.version=3.6}, gradleUserHomeDir=/Users/jackie/.gradle, gradleHome=/Users/jackie/.gradle/wrapper/dists/gradle-5.6.4-all/ankdp27end7byghfw1q2sw75f/gradle-5.6.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=12, buildCacheEnabled=false, interactive=false, writeDependencyLocks=false}app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'com.alibaba:arouter-compiler:1.2.2'.

到了android.gradle插件的執(zhí)行邏輯里燕酷,會被轉成如下枚舉定義,分別表示不同的編譯類型:

//源碼路徑 gradle3.0.0版本
//Users/jackie/Desktop/WorkPlace/InstantRun/base/build-system/builder-周瞎、model/src/main/java/com/android/builder/model/OptionalCompilationStep.java

/**
 * enum describing possible optional compilation steps. This can be used to turn on java byte code
 * manipulation in order to support instant reloading, or profiling, or anything related to
 * transforming java compiler .class files before they are processed into .dex files.
 */
public enum OptionalCompilationStep {

    /**
     * presence will turn on the InstantRun feature.
     */
    INSTANT_DEV, 
    /**
     * Force rebuild of cold swap artifacts.
     *
     * <p>Dex files and/or resources.ap_ for ColdswapMode.MULTIDEX and some split APKs for
     * ColdswapMode.MULTIAPK.
     */
    RESTART_ONLY,
    /**
     * Force rebuild of fresh install artifacts.
     *
     * <p>A full apk for ColdswapMode.MULTIDEX and all the split apks for ColdswapMode.MULTIAPK.
     */
    FULL_APK,
}

Gradle4.1的Instant Run

源碼分析是基于Gradle4.1版本研究的Instant Run苗缩,但是這個版本的Instant Run功能已經削減很多了,下面還會介紹其他版本的Gradle

運行后反編譯app-debug.apk會找到多個dex(一般是兩個)声诸,一開始是通過dex2jar-2.0和jd-gui酱讶,但是有時候有些方法無法進行反編譯而是依舊顯示初始的字節(jié)碼,很不方便閱讀彼乌,后來使用了jadx-gui進行直接反編譯apk泻肯,使用很方便,但依舊還是會有些方法還是顯示字節(jié)碼慰照,所以我是兩者交叉著看灶挟,但是有時候甚至兩者都是字節(jié)碼,只能上網上直接找別人的博客代碼了毒租。

因為我研究的版本是基于Gradle4.1的膏萧,僅僅剩下可憐的熱插拔和處理資源補丁,而且我還找不到intant-run.zip了,所以我找不到項目中的代碼了蝌衔,不在dex文件中榛泛,原本這玩意解壓apk之后就有了,所以暫時只能在build下的目錄里面尋找了噩斟,后面再看看這些文件時如何弄到apk當中曹锨。

InstantRunContentProvider的onCreate方法中初始化Socket

  public boolean onCreate() {
    if (isMainProcess()) {  //只支持主進程
      Log.i("InstantRun", "starting instant run server: is main process");
      Server.create(getContext()); 
      return true;
    } 
    Log.i("InstantRun", "not starting instant run server: not main process");
    return true;
  }

然后啟動一個socket監(jiān)聽Android Studio推送的消息

  private class SocketServerThread extends Thread {
    private SocketServerThread() {}

    public void run() {
      while (true) {
        try {
          LocalServerSocket localServerSocket = Server.this.serverSocket;
          if (localServerSocket == null)
            return; 
          LocalSocket localSocket = localServerSocket.accept();
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Received connection from IDE: spawning connection thread"); 
          (new Server.SocketServerReplyThread(localSocket)).run();
          if (wrongTokenCount > 50) {
            if (Log.isLoggable("InstantRun", 2))
              Log.v("InstantRun", "Stopping server: too many wrong token connections"); 
            Server.this.serverSocket.close();
            return;
          } 
        } catch (Throwable throwable) {
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Fatal error accepting connection on local socket", throwable); 
        } 
      } 
    }
  }

然后在SocketServerReplyThread的run方法值接受數據并處理

//處理補丁 
private int handlePatches(List<ApplicationPatch> paramList, boolean paramBoolean, int paramInt) {
    if (paramBoolean)
      FileManager.startUpdate(); 
    for (ApplicationPatch applicationPatch : paramList) {
      String str = applicationPatch.getPath();
      if (str.equals("classes.dex.3")) { //如果有classes.dex.3處理熱插拔
        paramInt = handleHotSwapPatch(paramInt, applicationPatch);
        continue;
      } 
      if (isResourcePath(str))
        //處理資源補丁
        paramInt = handleResourcePatch(paramInt, applicationPatch, str); 
    } 
    if (paramBoolean)
      FileManager.finishUpdate(true); 
    return paramInt;
  }

這里先來看看ApplicationPatch是什么

public static List<ApplicationPatch> read(DataInputStream paramDataInputStream) throws IOException {
    int j = paramDataInputStream.readInt();
    if (Log.logging != null && Log.logging.isLoggable(Level.FINE))
      Log.logging.log(Level.FINE, "Receiving " + j + " changes"); 
    ArrayList<ApplicationPatch> arrayList = new ArrayList(j);
    for (int i = 0; i < j; i++) {
      String str = paramDataInputStream.readUTF();
      byte[] arrayOfByte = new byte[paramDataInputStream.readInt()];
      paramDataInputStream.readFully(arrayOfByte);
      arrayList.add(new ApplicationPatch(str, arrayOfByte));
    } 
    return arrayList;
  }

可以看到ApplicationPatch是從Socket接收到的數據輸入流中調用readFully來讀取的,關于readFully的使用while循環(huán)判斷byte數組是否已經讀滿所有數據剃允,如果沒有讀滿則繼續(xù)讀取補充直到讀滿為止沛简,從而改善輸入流出現空檔齐鲤,造成read方法直接跳出的問題。即通過緩沖來保證數量的完整椒楣,也算是常用的一種方法给郊。所以以后若要讀取特定長度的數據,使用readFully讀取更加安全捧灰。

1.處理熱插拔

下面來看看是如何處理熱插拔的

//處理熱插拔
private int handleHotSwapPatch(int paramInt, ApplicationPatch paramApplicationPatch) {
    if (Log.isLoggable("InstantRun", 2))
      Log.v("InstantRun", "Received incremental code patch"); 
    try {
      //創(chuàng)建或獲取“data/data/applicationid/files/instant-run/dex”文件路徑
      String str1 = FileManager.writeTempDexFile(paramApplicationPatch.getBytes());
      if (str1 == null) {
        Log.e("InstantRun", "No file to write the code to");
        return paramInt;
      } 
      if (Log.isLoggable("InstantRun", 2))
        Log.v("InstantRun", "Reading live code from " + str1);

      String str2 = FileManager.getNativeLibraryFolder().getPath();
      //反射構造AppPatchesLoaderImpl實例
      Class<?> clazz = Class.forName("com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, (ClassLoader)new DexClassLoader(str1, this.context.getCacheDir().getPath(), str2, getClass().getClassLoader()));
      try {
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Got the patcher class " + clazz); 
        PatchesLoader patchesLoader = (PatchesLoader)clazz.newInstance();
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Got the patcher instance " + patchesLoader); 
        //獲取熱修復所要替換的類的classname
        String[] arrayOfString = (String[])clazz.getDeclaredMethod("getPatchedClasses", new Class[0]).invoke(patchesLoader, new Object[0]);
        if (Log.isLoggable("InstantRun", 2)) {
          Log.v("InstantRun", "Got the list of classes ");
          int j = arrayOfString.length;
          for (int i = 0; i < j; i++) {
            String str = arrayOfString[i];
            Log.v("InstantRun", "class " + str);
          } 
        } 
        //執(zhí)行AppPatchesLoaderImpl的load方法進行類修復
        boolean bool = patchesLoader.load();
        if (!bool)
          paramInt = 3; 
      } catch (Exception exception) {}
    } catch (Throwable throwable) {
      Log.e("InstantRun", "Couldn't apply code changes", throwable);
      paramInt = 3;
    } 
    return paramInt;
  }
//AppPatchesLoaderImpl實現抽象類AbstractPatchesLoaderImpl淆九,重寫getPatchedClasses方法,重寫方法中有提供需要熱更新的類
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
  public abstract String[] getPatchedClasses();

  public boolean load() {
    try {
      //《《《《《《最關鍵的方法毛俏,一個個替換類中要替換的方法》》》》》》
      for (String str : getPatchedClasses()) {
        ClassLoader classLoader = getClass().getClassLoader();
        Object object1 = classLoader.loadClass(str + "$override").newInstance();
        Field field = classLoader.loadClass(str).getDeclaredField("$change");
        field.setAccessible(true);
        Object object2 = field.get(null);
        if (object2 != null) {
          object2 = object2.getClass().getDeclaredField("$obsolete");
          if (object2 != null)
            object2.set(null, Boolean.valueOf(true)); 
        } 
        field.set(null, object1);
        if (Log.logging != null && Log.logging.isLoggable(Level.FINE))
          Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { str })); 
      } 
    } catch (Exception exception) {
      if (Log.logging != null)
        Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), exception); 
      return false;
    } 
    return true;
  }
}
//AppPatchesLoaderImpl繼承AbstractPatchesLoaderImpl,真正的實現類炭庙,這里我們只要求change的MainActivity這個類里面的方法
public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
    public static final long BUILD_ID = 1597285889481L;

    public AppPatchesLoaderImpl() {
    }

    public String[] getPatchedClasses() {
        return new String[]{"com.example.jackie.instantrundemo.MainActivity"};
    }
}

其中DexClassLoader的構造方法定義

//dexPath:被解壓的apk路徑,不能為空煌寇。
//optimizedDirectory:解壓后的.dex文件的存儲路徑焕蹄,不能為空。這個路徑強烈建議使用應用程序的私有路徑阀溶,不要放到sdcard上腻脏,否則代碼容易被注入攻擊。
//libraryPath:os庫的存放路徑银锻,可以為空永品,若有os庫,必須填寫徒仓。
//parent:父類加載器腐碱,一般為context.getClassLoader(),使用當前上下文的類加載器誊垢。
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

我們的MainActivity會被修改成這樣

public class MainActivity extends AppCompatActivity {
    public static final long serialVersionUID = 2158910920756968252L;

    //重寫空構造方法掉弛,方便于替換該方法的實現
    public MainActivity() {
        IncrementalChange var1 = $change;
        if(var1 != null) {
            Object[] var10001 = (Object[])var1.access$dispatch("init$args.([Lcom/example/jackie/instantrundemo/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/jackie/instantrundemo/MainActivity;[Ljava/lang/Object;)V", var2);
        } else {
            super();
        }
    }

    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.setContentView(2130968603);
            if(this.test(30) > 20333005) {
                Log.d("jackie", "==4444099994==sf=dd=ddecf==999=abc==");
            } else {
                Log.d("jackie", "==999999999999=");
            }

            byte b = 0;
            Toast.makeText(this, "hellodd4fdddd", 1).show();
            Log.d("jackie", "===d=666==dd=dddd==abc==" + b);
        }
    }

    public int test(int a) {
        IncrementalChange var2 = $change;
        if(var2 != null) {
            return ((Number)var2.access$dispatch("test.(I)I", new Object[]{this, new Integer(a)})).intValue();
        } else {
            byte age = 100;
            int b = 300189 + age;

            for(int i = 0; i < b + 9; ++i) {
                a += b;
            }

            return 20 + a;
        }
    }
        //增加類的構造方法,方便與修改類的任何構造方法
    MainActivity(Object[] var1, InstantReloadException var2) {
        String var3 = (String)var1[1];
        switch(var3.hashCode()) {
        case -2089128195:
            super();
            return;
        case 173992496:
            this();
            return;
        default:
            throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var3, Integer.valueOf(var3.hashCode()), "com/example/jackie/instantrundemo/MainActivity"}));
        }
    }
}

在MainActivity中做個小修改喂走,點擊小閃電執(zhí)行Instant Run殃饿,可以看到build下面文件夾4000中會找一個MainActivity$override.class和AppPatchesLoaderImpl.class

要替換的MainActivity$override實現了IncrementalChange,從這里面進行方法的替換芋肠,所有的方法都會被替換乎芳,因為change值不為空

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/support/v7/app/AppCompatActivity.()V"};
        return var2;
    }

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

    public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
        Object[] var2 = new Object[]{savedInstanceState};
        MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
        $this.setContentView(2130968603);
        if($this.test(30) > 20333005) {
            Log.d("jackie", "==44440999940==sf=dd=ddecf==999=abc==");
        } else {
            Log.d("jackie", "==999999999999=");
        }

        byte b = 0;
        //因為修改了全局變量的值,所以進行處理
        AndroidInstantRuntime.setPrivateField($this, new Integer(((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue() + 100), MainActivity.class, "cmd");
        Toast.makeText($this, "hellodd4fdddd", 1).show();
        Log.d("jackie", "===d=666==dd=dddd==abc==" + b);
    }

    public static int test(MainActivity $this, int a) {
        int ageabc = 100 + ((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue();
        int b = 300189 + ageabc;

        for(int i = 0; i < b + 9; ++i) {
            a += b;
        }

        return 20 + a;
    }

    //替換相對應的方法,最后兩個方法我估計是前面MainActivity類里面的copy
    public Object access$dispatch(String var1, Object... var2) {
        switch(var1.hashCode()) {
        case -1227667971:
            return new Integer(test((MainActivity)var2[0], ((Number)var2[1]).intValue()));
        case -641568046:
            onCreate((MainActivity)var2[0], (Bundle)var2[1]);
            return null;
        case 435530788:
            return init$args((MainActivity[])var2[0], (Object[])var2[1]);
        case 1043612718:
            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", new Object[]{var1, Integer.valueOf(var1.hashCode()), "com/example/jackie/instantrundemo/MainActivity"}));
        }
    }
}

2.處理資源補丁(溫插拔)

下面來看看是如何處理資源補丁的帖池,但是設計到資源的處理需要重啟當前界面奈惑,我們先來看看重啟App這種狀況下的邏輯

//Server類的handle()
case 5:
    if (authenticate(param1DataInputStream)) {
      Activity activity1 = Restarter.getForegroundActivity(Server.this.context);
      if (activity1 != null) {
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Restarting activity per user request"); 
        Restarter.restartActivityOnUiThread(activity1);
      } 
      continue;
    } 
    return;
case 1:
    if (authenticate(param1DataInputStream)) {
      List<ApplicationPatch> list = ApplicationPatch.read(param1DataInputStream);
      if (list != null) {
        bool = Server.hasResources(list);
        i = param1DataInputStream.readInt();
        //處理acitivity或者把資源寫到文件中
        i = Server.this.handlePatches(list, bool, i);
        boolean bool1 = param1DataInputStream.readBoolean();
        param1DataOutputStream.writeBoolean(true);
        //重啟Activity
        Server.this.restart(i, bool, bool1);
      } 
      continue;
} 

我們先來看看handlePatches里面的handleResourcePatch是如何處理資源的

//Server.class
//處理資源補丁
private static int handleResourcePatch(int paramInt, ApplicationPatch paramApplicationPatch, String paramString) {
    if (Log.isLoggable("InstantRun", 2))
      Log.v("InstantRun", "Received resource changes (" + paramString + ")"); 
    FileManager.writeAaptResources(paramString, paramApplicationPatch.getBytes());
    return Math.max(paramInt, 2);
  }
//FileManager.class
 //writeAaptResources
 public static void writeAaptResources(String paramString, byte[] paramArrayOfbyte) {
    File file1 = getResourceFile(getWriteFolder(false)); //獲取資源文件
    File file2 = file1.getParentFile();
    if (!file2.isDirectory() && !file2.mkdirs()) {
      if (Log.isLoggable("InstantRun", 2))
        Log.v("InstantRun", "Cannot create local resource file directory " + file2); 
      return;
    } 
    //把內容寫到resources.ap_資源中
    if (paramString.equals("resources.ap_")) {
      writeRawBytes(file1, paramArrayOfbyte);
      return;
    } 
   //把raw資源寫入
    writeRawBytes(file1, paramArrayOfbyte);
  }
//getWriteFolder
public static File getWriteFolder(boolean paramBoolean) {
    String str;
    if (leftIsActive()) {
      str = "right";
    } else {
      str = "left";
    } 
    File file = new File(getDataFolder(), str);
    if (paramBoolean && file.exists()) {
      delete(file);
      if (!file.mkdirs())
        Log.e("InstantRun", "Failed to create folder " + file); 
    } 
    return file;
  }
//getResourceFile
private static File getResourceFile(File paramFile) { return new File(paramFile, "resources.ap_"); }

//writeRawBytes
public static boolean writeRawBytes(File paramFile, byte[] paramArrayOfbyte) {
    try {
      BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(paramFile));
      try {
        bufferedOutputStream.write(paramArrayOfbyte);
        bufferedOutputStream.flush();
        return true;
      } finally {
        bufferedOutputStream.close();
      } 
    } catch (IOException iOException) {
      Log.wtf("InstantRun", "Failed to write file, clean project and rebuild " + paramFile, iOException);
      throw new RuntimeException(String.format("InstantRun could not write file %1$s, clean project and rebuild ", new Object[] { paramFile }));
    } 
  }
public static String writeTempDexFile(byte[] paramArrayOfbyte) {
    File file = getTempDexFile();
    if (file != null) {
      writeRawBytes(file, paramArrayOfbyte);
      return file.getPath();
    } 
    Log.e("InstantRun", "No file to write temp dex content to");
    return null;
  }

下面來看看Server.this.restart(i, bool, bool1)是如何處理的,不必拘泥于細節(jié)是如何啟動的睡汹,在里面找到一行關鍵代碼

MonkeyPatcher.monkeyPatchExistingResources(this.context, str, list);

具體實現如下

public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection<Activity> activities) {
    Collection<WeakReference<Resources>> references;
    if (externalResourceFile != null) {
        //通過反射獲取AssetManager
        AssetManager newAssetManager = AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
        mAddAssetPath.setAccessible(true);
       //將當前的資源文件路徑添加到AssetManager中
        if (((Integer) mAddAssetPath.invoke(newAssetManager, new Object[]{externalResourceFile})).intValue() == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
        mEnsureStringBlocks.setAccessible(true);
        //進行資源初始化StringBlock對象
        mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
        if (activities != null) {
            for (Activity activity : activities) {
                Resources resources = activity.getResources();
                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable e) {
                    throw new IllegalStateException(e);
                }
                Resources.Theme theme = activity.getTheme();
                try {
                    Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                    ma.setAccessible(true);
                    ma.set(theme, newAssetManager);
                } catch (NoSuchFieldException e2) {
                    Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                    themeField.setAccessible(true);
                    Object impl = themeField.get(theme);
                    Field ma2 = impl.getClass().getDeclaredField("mAssets");
                    ma2.setAccessible(true);
                    ma2.set(impl, newAssetManager);
                } catch (Throwable e3) {
                    Log.e(Logging.LOG_TAG, "Failed to update existing theme for activity " + activity, e3);
                }
                Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                mt.setAccessible(true);
                mt.set(activity, (Object) null);
                Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
                mtm.setAccessible(true);
                mtm.invoke(activity, new Object[0]);
                if (Build.VERSION.SDK_INT < 24) {
                    Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
                    mCreateTheme.setAccessible(true);
                    Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
                    Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                    mTheme.setAccessible(true);
                    mTheme.set(theme, internalTheme);
                }
                pruneResourceCaches(resources);
            }
        }
              //獲取當前JVM中的ResourcesManager的final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources
        if (Build.VERSION.SDK_INT >= 19) {
            Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
            mGetInstance.setAccessible(true);
            Object resourcesManager = mGetInstance.invoke((Object) null, new Object[0]);
            try {
                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                references = ((ArrayMap) fMActiveResources.get(resourcesManager)).values();
            } catch (NoSuchFieldException e4) {
                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                references = (Collection) mResourceReferences.get(resourcesManager);
            }
        } else {
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Field fMActiveResources2 = activityThread.getDeclaredField("mActiveResources");
            fMActiveResources2.setAccessible(true);
            references = ((HashMap) fMActiveResources2.get(getActivityThread(context, activityThread))).values();
        }
        //循環(huán)便利當前Resources肴甸,將其成員變量mAssets指向自定義的newAssetManager
        for (WeakReference<Resources> wr : references) {
            Resources resources2 = (Resources) wr.get();
            if (resources2 != null) {
                try {
                    Field mAssets2 = Resources.class.getDeclaredField("mAssets");
                    mAssets2.setAccessible(true);
                    mAssets2.set(resources2, newAssetManager);
                } catch (Throwable th) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources2);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                //更新資源
                resources2.updateConfiguration(resources2.getConfiguration(), resources2.getDisplayMetrics());
            }
        }
    }
}

在研究過程中,本來想基于退出gradle4.1然后在gradle2.2.3重新再搞一下囚巴,后面想想不把4.1研究透再去搞2.2.3總是心有不甘原在,網上也基本找不到gradle 4.1的研究的友扰,其實在2.3.0之后就沒有instant-run.zip包了,但是好像所有人都沒有提到這點庶柿,難道他們都是基于2.2.3或者更早的村怪?

找不到的項目代碼去哪里了

下面來看看我們的MainActivity,MyApplication等文件在去哪里了,找到之前的Server(SocketServerThread)浮庐,明白我們是從AS端接受這些文件的甚负,接受這些dex文件后,存儲在app的cache文件中(getWriteFolder)兔辅,并進行處理腊敲,

private class SocketServerThread extends Thread {
    private SocketServerThread() {}

    public void run() {
      while (true) {
        try {
          LocalServerSocket localServerSocket = Server.this.serverSocket;
          if (localServerSocket == null)
            return; 
          //接受AS發(fā)過來的文件
          LocalSocket localSocket = localServerSocket.accept();
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Received connection from IDE: spawning connection thread"); 
          (new Server.SocketServerReplyThread(localSocket)).run();
          if (wrongTokenCount > 50) {
            if (Log.isLoggable("InstantRun", 2))
              Log.v("InstantRun", "Stopping server: too many wrong token connections"); 
            Server.this.serverSocket.close();
            return;
          } 
        } catch (Throwable throwable) {
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Fatal error accepting connection on local socket", throwable); 
        } 
      } 
    }
  }

下面可以使用我們一開始下載的源碼,在instant-run下面的intant-run-client维苔,InstantClient類中碰辅,將文件發(fā)送到設備中

private void transferBuildIdToDevice(@NonNull IDevice device, @NonNull String buildId) {
    try {
        String remoteIdFile = getDeviceIdFolder(mPackageName);
        //noinspection SSBasedInspection This should work
        File local = File.createTempFile("build-id", "txt");
        local.deleteOnExit();
        Files.write(buildId, local, Charsets.UTF_8);
        device.pushFile(local.getPath(), remoteIdFile);
    } catch (IOException ioe) {
        mLogger.warning("Couldn't write build id file: %s", ioe);
    } catch (AdbCommandRejectedException | TimeoutException | SyncException e) {
        mLogger.warning("%s", Throwables.getStackTraceAsString(e));
    }
}

我們回看之前的handleHotSwapPatch方法中讀取文件的方式,可以看到在cache文件中

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
    ···
    try {
        String dexFile = FileManager.writeTempDexFile(patch.getBytes());
        ···
        String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                context.getCacheDir().getPath(), nativeLibraryPath,
                getClass().getClassLoader());

使用InstantRun更新的文件去哪里了

下面來看看我們的MainActivity$override,MyApplication$override,AppPatchesLoaderImpl等文件在去哪里了,我們進入應用內部的/data/data/com.example.jackie.instantrundemo/files/instant-run/dex-temp中會發(fā)現一個reload0x0000.dex文件介时,里面就有提供更新的內容没宾,instant-run里面中的right是用于存儲resource.ap_。

其他一些Gradle版本

Gradle2.2.3

在當前版本中沸柔,我們可以看到instant-run.zip包循衰,里面包含的項目的代碼和要替換的代碼。解壓后可以看到AndroidManifest.xml文件褐澎,從 AndroidManifest.xml 中我們看到了MyApplicationBootstrapApplication 替代会钝,那么我們可以想象當 ApplicationInstant-run 自己的時,那么它至少可以像加載插件一樣在應用啟動的時候(程序入口)加載替換自己的dex和資源文件工三,從而達到修改運行程序的目的迁酸。

@Override
protected void attachBaseContext(Context context) {
    // As of Marshmallow, we use APK splits and don't need to rely on
    // reflection to inject classes and resources for coldswap
    //noinspection PointlessBooleanExpression
    //是否使用了apk分裂安裝
    if (!AppInfo.usingApkSplits) {
        String apkFile = context.getApplicationInfo().sourceDir;
        long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;
        //判斷資源resource.ap_是否進行了修改,將其路徑保存在externalResourcePath
        createResources(apkModified);
        //創(chuàng)建classloade
        //delegateClassLoader->PathClassLoader->IncrementalClassLoader->BootClassLoader
        setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
    }
    //創(chuàng)建正真的Application
    createRealApplication();

    // This is called from ActivityThread#handleBindApplication() -> LoadedApk#makeApplication().
    // Application#mApplication is changed right after this call, so we cannot do the monkey
    // patching here. So just forward this method to the real Application instance.
    super.attachBaseContext(context);

    if (realApplication != null) {
        try {
            Method attachBaseContext =
                    ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
            attachBaseContext.setAccessible(true);
            //執(zhí)行自己的Application的attachBaseContext方法
            attachBaseContext.invoke(realApplication, context);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}

該方法的主要目的在于俭正,創(chuàng)建自定義的ClassLoader和真正的Application實例奸鬓。而 BootstrapApplication 只起到一個殼子的作用。

替換Application的時候我們可以看看MonkeyPatcher中是如何替換的

public class MonkeyPatcher {
    public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {
        Class<?> activityThread;
        Class<?> loadedApkClass;
        try {
            //獲取ActivityThread實例
            activityThread = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context, activityThread);
            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            //替換ActivityThread的mInitialApplication成員變量
            Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
            if (realApplication != null && initialApplication == bootstrap) {
                mInitialApplication.set(currentActivityThread, realApplication);
            }
            //替換ActivityThread的mAllApplications隊列中的BootstrapApplication為realApplication
            if (realApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List<Application> allApplications = (List) mAllApplications.get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                        allApplications.set(i, realApplication);
                    }
                }
            }
            loadedApkClass = Class.forName("android.app.LoadedApk");
        } catch (ClassNotFoundException e) {
            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
        } catch (Throwable e2) {
            IllegalStateException illegalStateException = new IllegalStateException(e2);
        }
        Field mApplication = loadedApkClass.getDeclaredField("mApplication");
        mApplication.setAccessible(true);
        Field mResDir = loadedApkClass.getDeclaredField("mResDir");
        mResDir.setAccessible(true);
        Field mLoadedApk = null;
        try {
            mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
        } catch (NoSuchFieldException e3) {
        }
        //final ArrayMap<String, WeakReference<LoadedApk>> mPackages
        //final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages
        for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
            Field field = activityThread.getDeclaredField(fieldName);
            field.setAccessible(true);
            for (Entry<String, WeakReference<?>> entry : ((Map) field.get(currentActivityThread)).entrySet()) {
                Object loadedApk = ((WeakReference) entry.getValue()).get();
                if (loadedApk != null && mApplication.get(loadedApk) == bootstrap) {
                    //將ActivityThread的mPackages|mResourcePackages的LoadedApk
                    //LoadedApk的成員變量mApplication的BootstrapApplication替換為realApplication
                    if (realApplication != null) {
                        mApplication.set(loadedApk, realApplication);
                    }
                    //LoadedApk的成員變量mResDir替換為externalResourceFile
                    if (externalResourceFile != null) {
                        mResDir.set(loadedApk, externalResourceFile);
                    }
                    //將realApplication的mLoadedApk替換為BootstrapApplication的mLoadedApk
                    if (!(realApplication == null || mLoadedApk == null)) {
                        mLoadedApk.set(realApplication, loadedApk);
                    }
                }
            }
        }
    }

    public static Object getActivityThread(Context context, Class<?> activityThread) {
        if (activityThread == null) {
            try {
                activityThread = Class.forName("android.app.ActivityThread");
            } catch (Throwable th) {
                return null;
            }
        }
        //獲取ActivityThread的靜態(tài)變量sCurrentActivityThread
        Method m = activityThread.getMethod("currentActivityThread", new Class[0]);
        m.setAccessible(true);
        Object currentActivityThread = m.invoke(null, new Object[0]);
        if (currentActivityThread != null || context == null) {
            return currentActivityThread;
        }
        //獲取BootstrapApplication的mLoadedApk
        Field mLoadedApk = context.getClass().getField("mLoadedApk");
        mLoadedApk.setAccessible(true);
        Object apk = mLoadedApk.get(context);
        Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
        mActivityThreadField.setAccessible(true);
        //返回mLoadedApk的成員變量mActivityThread
        return mActivityThreadField.get(apk);
    }
    /***部分代碼省略***/
}

  • 獲取ActivityThread實例

    • 先獲取ActivityThread的靜態(tài)變量sCurrentActivityThread掸读;
    • 否則獲取Application對象的成員變mLoadedApk的成員對象mActivityThread串远;
  • 替換ActivityThread的mInitialApplication為realApplication

  • 替換ActivityThread的mAllApplications中的所有的BootstrapApplication為realApplication

  • 替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication。

  • 替換realApplication中的mLoadedApk為BootstrapApplication的MLoadedApk

AndroidMainifest文件中的Application如何被替換

首先我們切換到之前下載的git庫中的base/build-system目錄下面儿惫,然后切換到tag gradle_2.2.2分支下肌索,然后全局搜索該Application值

//ManifestMerger2.class
static final String BOOTSTRAP_APPLICATION
        = "com.android.tools.fd.runtime.BootstrapApplication";

被調用的地方

//如方法定義一樣嫩挤,替換為instantrun的APPLICATION
@NonNull
private static XmlDocument instantRunReplacement(XmlDocument document) {
    Optional<XmlElement> applicationOptional = document
            .getByTypeAndKey(ManifestModel.NodeTypes.APPLICATION, null /* keyValue */);
    if (applicationOptional.isPresent()) {
        XmlElement application = applicationOptional.get();
        Attr nameAttribute = application.getXml().getAttributeNodeNS(
                SdkConstants.ANDROID_URI, "name");

        if (nameAttribute != null) {
            String originalAppName = nameAttribute.getValue();
            if (BOOTSTRAP_APPLICATION.equals(originalAppName)) {
                return document;
            }
            application.getXml().setAttribute(SdkConstants.ATTR_NAME, originalAppName);
            application.getXml().setAttributeNS(
                    SdkConstants.ANDROID_URI,
                    nameAttribute.getName(),
                    BOOTSTRAP_APPLICATION);
        } else {
            application.getXml().setAttributeNS(
                    SdkConstants.ANDROID_URI,
                    SdkConstants.ANDROID_NS_NAME_PREFIX + SdkConstants.ATTR_NAME,
                    BOOTSTRAP_APPLICATION);
        }
    }
    return document.reparse();
}

調用鏈ManifestMerger2 -> MergeManifests ->TaskManager.createMergeLibManifestsTask -> LibraryTaskManager.createTasksForVariantData -> LibraryPlugin.createTaskManager肢执,LibraryPlugin是一個gradle Plugin插件肌毅,會被我們自動注冊處理這些東西。

小結

  • 修改源代碼筐喳,每個類增加 $change 字段催式;
  • 替換 Application 函喉;
  • 創(chuàng)建自己的類加載器,修改正常的類加載器的加載順序荣月;
  • 開啟 Socket 監(jiān)聽 AndroidStudio 推送的消息管呵;
  • 處理消息(熱、溫哺窄、冷)
    • 熱:給類的 $change 字段賦值捐下,改變運行邏輯;
    • 溫:替換加載新的資源萌业,重啟當前 Activity 生效坷襟;
    • 冷:寫入新的 dex 文件,重新加載新的 dex;

Gradle2.3.0

去掉了 BootstrapApplication 替換生年,直接啟動一個 InstantRunService 用來啟動 SocketAndroid Studio 進行信息傳遞婴程;

去掉了所謂的冷啟動(handleColdSwapPatch),需要冷啟動的時候直接進行碎片安裝重啟不就好了抱婉;

啟動InstantRunService是通過adb命令啟動

Instant Run的一些問題

調用Instant Run后档叔,殺死進程并重啟APP后,有時候這些新的的修改并沒有在當前的APP中蒸绩。

FileManager.class的一些方法

public class FileManager {
    public static final String CLASSES_DEX_SUFFIX = ".dex";
    private static final String FILE_NAME_ACTIVE = "active";
    private static final String FOLDER_NAME_LEFT = "left";
    private static final String FOLDER_NAME_RIGHT = "right";
    private static final String RELOAD_DEX_PREFIX = "reload";
    private static final String RESOURCE_FILE_NAME = "resources.ap_";
    private static final String RESOURCE_FOLDER_NAME = "resources";
    /***部分代碼省略***/
    public static String writeTempDexFile(byte[] bytes) {
        File file = getTempDexFile();
        if (file != null) {
            writeRawBytes(file, bytes);
            return file.getPath();
        }
        Log.e(BootstrapApplication.LOG_TAG, "No file to write temp dex content to");
        return null;
    }
    /***部分代碼省略***/
    public static File getTempDexFile() {
        //“data/data/applicationid/files/instant-run”
        File dataFolder = getDataFolder();
        //“data/data/applicationid/files/instant-run/dex-temp”
        File dexFolder = getTempDexFileFolder(dataFolder);
        if (dexFolder.exists()) {
            if (!sHavePurgedTempDexFolder) {
                //刪除之前的
                purgeTempDexFiles(dataFolder);
            }
        } else if (dexFolder.mkdirs()) {
            sHavePurgedTempDexFolder = true;
        } else {
            Log.e(BootstrapApplication.LOG_TAG, "Failed to create directory " + dexFolder);
            return null;
        }
        File[] files = dexFolder.listFiles();
        int max = -1;
        if (files != null) {
            for (File file : files) {
                String name = file.getName();
                //文件以“reload"開頭衙四,以".dex"結尾
                if (name.startsWith(RELOAD_DEX_PREFIX) && name.endsWith(CLASSES_DEX_SUFFIX)) {
                    try {
                        //獲取中間版本,取最高版本
                        int version = Integer.decode(name.substring(RELOAD_DEX_PREFIX.length(), name.length() - CLASSES_DEX_SUFFIX.length())).intValue();
                        if (version > max) {
                            max = version;
                        }
                    } catch (NumberFormatException e) {
                    }
                }
            }
        }
        //創(chuàng)建版本號+1的reloadxxx.dex文件患亿,比如reload0x0000.dex
        File file2 = new File(dexFolder, String.format("%s0x%04x%s", new Object[]{RELOAD_DEX_PREFIX, Integer.valueOf(max + 1), CLASSES_DEX_SUFFIX}));
        if (!Log.isLoggable(BootstrapApplication.LOG_TAG, 2)) {
            return file2;
        }
        Log.v(BootstrapApplication.LOG_TAG, "Writing new dex file: " + file2);
        return file2;
    }
}

InstantTransfrom

InstantRun是通過ASM插件來給每個方法前插入change传蹈,然后在運行instant-run的時候進行替換,地址在這步藕,我們可以參考他們的實現來進行我們自己的熱修復框架開發(fā)惦界。

build下面的文件

com.android.build.gradle.tasks.ir.FastDeployRuntimeExtractorTask這個類負責從gradle插件的jar包中把instant-run-server.jar提取出來放到build目錄下面

// we could just extract the instant-runtime jar and place it as a stream once we
// don't have to deal with AppInfo replacement.
@TaskAction
public void extract() throws IOException {
    URL fdrJar =
            FastDeployRuntimeExtractorTask.class.getResource(
                    "/instant-run/instant-run-server.jar");
    if (fdrJar == null) {
        throw new RuntimeException("Couldn't find Instant-Run runtime library");
    }
  URLConnection urlConnection = fdrJar.openConnection();
  ···
}

總結

從零開始分析InstantRun確實遇到不少問題:

  1. git代理和全局代理,以后再以后下載一些第三方源碼時會大大提高效率漱抓;

  2. 反編譯使用不同的工具交叉查看

  3. 下載了不同版本的AS表锻,ndk配置問題恕齐,graldle下載緩慢問題(3.3解決)

  4. 不同版本Gradle乞娄,InstantRun機制不一樣,原先使用instant-run.zip显歧,然后被移除仪或,直接弄到cache和dex-temp中,最終AS3.5廢棄而使用Apply Changes

  5. 理解的Instant Run士骤,感覺熱修復也沒那么難啊范删,但不同框架使用不用的原理需要繼續(xù)研究。

參考文章

https://blog.csdn.net/qq_33487412/article/details/78458000

https://juejin.im/post/6844903952287268877

https://developer.android.google.cn/studio/run/index.html?authuser=19#apply-changes

https://www.zhihu.com/question/309772986

https://blog.csdn.net/guolin_blog/article/details/51271369

https://github.com/stven0king/InstantRun-ApkParse(非常完整)

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末拷肌,一起剝皮案震驚了整個濱河市到旦,隨后出現的幾起案子旨巷,更是在濱河造成了極大的恐慌,老刑警劉巖添忘,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件采呐,死亡現場離奇詭異,居然都是意外死亡搁骑,警方通過查閱死者的電腦和手機斧吐,發(fā)現死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仲器,“玉大人煤率,你說我怎么就攤上這事》剑” “怎么了蝶糯?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辆沦。 經常有香客問我裳涛,道長,這世上最難降的妖魔是什么众辨? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任端三,我火速辦了婚禮,結果婚禮上鹃彻,老公的妹妹穿的比我還像新娘郊闯。我一直安慰自己,他們只是感情好蛛株,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布团赁。 她就那樣靜靜地躺著,像睡著了一般谨履。 火紅的嫁衣襯著肌膚如雪欢摄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天笋粟,我揣著相機與錄音怀挠,去河邊找鬼。 笑死害捕,一個胖子當著我的面吹牛绿淋,可吹牛的內容都是我干的。 我是一名探鬼主播尝盼,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼吞滞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盾沫?” 一聲冷哼從身側響起裁赠,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤殿漠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后佩捞,有當地人在樹林里發(fā)現了一具尸體凸舵,經...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年失尖,在試婚紗的時候發(fā)現自己被綠了啊奄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡掀潮,死狀恐怖菇夸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情仪吧,我是刑警寧澤庄新,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站薯鼠,受9級特大地震影響择诈,放射性物質發(fā)生泄漏。R本人自食惡果不足惜出皇,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一羞芍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郊艘,春花似錦荷科、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至狞贱,卻和暖如春刻获,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞎嬉。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工蝎毡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人佑颇。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓顶掉,卻偏偏與公主長得像草娜,于是被迫代替她去往敵國和親挑胸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內容