Instant Run原理解析

背景

Android studio 2.0有一個(gè)新特性-Instanct Run,可以在不重啟App的情況下運(yùn)行修改后的代碼。具體使用方法可以參考官方文檔,接下來(lái)我們具體分析下Instant Run的實(shí)現(xiàn)原理。

原理

涉及到的工具

  • dex2jar
  • jd-gui

涉及到的Jar包

  • instant-run.jar
  • 反編譯后的apk

打開反編譯后的apk,我們可以很清晰的看到多了2個(gè)包,com.android.build.gradle.internal.incremental和com.android.tools,之后我們就會(huì)發(fā)現(xiàn)其實(shí)這2個(gè)包就是instance-run.jar,在build期間被打包到apk里面。

Paste_Image.png

這部分我們先不管,我們先看下編寫的代碼里面變化了什么。

Paste_Image.png

打出的Patch包

Paste_Image.png

FloatingActionButtonBasicFragment$override

Paste_Image.png

我們可以發(fā)現(xiàn)每一個(gè)函數(shù)里面都多了一個(gè)$change,當(dāng) $change不為null時(shí),執(zhí)行access$dispatch,否則執(zhí)行舊邏輯。我們可以猜測(cè)是com.android.tools.build:gradle:2.0.0-alpha1處理的。
接下來(lái)我們?cè)倏纯粗拔覀兞粝碌?個(gè)新增包,看看都做了什么。
BootstrapApplication:
onCreate

  public void onCreate()
  {
    MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, this.externalResourcePath);

    MonkeyPatcher.monkeyPatchExistingResources(this, this.externalResourcePath, null);

    super.onCreate();
    if (AppInfo.applicationId != null) {
      Server.create(AppInfo.applicationId, this);
    }

    if (this.realApplication != null)
      this.realApplication.onCreate();
  }

先Monkey Application和已存在的資源,然后創(chuàng)建Server,該Server主要處理讀取客戶端的Dex文件,如果用更新,則進(jìn)行加載和處理。

Server
SocketServerThread

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

    public void run()
    {
      try
      {
        while (true)
        {
          LocalServerSocket serverSocket = Server.this.mServerSocket;
          if (serverSocket == null) {
            break;
          }
          LocalSocket socket = serverSocket.accept();

          if (Log.isLoggable("fd", 4)) {
            Log.i("fd", "Received connection from IDE: spawning connection thread");
          }

          Server.SocketServerReplyThread socketServerReplyThread = new Server.SocketServerReplyThread(Server.this, socket);

          socketServerReplyThread.run();

          if (Server.mWrongTokenCount > 50) {
            if (Log.isLoggable("fd", 4)) {
              Log.i("fd", "Stopping server: too many wrong token connections");
            }
            Server.this.mServerSocket.close();
            break;
          }
        }
      } catch (IOException e) {
        if (Log.isLoggable("fd", 4))
          Log.i("fd", "Fatal error accepting connection on local socket", e);
      }
    }
  }

SocketServerReplyThread

private class SocketServerReplyThread extends Thread
  {
    private final LocalSocket mSocket;

    SocketServerReplyThread(LocalSocket socket)
    {
      this.mSocket = socket;
    }

    public void run()
    {
      try {
        DataInputStream input = new DataInputStream(this.mSocket.getInputStream());
        DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream());
        try {
          handle(input, output);
        } finally {
          try {
            input.close();
          } catch (IOException ignore) {
          }
          try {
            output.close();
          } catch (IOException ignore) {
          }
        }
      } catch (IOException e) {
        if (Log.isLoggable("fd", 4))
          Log.i("fd", "Fatal error receiving messages", e);
      }
    }

開啟Socket時(shí),讀取數(shù)據(jù)之后,進(jìn)行處理。

