Instant Run原理解析

背景

Android studio 2.0有一個新特性-Instanct Run,可以在不重啟App的情況下運行修改后的代碼。具體使用方法可以參考官方文檔,接下來我們具體分析下Instant Run的實現原理。

原理

涉及到的工具

  • dex2jar
  • jd-gui

涉及到的Jar包

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

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

Paste_Image.png

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

Paste_Image.png

打出的Patch包

Paste_Image.png

FloatingActionButtonBasicFragment$override

Paste_Image.png

我們可以發現每一個函數里面都多了一個$change,當 $change不為null時,執行access$dispatch,否則執行舊邏輯。我們可以猜測是com.android.tools.build:gradle:2.0.0-alpha1處理的。
接下來我們再看看之前我們留下的2個新增包,看看都做了什么。
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和已存在的資源,然后創建Server,該Server主要處理讀取客戶端的Dex文件,如果用更新,則進行加載和處理。

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時,讀取數據之后,進行處理。

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);
    }

我們可以看到,先進行一些簡單的校驗,判斷讀取的數據是否正確。然后依次讀取文件數據。

  • 如果讀到7,則表示已經讀到文件的末尾,退出讀取操作
  • 如果讀到2,則表示獲取當前Activity活躍狀態,并且進行記錄
  • 如果讀到3,讀取UTF-8字符串路徑,讀取該路徑下文件長度,并且進行記錄
  • 如果讀到4,讀取UTF-8字符串路徑,獲取該路徑下文件MD5值,如果沒有,則記錄0,否則記錄MD5值和長度。
  • 如果讀到5,先校驗輸入的值是否正確(根據token來判斷),如果正確,則在UI線程重啟Activity
  • 如果讀到1,先校驗輸入的值是否正確(根據token來判斷),如果正確,獲取代碼變化的List,處理代碼的改變(handlePatches,這個之后具體分析),然后重啟
  • 如果讀到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。接下來我們具體來看。
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) {
    //創建下一個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類,初始化,執行load方法
      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的關鍵,還記得多出來的$change嗎?在運行程序的時候,就可以根據該變量,執行被替換的函數。

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);
  }

現在我們終于理清了Instant Run的原理,大家有不明白的可以留言。這是初稿,之后會優化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容