InstantRun原理(2)——更新逻辑

上一篇博客我们介绍了InstantRun的初始化逻辑,接下来我们来看下在运行时阶段,InstantRun是如何加载修改的代码的。

上一篇博客的末尾我们介绍了InstantRun在初始化完成后,会启动一个server。不难猜测,这个server就是在监听是否有代码更新。当用户更改代码后,AndroidStudio会将相关更新发送给server,server获取到更新后执行修复逻辑。

1 SocketServerReplyThread

server的主要实现由其内部类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) { 
                } 
            } 
            return; 
        } catch (IOException e) { 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Fatal error receiving messages", e); 
            } 
        } 
    } 
 
    private void handle(DataInputStream input, DataOutputStream output) throws IOException { 
        long magic = input.readLong(); 
        if (magic != 890269988L) { 
            Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic)); 
            return; 
        } 
        int version = input.readInt(); 
        output.writeInt(4); 
        if (version != 4) { 
            Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version); 
        } else { 
            int message; 
            for (; ; ) { 
                message = input.readInt(); 
                switch (message) { 
                    case 7: 
                        if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", "Received EOF from the IDE"); 
                        } 
                        return; 
                    case 2: 
                        boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null; 
                        output.writeBoolean(active); 
                        if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", "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("InstantRun", 2)) { 
                            Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size); 
                        } 
                        break; 
                    case 4: 
                        long begin = System.currentTimeMillis(); 
                        path = input.readUTF(); 
                        byte[] checksum = FileManager.getCheckSum(path); 
                        if (checksum != null) { 
                            output.writeInt(checksum.length); 
                            output.write(checksum); 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                long end = System.currentTimeMillis(); 
                                String hash = new BigInteger(1, checksum) 
                                        .toString(16); 
                                Log.v("InstantRun", "Received checksum(" + path 
                                        + ") from the " + "IDE: took " 
                                        + (end - begin) + "ms to compute " 
                                        + hash); 
                            } 
                        } else { 
                            output.writeInt(0); 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                Log.v("InstantRun", "Received checksum(" + path 
                                        + ") from the " 
                                        + "IDE: returning "); 
                            } 
                        } 
                        break; 
                    case 5: 
                        if (!authenticate(input)) { 
                            return; 
                        } 
                        Activity activity = Restarter 
                                .getForegroundActivity(Server.this.mApplication); 
                        if (activity != null) { 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                Log.v("InstantRun", 
                                        "Restarting activity per user request"); 
                            } 
                            Restarter.restartActivityOnUiThread(activity); 
                        } 
                        break; 
                    case 1: 
                        if (!authenticate(input)) { 
                            return; 
                        } 
                        List changes = ApplicationPatch 
                                .read(input); 
                        if (changes != null) { 
                            boolean hasResources = Server.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); 
                        } else if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", 
                                    "Couldn't show toast (no activity) : " 
                                            + text); 
                        } 
                        break; 
                } 
            } 
        } 
    } 
} 

socket开启后,开始读取数据,先进行一些简单的校验,判断读取的数据是否正确。然后依次读取文件数据。

  • 如果读到7,则表示已经读到文件的末尾,退出读取操作
  • 如果读到2,则表示获取当前Activity活跃状态,并且进行记录
  • 如果读到3,读取UTF-8字符串路径,读取该路径下文件长度,并且进行记录
  • 如果读到4,读取UTF-8字符串路径,获取该路径下文件MD5值,如果没有,则记录0,否则记录MD5值和长度。
  • 如果读到5,先校验输入的值是否正确(根据token来判断),如果正确,则在UI线程重启Activity
  • 如果读到1,先校验输入的值是否正确(根据token来判断),如果正确,获取代码变化的List,处理代码的改变(handlePatches,这个之后具体分析),然后重启
  • 如果读到6,读取UTF-8字符串,showToast

当读到1时,获取代码变化的ApplicationPatch列表,然后调用handlePatches来处理代码的变化。

handlePatches:

private int handlePatches(List changes, boolean hasResources, int updateMode) { 
    if (hasResources) { 
        FileManager.startUpdate(); 
    } 
    for (ApplicationPatch change : changes) { 
        String path = change.getPath(); 
        if (path.endsWith(".dex")) { 
            handleColdSwapPatch(change); 
            boolean canHotSwap = false; 
            for (ApplicationPatch c : changes) { 
                if (c.getPath().equals("classes.dex.3")) { 
                    canHotSwap = true; 
                    break; 
                } 
            } 
            if (!canHotSwap) { 
                updateMode = 3; 
            } 
        } else if (path.equals("classes.dex.3")) { 
            updateMode = handleHotSwapPatch(updateMode, change); 
        } else if (isResourcePath(path)) { 
            updateMode = handleResourcePatch(updateMode, change, path); 
        } 
    } 
    if (hasResources) { 
        FileManager.finishUpdate(true); 
    } 
    return updateMode; 
}  

本方法主要通过判断Change的内容,来判断采用什么模式(热部署、温部署或冷部署)

  • 如果后缀为“.dex”,冷部署处理handleColdSwapPatch
  • 如果后缀为“classes.dex.3”,热部署处理handleHotSwapPatch
  • 其他情况,温部署,处理资源handleResourcePatch