private void handle(DataInputStream input, DataOutputStream output) throws IOException
    {
      long magic = input.readLong();
      if (magic != 890269988L) {
        Log.w("fd", "Unrecognized header format " + Long.toHexString(magic));

        return;
      }
      int version = input.readInt();

      output.writeInt(4);

      if (version != 4) {
        Log.w("fd", "Mismatched protocol versions; app is using version 4 and tool is using version " + version);

        return;
      }
      int message;
      while (true) {
        message = input.readInt();
        switch (message) {
        case 7:
          if (Log.isLoggable("fd", 4)) {
            Log.i("fd", "Received EOF from the IDE");
          }
          return;
        case 2:
          boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null;
          output.writeBoolean(active);
          if (!Log.isLoggable("fd", 4)) continue;
          Log.i("fd", "Received Ping message from the IDE; returned active = " + active); break;
        case 3:
          String path = input.readUTF();
          long size = FileManager.getFileSize(path);
          output.writeLong(size);
          if (!Log.isLoggable("fd", 4)) continue;
          Log.i("fd", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size); break;
        case 4:
          long begin = System.currentTimeMillis();
          String path = input.readUTF();
          byte[] checksum = FileManager.getCheckSum(path);
          if (checksum != null) {
            output.writeInt(checksum.length);
            output.write(checksum);
            if (!Log.isLoggable("fd", 4)) continue;
            long end = System.currentTimeMillis();
            String hash = new BigInteger(1, checksum).toString(16);
            Log.i("fd", "Received checksum(" + path + ") from the " + "IDE: took " + (end - begin) + "ms to compute " + hash);

            continue;
          }
          output.writeInt(0);
          if (!Log.isLoggable("fd", 4)) continue;
          Log.i("fd", "Received checksum(" + path + ") from the " + "IDE: returning <null>"); break;
        case 5:
          if (!authenticate(input)) {
            return;
          }

          Activity activity = Restarter.getForegroundActivity(Server.this.mApplication);
          if (activity == null) continue;
          if (Log.isLoggable("fd", 4)) {
            Log.i("fd", "Restarting activity per user request");
          }
          Restarter.restartActivityOnUiThread(activity); break;
        case 1:
          if (!authenticate(input)) {
            return;
          }

          List changes = ApplicationPatch.read(input);
          if (changes == null)
          {
            continue;
          }
          boolean hasResources = Server.this.hasResources(changes);
          int updateMode = input.readInt();
          updateMode = Server.this.handlePatches(changes, hasResources, updateMode);

          boolean showToast = input.readBoolean();

          output.writeBoolean(true);

          Server.this.restart(updateMode, hasResources, showToast);
          break;
        case 6:
          String text = input.readUTF();
          Activity foreground = Restarter.getForegroundActivity(Server.this.mApplication);
          if (foreground != null) {
            Restarter.showToast(foreground, text); continue;
          }if (!Log.isLoggable("fd", 4)) continue;
          Log.i("fd", "Couldn't show toast (no activity) : " + text);
        }

      }

      if (Log.isLoggable("fd", 6))
        Log.e("fd", "Unexpected message type: " + message);
    }

我們可以看到,先進(jìn)行一些簡(jiǎn)單的校驗(yàn),判斷讀取的數(shù)據(jù)是否正確。然后依次讀取文件數(shù)據(jù)。

  • 如果讀到7,則表示已經(jīng)讀到文件的末尾,退出讀取操作
  • 如果讀到2,則表示獲取當(dāng)前Activity活躍狀態(tài),并且進(jìn)行記錄
  • 如果讀到3,讀取UTF-8字符串路徑,讀取該路徑下文件長(zhǎng)度,并且進(jìn)行記錄
  • 如果讀到4,讀取UTF-8字符串路徑,獲取該路徑下文件MD5值,如果沒(méi)有,則記錄0,否則記錄MD5值和長(zhǎng)度。
  • 如果讀到5,先校驗(yàn)輸入的值是否正確(根據(jù)token來(lái)判斷),如果正確,則在UI線程重啟Activity
  • 如果讀到1,先校驗(yàn)輸入的值是否正確(根據(jù)token來(lái)判斷),如果正確,獲取代碼變化的List,處理代碼的改變(handlePatches,這個(gè)之后具體分析),然后重啟
  • 如果讀到6,讀取UTF-8字符串,showToast

handlePatches

private int handlePatches(@NonNull List<ApplicationPatch> changes, boolean hasResources, int updateMode)
  {
    if (hasResources) {
      FileManager.startUpdate();
    }

    for (ApplicationPatch change : changes) {
      String path = change.getPath();
      if (path.endsWith(".dex"))
        handleColdSwapPatch(change);
      else if (path.endsWith(".dex.3"))
        updateMode = handleHotSwapPatch(updateMode, change);
      else {
        updateMode = handleResourcePatch(updateMode, change, path);
      }
    }

    if (hasResources) {
      FileManager.finishUpdate(true);
    }

    return updateMode;
  }

如果文件路徑后綴是".dex",則handleColdSwapPatch,如果后綴是".dex.3",則handleHotSwapPatch,否則handleResourcePatch。接下來(lái)我們具體來(lái)看。
handleColdSwapPatch

  private void handleColdSwapPatch(@NonNull ApplicationPatch patch) {
    if (Log.isLoggable("fd", 4)) {
      Log.i("fd", "Received restart code patch");
    }
    FileManager.writeDexFile(patch.getBytes(), true);
  }

寫入Dex文件

