×

Instant Run原理解析

96
不二先生的世界
2015.12.23 22:14* 字数 790

背景

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的原理,大家有不明白的可以留言。这是初稿,之后会优化。

技术文章
Web note ad 1