2 热部署

我们知道如果仅仅修改某个方法的内部实现,InstantRun可以通过热部署的方式更新。还是以上一篇博客的例子,我们对代码进行一点修改,将Toast弹出的文字从'click'变为'click!!!':

    @Override
    public void onClick(View view) {
        Toast.makeText(this, "click!!!", Toast.LENGTH_SHORT).show();
    }

此时如果点击运行,可以看到应用在没有重启的情况更新了逻辑。当点击run按钮后,在build/intermediates/transforms/instantRun/debug/folders/4000/5目录下会出现我们输出即将发送给终端的patch:

InstantRun原理(2)——更新逻辑

可以看到patch总共分为两部分:

  • 修改后的代码,对应图中的com.alibaba.sdk.instantdemo.MainActivity$override
  • com.android.tools.fd.runtime.AppPatchesLoaderImpl.class用于记录哪些类被修改了,如本例中的MainActivity
public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
    public static final long BUILD_ID = 76160209775610L;

    public AppPatchesLoaderImpl() {
    }

    public String[] getPatchedClasses() {
        return new String[]{"com.alibaba.sdk.instandemo.MainActivity"};
    }
}

2.1 修改后的代码

修改后的代码会重新生成一个新的类名:旧类名+$override。如本例中的MainActivity$override,接下来看下MainActivity$override的源码:

public class MainActivity$override implements IncrementalChange {
    public MainActivity$override() {
    }

    public static Object init$args(MainActivity[] var0, Object[] var1) {
        Object[] var2 = new Object[]{new Object[]{var0, new Object[0]}, "android/support/v7/app/AppCompatActivity.()V"};
        return var2;
    }

    public static void init$body(MainActivity $this, Object[] var1) {
    }

    public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
        Object[] var2 = new Object[]{savedInstanceState};
        MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
        $this.setContentView(2130968603);
        AndroidInstantRuntime.setPrivateField($this, (Button)$this.findViewById(2131427416), MainActivity.class, "btn");
        ((Button)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "btn")).setOnClickListener($this);
    }

    public static void onClick(MainActivity $this, View view) {
        Toast.makeText($this, "click!!!", 0).show();
    }

    public Object access$dispatch(String var1, Object... var2) {
        switch(var1.hashCode()) {
        case -1912803358:
            onClick((MainActivity)var2[0], (View)var2[1]);
            return null;
        case -641568046:
            onCreate((MainActivity)var2[0], (Bundle)var2[1]);
            return null;
        case 1345615064:
            init$body((MainActivity)var2[0], (Object[])var2[1]);
            return null;
        case 1495908858:
            return init$args((MainActivity[])var2[0], (Object[])var2[1]);
        default:
            throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var1, Integer.valueOf(var1.hashCode()), "com/alibaba/sdk/instandemo/MainActivity"}));
        }
    }
}

我们看到,MainActivity$override实现了IncrementalChange并覆写了access$dispatch方法。

该patch会通过server被写到应用的私有目录下,然后通过handleHotSwapPatch进行加载。

2.2 hot swap:handleHotSwapPatch

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) { 
    if (Log.isLoggable("InstantRun", 2)) { 
        Log.v("InstantRun", "Received incremental code patch"); 
    } 
    try { 
        String dexFile = FileManager.writeTempDexFile(patch.getBytes()); 
        if (dexFile == null) { 
            Log.e("InstantRun", "No file to write the code to"); 
            return updateMode; 
        } 
        if (Log.isLoggable("InstantRun", 2)) { 
            Log.v("InstantRun", "Reading live code from " + dexFile); 
        } 
        String nativeLibraryPath = FileManager.getNativeLibraryFolder() 
                .getPath(); 
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile, 
                this.mApplication.getCacheDir().getPath(), 
                nativeLibraryPath, getClass().getClassLoader()); 
        Class aClass = Class.forName( 
                "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, 
                dexClassLoader); 
        try { 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the patcher class " + aClass); 
            } 
            PatchesLoader loader = (PatchesLoader) aClass.newInstance(); 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the patcher instance " + loader); 
            } 
            String[] getPatchedClasses = (String[]) aClass 
                    .getDeclaredMethod("getPatchedClasses", new Class[0]) 
                    .invoke(loader, new Object[0]); 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the list of classes "); 
                for (String getPatchedClass : getPatchedClasses) { 
                    Log.v("InstantRun", "class " + getPatchedClass); 
                } 
            } 
            if (!loader.load()) { 
                updateMode = 3; 
            } 
        } catch (Exception e) { 
            Log.e("InstantRun", "Couldn't apply code changes", e); 
            e.printStackTrace(); 
            updateMode = 3; 
        } 
    } catch (Throwable e) { 
        Log.e("InstantRun", "Couldn't apply code changes", e); 
        updateMode = 3; 
    } 
    return updateMode; 
}

