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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容