writeDexFile

  public static File writeDexFile(@NonNull byte[] bytes, boolean writeIndex) {
    //創(chuàng)建下一個(gè)Dex文件,
    File file = getNextDexFile();
    if (file != null) {
      writeRawBytes(file, bytes);
      if (writeIndex) {
        File indexFile = getIndexFile(file);
        try {
          BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(indexFile), getUtf8Charset()));

          DexFile dexFile = new DexFile(file);
          Enumeration entries = dexFile.entries();
          while (entries.hasMoreElements()) {
            String nextPath = (String)entries.nextElement();

            if (nextPath.indexOf(36) != -1)
            {
              continue;
            }
            writer.write(nextPath);
            writer.write(10);
          }
          writer.close();

          if (Log.isLoggable("fd", 4))
            Log.i("fd", "Wrote restart patch index " + indexFile);
        }
        catch (IOException ioe) {
          Log.e("fd", "Failed to write dex index file " + indexFile);
        }
      }
    }

    return file;
  }

handleHotSwapPatch

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch)
  {
    if (Log.isLoggable("fd", 4))
      Log.i("fd", "Received incremental code patch");
    try
    {
      //寫入Dex文件
      String dexFile = FileManager.writeTempDexFile(patch.getBytes());
      if (dexFile == null) {
        Log.e("fd", "No file to write the code to");
        return updateMode;
      }if (Log.isLoggable("fd", 4)) {
        Log.i("fd", "Reading live code from " + dexFile);
      }
      String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
      DexClassLoader dexClassLoader = new DexClassLoader(dexFile, this.mApplication.getCacheDir().getPath(), nativeLibraryPath, getClass().getClassLoader());

      //加載AppPatchesLoaderImpl類,初始化,執(zhí)行l(wèi)oad方法
      Class aClass = Class.forName("com.android.build.gradle.internal.incremental.AppPatchesLoaderImpl", true, dexClassLoader);
      try {
        if (Log.isLoggable("fd", 4)) {
          Log.i("fd", "Got the patcher class " + aClass);
        }

        PatchesLoader loader = (PatchesLoader)aClass.newInstance();
        if (Log.isLoggable("fd", 4)) {
          Log.i("fd", "Got the patcher instance " + loader);
        }
        String[] getPatchedClasses = (String[])(String[])aClass.getDeclaredMethod("getPatchedClasses", new Class[0]).invoke(loader, new Object[0]);
        if (Log.isLoggable("fd", 4)) {
          Log.i("fd", "Got the list of classes ");
          for (String getPatchedClass : getPatchedClasses) {
            Log.i("fd", "class " + getPatchedClass);
          }
        }
        if (!loader.load())
          updateMode = 3;
      }
      catch (Exception e) {
        Log.e("fd", "Couldn't apply code changes", e);
        e.printStackTrace();
        updateMode = 3;
      }
    } catch (Throwable e) {
      Log.e("fd", "Couldn't apply code changes", e);
      updateMode = 3;
    }
    return updateMode;
  }

AbstractPatchesLoaderImpl

public boolean load()
  {
    try
    {
      for (String className : getPatchedClasses()) {
        ClassLoader cl = getClass().getClassLoader();
        Class aClass = cl.loadClass(className + "$override");
        Object o = aClass.newInstance();
        Class originalClass = cl.loadClass(className);
        Field changeField = originalClass.getDeclaredField("$change");

        changeField.setAccessible(true);

        Object previous = changeField.get(null);
        if (previous != null) {
          Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
          if (isObsolete != null) {
            isObsolete.set(null, Boolean.valueOf(true));
          }
        }
        changeField.set(null, o);

        Log.i("fd", String.format("patched %s", new Object[] { className }));
      }
    } catch (Exception e) {
      Log.e("fd", String.format("Exception while patching %s", new Object[] { "foo.bar" }), e);
      return false;
    }
    return true;
  }

加載class名稱+override類,給$change賦值,這就是Instance Run的關(guān)鍵,還記得多出來(lái)的$change嗎?在運(yùn)行程序的時(shí)候,就可以根據(jù)該變量,執(zhí)行被替換的函數(shù)。

handleResourcePatch

  private int handleResourcePatch(int updateMode, @NonNull ApplicationPatch patch, @NonNull String path)
  {
    if (Log.isLoggable("fd", 4)) {
      Log.i("fd", "Received resource changes (" + path + ")");
    }
    FileManager.writeAaptResources(path, patch.getBytes());

    updateMode = Math.max(updateMode, 2);
    return updateMode;
  }

寫入aapt Resource

public static void writeAaptResources(@NonNull String relativePath, @NonNull byte[] bytes)
  {
    File resourceFile = getResourceFile(getWriteFolder(false));
    File file = resourceFile;

    File folder = file.getParentFile();
    if (!folder.isDirectory()) {
      boolean created = folder.mkdirs();
      if (!created) {
        if (Log.isLoggable("fd", 4)) {
          Log.i("fd", "Cannot create local resource file directory " + folder);
        }
        return;
      }
    }

    if (relativePath.equals("resources.ap_"))
    {
      writeRawBytes(file, bytes);
    }
    else
      writeRawBytes(file, bytes);
  }

現(xiàn)在我們終于理清了Instant Run的原理,大家有不明白的可以留言。這是初稿,之后會(huì)優(yōu)化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容