该方法将patch的dex文件写入到临时目录,然后使用DexClassLoader去加载dex。然后反射调用AppPatchesLoaderImpl类的load方法。

AppPatchesLoaderImpl继承自抽象类AbstractPatchesLoaderImpl,并实现了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象类代码如下:

public abstract class AbstractPatchesLoaderImpl implements PatchesLoader { 
      public abstract String[] getPatchedClasses(); 
      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); 
                       if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) { 
                            Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className })); 
                       } 
                  } 
            } catch (Exception e) { 
                  if (Log.logging != null) { 
                         Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), e); 
} 
                  return false; 
            } 
            return true; 
      } 
}  

现在我们再回过头去看下MainActivity的代码:

package com.alibaba.sdk.instandemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.android.tools.fd.runtime.IncrementalChange;
import com.android.tools.fd.runtime.InstantReloadException;

public class MainActivity extends AppCompatActivity
  implements View.OnClickListener
{
  public static final long serialVersionUID = 0L;
  private Button btn;

  public MainActivity()
  {
  }

  MainActivity(Object[] paramArrayOfObject, InstantReloadException paramInstantReloadException)
  {
    this();
  }

  public void onClick(View paramView)
  {
    IncrementalChange localIncrementalChange = $change;
    if (localIncrementalChange != null)
    {
      localIncrementalChange.access$dispatch("onClick.(Landroid/view/View;)V", new Object[] { this, paramView });
      return;
    }
    Toast.makeText(this, "click", 0).show();
  }

  public void onCreate(Bundle paramBundle)
  {
    IncrementalChange localIncrementalChange = $change;
    if (localIncrementalChange != null)
    {
      localIncrementalChange.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[] { this, paramBundle });
      return;
    }
    super.onCreate(paramBundle);
    setContentView(2130968603);
    this.btn = ((Button)findViewById(2131427416));
    this.btn.setOnClickListener(this);
  }
}

结合两段代码,不难看出,loadClass方法的原理其实就是通过反射的方法将原有class中的$change设置为修复类,然后通过access$dispatch执行更新后的逻辑。

这里有一个问题。如果我多次修改MainActivityhandleHotSwapPatch就会加载多次MainActivity$override,难道不会冲突吗?一个类不是只能加载一次吗?其实这个不用担心,因为handleHotSwapPatch每次都重新创建了一个DexClassLoader,不同的ClassLoader即使加载同一个class也会被认为是不同class,所以不用担心。

2.3 warm swap:handleResourcePatch

private static int handleResourcePatch(int updateMode, ApplicationPatch patch, String path){
    if (Log.isLoggable("InstantRun", 2)) {
        Log.v("InstantRun", "Received resource changes (" + path + ")");
    }
    FileManager.writeAaptResources(path, patch.getBytes());
    updateMode = Math.max(updateMode, 2);
    return updateMode;
}

调用了FileManager.writeAaptResources方法写入Aapt resource。

public static void writeAaptResources(String relativePath, 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("InstantRun", 2)) {
                Log.v("InstantRun", "Cannot create local resource file directory " + folder);
            }
            return;
        }
    }
    if (relativePath.equals("resources.ap_"))
    {
        writeRawBytes(file, bytes);
    }
    else
        writeRawBytes(file, bytes);
}

可以看到它去获取了对应的资源文件,就是我们在上面提到的/data/data/[applicationId]/files/instant-run/resources.ap_,InstantRun直接对它进行了字节码操作,把通过Socket传过来的修改过的资源传递了进去。对Android上的资源打包不了解的同学可以去看老罗的Android应用程序资源的编译和打包过程分析这篇文章。

2.4 cold swap:handleColdSwapPatch

private static void handleColdSwapPatch(ApplicationPatch patch) {
    if (patch.path.startsWith("slice-")) {
        File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
        if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Received dex shard " + file);
    }
    }
public static File writeDexShard(byte[] bytes, String name){
    File dexFolder = getDexFileFolder(getDataFolder(), true);
    if (dexFolder == null) {
        return null;
    }
    File file = new File(dexFolder, name);
    writeRawBytes(file, bytes);
    return file;
}

对于cold swap,其实就是把数据写进对应的dex中,所以在art的情况下需要重启app,而对于API20以下的只能重新构建和部署了。

3 总结

两篇博客大致介绍了InstantRun的原理,从宏观上讲,InstantRun通过创建宿主Application的方式来代理所有来的加载,为热更新提供了Runtime。从微观上来讲,三种情况的原理各有不同:

  • hot swap玩的方法替换,通过重新生成一个新类,并将原有类的方法映射到新类中的方法。思想上比较类似AndFix,不过AndFix的更新在native层完成,hot swap则是在java层通过插桩完成。不熟悉AndFix的朋友可以看下这篇博客:AndFix Bug热修复框架及源码解析
  • warm swap的原理是加载resources.ap_并写入到AssetManager的加载路径中
  • cold swap的原理其实就是把数据写进对应的dex中。
上一篇:「镁客早报」科大讯飞回应裁员传闻;谷歌Waymo自动驾驶汽车部门在密歇根州建造工厂


下一篇:python 十进制 和 IP 地址互转