从零开始分析lnstantRun源码

背景

Android Studio 2.0 中引入的 Instant Run 是 RunDebug 命令的行为,可以大幅缩短应用更新的时间。尽管首次构建可能需要花费较长的时间,Instant Run 在向应用推送后续更新时则无需构建新的 APK,因此,这样可以更快地看到更改,曾经是Android开发人员的开发的利器,现在已被废弃,用Apply changes替代。但我们仍然可以学习它的源码,提升自己的开发技术。

使用

instant-run加载更新有三种方式hot swap,cold swap,warm swap,当然在不同gradle版本中不一定都有这个三个功能。

Hot Swap(热插拔)

hot swap是所有swap方式中效率最高的,不需要重新安装和重启,但是hot swap不会对程序中的对象重新进行初始化,也就是说某些场景需要重启Activity才能看出具体的变更内容,Android Studio对于hot swap这种情况默认是重启Activity的,当然你也可以到设置中去改变这一默认行为,具体路径是 Settings -> Build, Execution, Deployment -> Instant Run -> Restart activity on code changes。Hot Swap适用的条件比较少,只有一种情况会被视为hop swap类型,就是修改一个现有方法中的代码。

Warm Swap(温插拔)

只有一种情况会被Android Studio视为warm swap类型,就是修改或删除一个现有的资源文件,要求必须重启Activity

Cold Swap(冷插拔)

Android Studio会自动记录我们项目的每次修改,然后将修改的这部分内容打成一个dex文件发送到手机上,尽管这种swap类型仍然不需要去安装一个全新的APK,但是为了加载这个新的dex文件,整个应用程序必须进行重启才行。另外,cold swap的工作原理是基于multidex机制来实现的,在不引入外部library的情况下,只有5.0及以上的设备才支持multidex,5.0以下只能重新安装。该模式在3.0时候被废弃。

cold swap的使用场景非常多,如下

  • 添加、删除或修改一个注解,字段,方法
  • 添加一个类
  • 修改一个类的继承结构
  • 修改一个类的接口实现
  • 修改一个类的static修饰符
  • 涉及资源文件id的改动
使用时的注意点:
  1. 如果应用的minSdkVersion小于21,可能多数的Instant Run功能会挂掉,这里提供一个解决方法,通过product flavor建立一个minSdkVersion大于21的新分支,用来debug。

  2. Instant Run目前只能在主进程里运行,如果应用是多进程的,类似微信,把webView抽出来单独一个进程,那热、温拔插会被降级为冷拔插。后面的版本好像就只能在主进程中了,冷插拔都没了

  3. 在Windows下,Windows Defender Real-Time Protection可能会导致Instant Run挂掉,可用通过添加白名单列表解决。

  4. 暂时不支持Jack compiler,Instrumentation Tests,或者同时部署到多台设备。

Instant Run的设计需要Android构建工具和Android Studio的配合,相关的源码在两个库中,这两个库都在AOSP的源码中,Google使用基于git开发的版本管理工具repo进行管理,全部开源的代码及其庞大,我们只需要下载相关的git仓库就行。

配置代理

配置代理,我的代理使用的是蓝灯,有HTTP和SOCKS端口,使用HTTP速度只有几百kb,而使用SOCKS真是快啊,这玩意这么快在下载一些源码库的时候非常有用

HTTP(S)代理服务器:127.0.0.1:54504
SOCKS代理服务器:127.0.0.1:54505
git config --global http.proxy http://127.0.0.1:1080(本机的端口),可能会遇到Time out,使用下面那个
git config --global http.proxy socks5://127.0.0.1:54505

配置代理的常用命令

//设置git代理
git config --global http.proxy socks5://127.0.0.1:54505
//取消git代理:
git config --global --unset http.proxy
git config --global --unset https.proxy
//查看全局的所有代理
env | grep -i proxy
//只在当前shell窗口配置,也可以在配置文件中配置,那就是永久配置了
export http_proxy="http://127.0.0.1:1087"
//取消本机http代理
unset http_proxy

查看 端口所在线程 lsof -i:8080(端口)
查看mac终端端口命令 netstat -AaLlnW (相当于linux的 netstat -lntp)

