Instant Run原理解析

作者: 不二先生的世界 | 来源:发表于2015-12-23 22:14 被阅读2337次

    背景

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

    相关文章

      网友评论

      • farmerjohn:请问下instant run 是怎么把资源动态加载的呢?
        不二先生的世界:@farmerjohn 在handleResourcePatch有介绍,直接拷贝资源文件。

      本文标题:Instant Run原理解析

      本文链接:https://www.haomeiwen.com/subject/xedvhttx.html