查看端口是否被占用: sudo lsof -i :8080
结束占用端口的所有进程: lsof -P | grep ':8080' | awk '{print $2}' | xargs kill -9
获取源码

Instant Run的设计需要Android构建工具和Android Studio的配合,这个库中有Android gradle插件的代码,instant-run框架的代码全部在其中的instant-run目录中

git clone https://android.googlesource.com/platform/tools/base

在3.5版本的Android Studio之后,Google使用了新的apply change架构代替了instant run,所以最新的代码中看不到,需要切换到studio-3.2.1这个tag,最新的apply change使用时有诸多限制

  • 您使用调试构建变体来构建应用的 APK。
  • 您将应用部署到搭载 Android 8.0(API 级别 26)或更高版本的目标设备或模拟器上。

需要重启应用(不是重启Activity)才能实现的代码更改

某些代码和资源更改必须在重启应用之后才能应用,其中包括以下更改:

  • 添加或删除方法或字段
  • 更改方法签名
  • 更改方法或类的修饰符
  • 更改类继承行为
  • 更改枚举中的值
  • 添加或移除资源
  • 更改应用清单
  • 更改原生库(SO 文件)

所以我感觉这玩意以后可以用来在修改代码逻辑的时候使用,使用的范围非常有限。

知乎上也有人提问Android Studio3.5提供的Apply Changes是什么原理?

这里引用weishu大佬的回答,“猜测是使用JVMTI实现的,JVMTI 的全称是 JVM Tool Interface。它是 Java 虚拟机(ART)实现的一部分,包含了虚拟机中线程 / 内存 / 类 / 方法 / 变量 / 事件 / 定时器处理等等 20 多类功能。比如:内存控制和对象获取、线程和锁、调试功能。
对这个「Apply Changes」来说,比较重要的应该是 ClassTransform 和 ClassRedefine;它允许虚拟机在运行时动态修改类(Redefine只在9.0上实现了)。比如说 Activity 这个 class,你可以通过此接口在字节码层面往里面直接添加方法/修改方法,然后虚拟机会为你重新加载这个类,之后这个被改过的类就是原来那个货真价值的 Activity 类。所以,这个技术跟 Instant Run/Robust 编译期字节码编织 / ClassLoader 替换 / AndFix 方法替换那种动态修改完全不是一个层面的东西,这是 运行时动态字节码编织

另一个需要下载的库中有Android Studio相关的源代码,其中可以看到AS是如何配合instant-run工作的,需要切换到studio-3.2.1这个tag

git clone https://android.googlesource.com/platform/tools/adt/idea

我们可以在build.gradle中添加一行代码,查看启动gradle的命令和全部参数

println getGradle().getStartParameter()

我在3.4.2的Android Studio中看到 projectProperties={android.optional.compilation=INSTANT_DEV,这里就表示开启instant-run支持了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=F:\GitAndroid\RxDemo, searchUpwards=true, projectProperties={android.optional.compilation=INSTANT_DEV, android.injected.build.density=xxhdpi, android.injected.coldswap.mode=MULTIAPK, android.injected.build.api=28, android.injected.invoked.from.ide=true, android.injected.build.abi=arm64-v8a,armeabi-v7a,armeabi, android.injected.restrict.variant.name=debug, android.injected.restrict.variant.project=:app}, systemPropertiesArgs={}, gradleUserHomeDir=C:\Users\Jackie\.gradle, gradleHome=C:\Users\Jackie\.gradle\wrapper\dists\gradle-4.4-all\9br9xq1tocpiv8o6njlyu5op1\gradle-4.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=8, buildCacheEnabled=false, interactive=false}:app:buildInfoDebugLoader

在3.6.0的Android Studio中就看不到了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=/Users/jackie/Desktop/WorkPlace/AndroidWorkPlace/MyApplication2, searchUpwards=true, projectProperties={android.injected.build.density=xhdpi, android.injected.build.api=29, android.injected.invoked.from.ide=true, android.injected.build.abi=x86}, systemPropertiesArgs={idea.active=true, idea.version=3.6}, gradleUserHomeDir=/Users/jackie/.gradle, gradleHome=/Users/jackie/.gradle/wrapper/dists/gradle-5.6.4-all/ankdp27end7byghfw1q2sw75f/gradle-5.6.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=12, buildCacheEnabled=false, interactive=false, writeDependencyLocks=false}app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'com.alibaba:arouter-compiler:1.2.2'.

到了android.gradle插件的执行逻辑里,会被转成如下枚举定义,分别表示不同的编译类型:

//源码路径 gradle3.0.0版本
//Users/jackie/Desktop/WorkPlace/InstantRun/base/build-system/builder-、model/src/main/java/com/android/builder/model/OptionalCompilationStep.java

/**
 * enum describing possible optional compilation steps. This can be used to turn on java byte code
 * manipulation in order to support instant reloading, or profiling, or anything related to
 * transforming java compiler .class files before they are processed into .dex files.
 */
public enum OptionalCompilationStep {
   

    /**
     * presence will turn on the InstantRun feature.
     */
    INSTANT_DEV, 
    /**
     * Force rebuild of cold swap artifacts.
     *
     * <p>Dex files and/or resources.ap_ for ColdswapMode.MULTIDEX and some split APKs for
     * ColdswapMode.MULTIAPK.
     */
    RESTART_ONLY,
    /**
     * Force rebuild of fresh install artifacts.
     *
     * <p>A full apk for ColdswapMode.MULTIDEX and all the split apks for ColdswapMode.MULTIAPK.
     */
    FULL_APK,
}

Gradle4.1的Instant Run

源码分析是基于Gradle4.1版本研究的Instant Run,但是这个版本的Instant Run功能已经削减很多了,下面还会介绍其他版本的Gradle

运行后反编译app-debug.apk会找到多个dex(一般是两个),一开始是通过dex2jar-2.0和jd-gui,但是有时候有些方法无法进行反编译而是依旧显示初始的字节码,很不方便阅读,后来使用了jadx-gui进行直接反编译apk,使用很方便,但依旧还是会有些方法还是显示字节码,所以我是两者交叉着看,但是有时候甚至两者都是字节码,只能上网上直接找别人的博客代码了。

因为我研究的版本是基于Gradle4.1的,仅仅剩下可怜的热插拔和处理资源补丁,而且我还找不到intant-run.zip了,所以我找不到项目中的代码了,不在dex文件中,原本这玩意解压apk之后就有了,所以暂时只能在build下的目录里面寻找了,后面再看看这些文件时如何弄到apk当中。

InstantRunContentProvider的onCreate方法中初始化Socket

  public boolean onCreate() {
   
    if (isMainProcess()) {
     //只支持主进程
      Log.i("InstantRun", "starting instant run server: is main process");
      Server.create(getContext()); 
      return true;
    } 
    Log.i("InstantRun", "not starting instant run server: not main process");
    return true;
  }

然后启动一个socket监听Android Studio推送的消息

  private class SocketServerThread extends Thread {
   
    private SocketServerThread() {
   }
    
    public void run() {
   
      while (true) {
   
        try {
   
          LocalServerSocket localServerSocket = Server.this.serverSocket;
          if (localServerSocket == null)
            return; 
          LocalSocket localSocket = localServerSocket.accept();
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Received connection from IDE: spawning connection thread"); 
          (new Server.SocketServerReplyThread(localSocket)).run();
          if (wrongTokenCount > 50) {
   
            if (Log.isLoggable("InstantRun", 2))
              Log.v("InstantRun", "Stopping server: too many wrong token connections"); 
            Server.this.serverSocket.close();
            return;
          } 
        } catch (Throwable throwable) {
   
          if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Fatal error accepting connection on local socket", throwable); 
        } 
      } 
    }
  }

然后在SocketServerReplyThread的run方法值接受数据并处理

//处理补丁 
private int handlePatches(List<ApplicationPatch> paramList, boolean paramBoolean, int paramInt) {
   
    if (paramBoolean)
      FileManager.startUpdate(); 
    for (ApplicationPatch applicationPatch : paramList) {
   
      String str = applicationPatch.getPath();
      if (str.equals("classes.dex.3")) {
    //如果有classes.dex.3处理热插拔
        paramInt = handleHotSwapPatch(paramInt, applicationPatch);
        continue;
      } 
      if (isResourcePath(str))
        //处理资源补丁
        paramInt = handleResourcePatch(paramInt, applicationPatch, str); 
    } 
    if (paramBoolean)
      FileManager.finishUpdate(true); 
    return paramInt;
  }

这里先来看看ApplicationPatch是什么

public static List<ApplicationPatch> read(DataInputStream paramDataInputStream) throws IOException {
   
    int j = paramDataInputStream.readInt();
    if (Log.logging != null && Log.logging.isLoggable(Level.FINE))
      Log.logging.log(Level.FINE, "Receiving " + j + " changes"); 
    ArrayList<ApplicationPatch> arrayList = new ArrayList(j);
    for (int i = 0; i < j; i++) {
   
      String str = paramDataInputStream.readUTF();
      byte[] arrayOfByte = new byte[paramDataInputStream.readInt()];
      paramDataInputStream.readFully(arrayOfByte);
      arrayList.add(new ApplicationPatch(str, arrayOfByte));
    } 
    return arrayList;
  }

可以看到ApplicationPatch是从Socket接收到的数据输入流中调用readFully来读取的,关于readFully的使用while循环判断byte数组是否已经读满所有数据,如果没有读满则继续读取补充直到读满为止,从而改善输入流出现空档,造成read方法直接跳出的问题。即通过缓冲来保证数量的完整,也算是常用的一种方法。所以以后若要读取特定长度的数据,使用readFully读取更加安全。

1.处理热插拔

下面来看看是如何处理热插拔的

//处理热插拔
private int handleHotSwapPatch(int paramInt, ApplicationPatch paramApplicationPatch) {
   
    if (Log.isLoggable("InstantRun", 2))
      Log.v("InstantRun", "Received incremental code patch"); 
    try {
   
      //创建或获取“data/data/applicationid/files/instant-run/dex”文件路径
      String str1 = FileManager.writeTempDexFile(paramApplicationPatch.getBytes());
      if (str1 == null) {
   
        Log.e("InstantRun", "No file to write the code to");
        return paramInt;
      } 
      if (Log.isLoggable("InstantRun", 2))
        Log.v("InstantRun", "Reading live code from " + str1);
      
      String str2 = FileManager.getNativeLibraryFolder().getPath();
      //反射构造AppPatchesLoaderImpl实例
      Class<?> clazz = Class.forName("com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, (ClassLoader)new DexClassLoader(str1, this.context.getCacheDir().getPath(), str2, getClass().getClassLoader()));
      try {
   
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Got the patcher class " + clazz); 
        PatchesLoader patchesLoader = (PatchesLoader)clazz.newInstance();
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Got the patcher instance " + patchesLoader); 
        //获取热修复所要替换的类的classname
        String[] arrayOfString = (String[])clazz.getDeclaredMethod("getPatchedClasses", new Class[0]).invoke(patchesLoader, new Object[0]);
        if (Log.isLoggable("InstantRun", 2)) {
   
          Log.v("InstantRun", "Got the list of classes ");
          int j = arrayOfString.length;
          for (int i = 0; i < j; i++) {
   
            String str = arrayOfString[i];
            Log.v("InstantRun", "class " + str);
          } 
        } 
        //执行AppPatchesLoaderImpl的load方法进行类修复
        boolean bool = patchesLoader.load();
        if (!bool)
          paramInt = 3; 
      } catch (Exception exception) {
   }
    } catch (Throwable throwable) {
   
      Log.e("InstantRun", "Couldn't apply code changes", throwable);
      paramInt = 3;
    } 
    return paramInt;
  }
//AppPatchesLoaderImpl实现抽象类AbstractPatchesLoaderImpl,重写getPatchedClasses方法,重写方法中有提供需要热更新的类
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
   
  public abstract String[] getPatchedClasses();
  
  public boolean load() {
   
    try {
   
      //《《《《《《最关键的方法,一个个替换类中要替换的方法》》》》》》
      for (String str : getPatchedClasses()) {
   
        ClassLoader classLoader = getClass().getClassLoader();
   
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值