从零开始分析lnstantRun源码

从零开始分析lnstantRun源码,第1张

从零开始分析lnstantRun源码

背景

Android Studio 2.0 中引入的 Instant Run 是 Run 和 Debug 命令的行为,可以大幅缩短应用更新的时间。尽管首次构建可能需要花费较长的时间,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 }' | 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:GitAndroidRxDemo, 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:UsersJackie.gradle, gradleHome=C:UsersJackie.gradlewrapperdistsgradle-4.4-all9br9xq1tocpiv8o6njlyu5op1gradle-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


public enum OptionalCompilationStep {

    
    INSTANT_DEV, 
    
    RESTART_ONLY,
    
    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 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 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 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();
        Object object1 = classLoader.loadClass(str + "$override").newInstance();
        Field field = classLoader.loadClass(str).getDeclaredField("$change");
        field.setAccessible(true);
        Object object2 = field.get(null);
        if (object2 != null) {
          object2 = object2.getClass().getDeclaredField("$obsolete");
          if (object2 != null)
            object2.set(null, Boolean.valueOf(true)); 
        } 
        field.set(null, object1);
        if (Log.logging != null && Log.logging.isLoggable(Level.FINE))
          Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { str })); 
      } 
    } catch (Exception exception) {
      if (Log.logging != null)
        Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), exception); 
      return false;
    } 
    return true;
  }
}
//AppPatchesLoaderImpl继承AbstractPatchesLoaderImpl,真正的实现类,这里我们只要求change的MainActivity这个类里面的方法
public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
    public static final long BUILD_ID = 1597285889481L;

    public AppPatchesLoaderImpl() {
    }

    public String[] getPatchedClasses() {
        return new String[]{"com.example.jackie.instantrundemo.MainActivity"};
    }
}

其中DexClassLoader的构造方法定义

//dexPath:被解压的apk路径,不能为空。
//optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
//libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
//parent:父类加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。
DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

我们的MainActivity会被修改成这样

public class MainActivity extends AppCompatActivity {
    public static final long serialVersionUID = 2158910920756968252L;

    //重写空构造方法,方便于替换该方法的实现
    public MainActivity() {
        IncrementalChange var1 = $change;
        if(var1 != null) {
            Object[] var10001 = (Object[])var1.access$dispatch("init$args.([Lcom/example/jackie/instantrundemo/MainActivity;[Ljava/lang/Object;)Ljava/lang/Object;", new Object[]{null, new Object[0]});
            Object[] var2 = (Object[])var10001[0];
            this(var10001, (InstantReloadException)null);
            var2[0] = this;
            var1.access$dispatch("init$body.(Lcom/example/jackie/instantrundemo/MainActivity;[Ljava/lang/Object;)V", var2);
        } else {
            super();
        }
    }

    public void onCreate(Bundle savedInstanceState) {
        IncrementalChange var2 = $change;
        if(var2 != null) {
            var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
        } else {
            super.onCreate(savedInstanceState);
            this.setContentView(2130968603);
            if(this.test(30) > 20333005) {
                Log.d("jackie", "==4444099994==sf=dd=ddecf==999=abc==");
            } else {
                Log.d("jackie", "==999999999999=");
            }

            byte b = 0;
            Toast.makeText(this, "hellodd4fdddd", 1).show();
            Log.d("jackie", "===d=666==dd=dddd==abc==" + b);
        }
    }

    public int test(int a) {
        IncrementalChange var2 = $change;
        if(var2 != null) {
            return ((Number)var2.access$dispatch("test.(I)I", new Object[]{this, new Integer(a)})).intValue();
        } else {
            byte age = 100;
            int b = 300189 + age;

            for(int i = 0; i < b + 9; ++i) {
                a += b;
            }

            return 20 + a;
        }
    }
		//增加类的构造方法,方便与修改类的任何构造方法
    MainActivity(Object[] var1, InstantReloadException var2) {
        String var3 = (String)var1[1];
        switch(var3.hashCode()) {
        case -2089128195:
            super();
            return;
        case 173992496:
            this();
            return;
        default:
            throw new InstantReloadException(String.format("String switch could not find '%s' with hashcode %s in %s", new Object[]{var3, Integer.valueOf(var3.hashCode()), "com/example/jackie/instantrundemo/MainActivity"}));
        }
    }
}

在MainActivity中做个小修改,点击小闪电执行Instant Run,可以看到build下面文件夹4000中会找一个MainActivity$override.class和AppPatchesLoaderImpl.class

要替换的MainActivity$override实现了IncrementalChange,从这里面进行方法的替换,所有的方法都会被替换,因为change值不为空

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) {
        AndroidInstantRuntime.setPrivateField($this, new Integer(100), MainActivity.class, "cmd");
    }

    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);
        if($this.test(30) > 20333005) {
            Log.d("jackie", "==44440999940==sf=dd=ddecf==999=abc==");
        } else {
            Log.d("jackie", "==999999999999=");
        }

        byte b = 0;
      	//因为修改了全局变量的值,所以进行处理
        AndroidInstantRuntime.setPrivateField($this, new Integer(((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue() + 100), MainActivity.class, "cmd");
        Toast.makeText($this, "hellodd4fdddd", 1).show();
        Log.d("jackie", "===d=666==dd=dddd==abc==" + b);
    }

    public static int test(MainActivity $this, int a) {
        int ageabc = 100 + ((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue();
        int b = 300189 + ageabc;

        for(int i = 0; i < b + 9; ++i) {
            a += b;
        }

        return 20 + a;
    }

    //替换相对应的方法,最后两个方法我估计是前面MainActivity类里面的copy
    public Object access$dispatch(String var1, Object... var2) {
        switch(var1.hashCode()) {
        case -1227667971:
            return new Integer(test((MainActivity)var2[0], ((Number)var2[1]).intValue()));
        case -641568046:
            onCreate((MainActivity)var2[0], (Bundle)var2[1]);
            return null;
        case 435530788:
            return init$args((MainActivity[])var2[0], (Object[])var2[1]);
        case 1043612718:
            init$body((MainActivity)var2[0], (Object[])var2[1]);
            return null;
        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/example/jackie/instantrundemo/MainActivity"}));
        }
    }
}

2.处理资源补丁(温插拔)

下面来看看是如何处理资源补丁的,但是设计到资源的处理需要重启当前界面,我们先来看看重启App这种状况下的逻辑

//Server类的handle()
case 5:
    if (authenticate(param1DataInputStream)) {
      Activity activity1 = Restarter.getForegroundActivity(Server.this.context);
      if (activity1 != null) {
        if (Log.isLoggable("InstantRun", 2))
          Log.v("InstantRun", "Restarting activity per user request"); 
        Restarter.restartActivityOnUiThread(activity1);
      } 
      continue;
    } 
    return;
case 1:
    if (authenticate(param1DataInputStream)) {
      List list = ApplicationPatch.read(param1DataInputStream);
      if (list != null) {
        bool = Server.hasResources(list);
        i = param1DataInputStream.readInt();
        //处理acitivity或者把资源写到文件中
        i = Server.this.handlePatches(list, bool, i);
        boolean bool1 = param1DataInputStream.readBoolean();
        param1DataOutputStream.writeBoolean(true);
        //重启Activity
        Server.this.restart(i, bool, bool1);
      } 
      continue;
} 

我们先来看看handlePatches里面的handleResourcePatch是如何处理资源的

//Server.class
//处理资源补丁
private static int handleResourcePatch(int paramInt, ApplicationPatch paramApplicationPatch, String paramString) {
    if (Log.isLoggable("InstantRun", 2))
      Log.v("InstantRun", "Received resource changes (" + paramString + ")"); 
    FileManager.writeAaptResources(paramString, paramApplicationPatch.getBytes());
    return Math.max(paramInt, 2);
  }
//FileManager.class
 //writeAaptResources
 public static void writeAaptResources(String paramString, byte[] paramArrayOfbyte) {
    File file1 = getResourceFile(getWriteFolder(false)); //获取资源文件
    File file2 = file1.getParentFile();
    if (!file2.isDirectory() && !file2.mkdirs()) {
      if (Log.isLoggable("InstantRun", 2))
        Log.v("InstantRun", "Cannot create local resource file directory " + file2); 
      return;
    } 
    //把内容写到resources.ap_资源中
    if (paramString.equals("resources.ap_")) {
      writeRawBytes(file1, paramArrayOfbyte);
      return;
    } 
   //把raw资源写入
    writeRawBytes(file1, paramArrayOfbyte);
  }
//getWriteFolder
public static File getWriteFolder(boolean paramBoolean) {
    String str;
    if (leftIsActive()) {
      str = "right";
    } else {
      str = "left";
    } 
    File file = new File(getDataFolder(), str);
    if (paramBoolean && file.exists()) {
      delete(file);
      if (!file.mkdirs())
        Log.e("InstantRun", "Failed to create folder " + file); 
    } 
    return file;
  }
//getResourceFile
private static File getResourceFile(File paramFile) { return new File(paramFile, "resources.ap_"); }

//writeRawBytes
public static boolean writeRawBytes(File paramFile, byte[] paramArrayOfbyte) {
    try {
      BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(paramFile));
      try {
        bufferedOutputStream.write(paramArrayOfbyte);
        bufferedOutputStream.flush();
        return true;
      } finally {
        bufferedOutputStream.close();
      } 
    } catch (IOException iOException) {
      Log.wtf("InstantRun", "Failed to write file, clean project and rebuild " + paramFile, iOException);
      throw new RuntimeException(String.format("InstantRun could not write file %1$s, clean project and rebuild ", new Object[] { paramFile }));
    } 
  }
public static String writeTempDexFile(byte[] paramArrayOfbyte) {
    File file = getTempDexFile();
    if (file != null) {
      writeRawBytes(file, paramArrayOfbyte);
      return file.getPath();
    } 
    Log.e("InstantRun", "No file to write temp dex content to");
    return null;
  }

下面来看看Server.this.restart(i, bool, bool1)是如何处理的,不必拘泥于细节是如何启动的,在里面找到一行关键代码

MonkeyPatcher.monkeyPatchExistingResources(this.context, str, list);

具体实现如下

public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {
    Collection> references;
    if (externalResourceFile != null) {
        //通过反射获取AssetManager
        AssetManager newAssetManager = AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
        mAddAssetPath.setAccessible(true);
       //将当前的资源文件路径添加到AssetManager中
        if (((Integer) mAddAssetPath.invoke(newAssetManager, new Object[]{externalResourceFile})).intValue() == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
        mEnsureStringBlocks.setAccessible(true);
        //进行资源初始化StringBlock对象
        mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
        if (activities != null) {
            for (Activity activity : activities) {
                Resources resources = activity.getResources();
                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable e) {
                    throw new IllegalStateException(e);
                }
                Resources.Theme theme = activity.getTheme();
                try {
                    Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                    ma.setAccessible(true);
                    ma.set(theme, newAssetManager);
                } catch (NoSuchFieldException e2) {
                    Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                    themeField.setAccessible(true);
                    Object impl = themeField.get(theme);
                    Field ma2 = impl.getClass().getDeclaredField("mAssets");
                    ma2.setAccessible(true);
                    ma2.set(impl, newAssetManager);
                } catch (Throwable e3) {
                    Log.e(Logging.LOG_TAG, "Failed to update existing theme for activity " + activity, e3);
                }
                Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                mt.setAccessible(true);
                mt.set(activity, (Object) null);
                Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
                mtm.setAccessible(true);
                mtm.invoke(activity, new Object[0]);
                if (Build.VERSION.SDK_INT < 24) {
                    Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
                    mCreateTheme.setAccessible(true);
                    Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
                    Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                    mTheme.setAccessible(true);
                    mTheme.set(theme, internalTheme);
                }
                pruneResourceCaches(resources);
            }
        }
              //获取当前JVM中的ResourcesManager的final ArrayMap > mActiveResources
        if (Build.VERSION.SDK_INT >= 19) {
            Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
            mGetInstance.setAccessible(true);
            Object resourcesManager = mGetInstance.invoke((Object) null, new Object[0]);
            try {
                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                references = ((ArrayMap) fMActiveResources.get(resourcesManager)).values();
            } catch (NoSuchFieldException e4) {
                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                references = (Collection) mResourceReferences.get(resourcesManager);
            }
        } else {
            Class activityThread = Class.forName("android.app.ActivityThread");
            Field fMActiveResources2 = activityThread.getDeclaredField("mActiveResources");
            fMActiveResources2.setAccessible(true);
            references = ((HashMap) fMActiveResources2.get(getActivityThread(context, activityThread))).values();
        }
      	//循环便利当前Resources,将其成员变量mAssets指向自定义的newAssetManager
        for (WeakReference wr : references) {
            Resources resources2 = (Resources) wr.get();
            if (resources2 != null) {
                try {
                    Field mAssets2 = Resources.class.getDeclaredField("mAssets");
                    mAssets2.setAccessible(true);
                    mAssets2.set(resources2, newAssetManager);
                } catch (Throwable th) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources2);
                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                //更新资源
                resources2.updateConfiguration(resources2.getConfiguration(), resources2.getDisplayMetrics());
            }
        }
    }
}

在研究过程中,本来想基于退出gradle4.1然后在gradle2.2.3重新再搞一下,后面想想不把4.1研究透再去搞2.2.3总是心有不甘,网上也基本找不到gradle 4.1的研究的,其实在2.3.0之后就没有instant-run.zip包了,但是好像所有人都没有提到这点,难道他们都是基于2.2.3或者更早的?

找不到的项目代码去哪里了

下面来看看我们的MainActivity,MyApplication等文件在去哪里了,找到之前的Server(SocketServerThread),明白我们是从AS端接受这些文件的,接受这些dex文件后,存储在app的cache文件中(getWriteFolder),并进行处理,

private class SocketServerThread extends Thread {
    private SocketServerThread() {}
    
    public void run() {
      while (true) {
        try {
          LocalServerSocket localServerSocket = Server.this.serverSocket;
          if (localServerSocket == null)
            return; 
          //接受AS发过来的文件
          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); 
        } 
      } 
    }
  }

下面可以使用我们一开始下载的源码,在instant-run下面的intant-run-client,InstantClient类中,将文件发送到设备中

private void transferBuildIdToDevice(@NonNull IDevice device, @NonNull String buildId) {
    try {
        String remoteIdFile = getDeviceIdFolder(mPackageName);
        //noinspection SSbasedInspection This should work
        File local = File.createTempFile("build-id", "txt");
        local.deleteOnExit();
        Files.write(buildId, local, Charsets.UTF_8);
        device.pushFile(local.getPath(), remoteIdFile);
    } catch (IOException ioe) {
        mLogger.warning("Couldn't write build id file: %s", ioe);
    } catch (AdbCommandRejectedException | TimeoutException | SyncException e) {
        mLogger.warning("%s", Throwables.getStackTraceAsString(e));
    }
}

我们回看之前的handleHotSwapPatch方法中读取文件的方式,可以看到在cache文件中

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {
    ···
    try {
        String dexFile = FileManager.writeTempDexFile(patch.getBytes());
        ···
        String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                context.getCacheDir().getPath(), nativeLibraryPath,
                getClass().getClassLoader());

使用InstantRun更新的文件去哪里了

下面来看看我们的MainActivity$override,MyApplication$override,AppPatchesLoaderImpl等文件在去哪里了,我们进入应用内部的/data/data/com.example.jackie.instantrundemo/files/instant-run/dex-temp中会发现一个reload0x0000.dex文件,里面就有提供更新的内容,instant-run里面中的right是用于存储resource.ap_。

其他一些Gradle版本 Gradle2.2.3

在当前版本中,我们可以看到instant-run.zip包,里面包含的项目的代码和要替换的代码。解压后可以看到AndroidManifest.xml文件,从 AndroidManifest.xml 中我们看到了MyApplication 被 BootstrapApplication 替代,那么我们可以想象当 Application 为 Instant-run 自己的时,那么它至少可以像加载插件一样在应用启动的时候(程序入口)加载替换自己的dex和资源文件,从而达到修改运行程序的目的。

@Override
protected void attachbaseContext(Context context) {
    // As of Marshmallow, we use APK splits and don't need to rely on
    // reflection to inject classes and resources for coldswap
    //noinspection PointlessBooleanexpression
    //是否使用了apk分裂安装
    if (!AppInfo.usingApkSplits) {
        String apkFile = context.getApplicationInfo().sourceDir;
        long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;
        //判断资源resource.ap_是否进行了修改,将其路径保存在externalResourcePath
        createResources(apkModified);
        //创建classloade
        //delegateClassLoader->PathClassLoader->IncrementalClassLoader->BootClassLoader
        setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
    }
    //创建正真的Application
    createRealApplication();

    // This is called from ActivityThread#handleBindApplication() -> LoadedApk#makeApplication().
    // Application#mApplication is changed right after this call, so we cannot do the monkey
    // patching here. So just forward this method to the real Application instance.
    super.attachbaseContext(context);

    if (realApplication != null) {
        try {
            Method attachbaseContext =
                    ContextWrapper.class.getDeclaredMethod("attachbaseContext", Context.class);
            attachbaseContext.setAccessible(true);
            //执行自己的Application的attachbaseContext方法
            attachbaseContext.invoke(realApplication, context);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}

该方法的主要目的在于,创建自定义的ClassLoader和真正的Application实例。而 BootstrapApplication 只起到一个壳子的作用。

替换Application的时候我们可以看看MonkeyPatcher中是如何替换的

public class MonkeyPatcher {
    public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {
        Class activityThread;
        Class loadedApkClass;
        try {
            //获取ActivityThread实例
            activityThread = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = getActivityThread(context, activityThread);
            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
            mInitialApplication.setAccessible(true);
            //替换ActivityThread的mInitialApplication成员变量
            Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
            if (realApplication != null && initialApplication == bootstrap) {
                mInitialApplication.set(currentActivityThread, realApplication);
            }
            //替换ActivityThread的mAllApplications队列中的BootstrapApplication为realApplication
            if (realApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List allApplications = (List) mAllApplications.get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                    if (allApplications.get(i) == bootstrap) {
                        allApplications.set(i, realApplication);
                    }
                }
            }
            loadedApkClass = Class.forName("android.app.LoadedApk");
        } catch (ClassNotFoundException e) {
            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
        } catch (Throwable e2) {
            IllegalStateException illegalStateException = new IllegalStateException(e2);
        }
        Field mApplication = loadedApkClass.getDeclaredField("mApplication");
        mApplication.setAccessible(true);
        Field mResDir = loadedApkClass.getDeclaredField("mResDir");
        mResDir.setAccessible(true);
        Field mLoadedApk = null;
        try {
            mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
        } catch (NoSuchFieldException e3) {
        }
        //final ArrayMap> mPackages
        //final ArrayMap> mResourcePackages
        for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
            Field field = activityThread.getDeclaredField(fieldName);
            field.setAccessible(true);
            for (Entry> entry : ((Map) field.get(currentActivityThread)).entrySet()) {
                Object loadedApk = ((WeakReference) entry.getValue()).get();
                if (loadedApk != null && mApplication.get(loadedApk) == bootstrap) {
                    //将ActivityThread的mPackages|mResourcePackages的LoadedApk
                    //LoadedApk的成员变量mApplication的BootstrapApplication替换为realApplication
                    if (realApplication != null) {
                        mApplication.set(loadedApk, realApplication);
                    }
                    //LoadedApk的成员变量mResDir替换为externalResourceFile
                    if (externalResourceFile != null) {
                        mResDir.set(loadedApk, externalResourceFile);
                    }
                    //将realApplication的mLoadedApk替换为BootstrapApplication的mLoadedApk
                    if (!(realApplication == null || mLoadedApk == null)) {
                        mLoadedApk.set(realApplication, loadedApk);
                    }
                }
            }
        }
    }

    public static Object getActivityThread(Context context, Class activityThread) {
        if (activityThread == null) {
            try {
                activityThread = Class.forName("android.app.ActivityThread");
            } catch (Throwable th) {
                return null;
            }
        }
        //获取ActivityThread的静态变量sCurrentActivityThread
        Method m = activityThread.getMethod("currentActivityThread", new Class[0]);
        m.setAccessible(true);
        Object currentActivityThread = m.invoke(null, new Object[0]);
        if (currentActivityThread != null || context == null) {
            return currentActivityThread;
        }
        //获取BootstrapApplication的mLoadedApk
        Field mLoadedApk = context.getClass().getField("mLoadedApk");
        mLoadedApk.setAccessible(true);
        Object apk = mLoadedApk.get(context);
        Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
        mActivityThreadField.setAccessible(true);
        //返回mLoadedApk的成员变量mActivityThread
        return mActivityThreadField.get(apk);
    }
    
}
  • 获取ActivityThread实例

    • 先获取ActivityThread的静态变量sCurrentActivityThread;
    • 否则获取Application对象的成员变mLoadedApk的成员对象mActivityThread;
  • 替换ActivityThread的mInitialApplication为realApplication

  • 替换ActivityThread的mAllApplications中的所有的BootstrapApplication为realApplication

  • 替换ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application为realApplication。

  • 替换realApplication中的mLoadedApk为BootstrapApplication的MLoadedApk

AndroidMainifest文件中的Application如何被替换

首先我们切换到之前下载的git库中的base/build-system目录下面,然后切换到tag gradle_2.2.2分支下,然后全局搜索该Application值

//ManifestMerger2.class
static final String BOOTSTRAP_APPLICATION
        = "com.android.tools.fd.runtime.BootstrapApplication";

被调用的地方

//如方法定义一样,替换为instantrun的APPLICATION
@NonNull
private static Xmldocument instantRunReplacement(Xmldocument document) {
    Optional applicationOptional = document
            .getByTypeAndKey(ManifestModel.NodeTypes.APPLICATION, null );
    if (applicationOptional.isPresent()) {
        XmlElement application = applicationOptional.get();
        Attr nameAttribute = application.getXml().getAttributeNodeNS(
                SdkConstants.ANDROID_URI, "name");

        if (nameAttribute != null) {
            String originalAppName = nameAttribute.getValue();
            if (BOOTSTRAP_APPLICATION.equals(originalAppName)) {
                return document;
            }
            application.getXml().setAttribute(SdkConstants.ATTR_NAME, originalAppName);
            application.getXml().setAttributeNS(
                    SdkConstants.ANDROID_URI,
                    nameAttribute.getName(),
                    BOOTSTRAP_APPLICATION);
        } else {
            application.getXml().setAttributeNS(
                    SdkConstants.ANDROID_URI,
                    SdkConstants.ANDROID_NS_NAME_PREFIX + SdkConstants.ATTR_NAME,
                    BOOTSTRAP_APPLICATION);
        }
    }
    return document.reparse();
}

调用链ManifestMerger2 -> MergeManifests ->TaskManager.createMergeLibManifestsTask -> LibraryTaskManager.createTasksForVariantData -> LibraryPlugin.createTaskManager,LibraryPlugin是一个gradle Plugin插件,会被我们自动注册处理这些东西。

小结

  • 修改源代码,每个类增加 $change 字段;
  • 替换 Application ;
  • 创建自己的类加载器,修改正常的类加载器的加载顺序;
  • 开启 Socket 监听 AndroidStudio 推送的消息;
  • 处理消息(热、温、冷)
    • 热:给类的 $change 字段赋值,改变运行逻辑;
    • 温:替换加载新的资源,重启当前 Activity 生效;
    • 冷:写入新的 dex 文件,重新加载新的 dex;

Gradle2.3.0

去掉了 BootstrapApplication 替换,直接启动一个 InstantRunService 用来启动 Socket 与 Android Studio 进行信息传递;

去掉了所谓的冷启动(handleColdSwapPatch),需要冷启动的时候直接进行碎片安装重启不就好了;

启动InstantRunService是通过adb命令启动

Instant Run的一些问题

调用Instant Run后,杀死进程并重启APP后,有时候这些新的的修改并没有在当前的APP中。

FileManager.class的一些方法

public class FileManager {
    public static final String CLASSES_DEX_SUFFIX = ".dex";
    private static final String FILE_NAME_ACTIVE = "active";
    private static final String FOLDER_NAME_LEFT = "left";
    private static final String FOLDER_NAME_RIGHT = "right";
    private static final String RELOAD_DEX_PREFIX = "reload";
    private static final String RESOURCE_FILE_NAME = "resources.ap_";
    private static final String RESOURCE_FOLDER_NAME = "resources";
    
    public static String writeTempDexFile(byte[] bytes) {
        File file = getTempDexFile();
        if (file != null) {
            writeRawBytes(file, bytes);
            return file.getPath();
        }
        Log.e(BootstrapApplication.LOG_TAG, "No file to write temp dex content to");
        return null;
    }
    
    public static File getTempDexFile() {
        //“data/data/applicationid/files/instant-run”
        File dataFolder = getDataFolder();
        //“data/data/applicationid/files/instant-run/dex-temp”
        File dexFolder = getTempDexFileFolder(dataFolder);
        if (dexFolder.exists()) {
            if (!sHavePurgedTempDexFolder) {
                //删除之前的
                purgeTempDexFiles(dataFolder);
            }
        } else if (dexFolder.mkdirs()) {
            sHavePurgedTempDexFolder = true;
        } else {
            Log.e(BootstrapApplication.LOG_TAG, "Failed to create directory " + dexFolder);
            return null;
        }
        File[] files = dexFolder.listFiles();
        int max = -1;
        if (files != null) {
            for (File file : files) {
                String name = file.getName();
                //文件以“reload"开头,以".dex"结尾
                if (name.startsWith(RELOAD_DEX_PREFIX) && name.endsWith(CLASSES_DEX_SUFFIX)) {
                    try {
                        //获取中间版本,取最高版本
                        int version = Integer.decode(name.substring(RELOAD_DEX_PREFIX.length(), name.length() - CLASSES_DEX_SUFFIX.length())).intValue();
                        if (version > max) {
                            max = version;
                        }
                    } catch (NumberFormatException e) {
                    }
                }
            }
        }
        //创建版本号+1的reloadxxx.dex文件,比如reload0x0000.dex
        File file2 = new File(dexFolder, String.format("%s0x%04x%s", new Object[]{RELOAD_DEX_PREFIX, Integer.valueOf(max + 1), CLASSES_DEX_SUFFIX}));
        if (!Log.isLoggable(BootstrapApplication.LOG_TAG, 2)) {
            return file2;
        }
        Log.v(BootstrapApplication.LOG_TAG, "Writing new dex file: " + file2);
        return file2;
    }
}

InstantTransfrom

InstantRun是通过ASM插件来给每个方法前插入change,然后在运行instant-run的时候进行替换,地址在这,我们可以参考他们的实现来进行我们自己的热修复框架开发。

build下面的文件

com.android.build.gradle.tasks.ir.FastDeployRuntimeExtractorTask这个类负责从gradle插件的jar包中把instant-run-server.jar提取出来放到build目录下面

// we could just extract the instant-runtime jar and place it as a stream once we
// don't have to deal with AppInfo replacement.
@TaskAction
public void extract() throws IOException {
    URL fdrJar =
            FastDeployRuntimeExtractorTask.class.getResource(
                    "/instant-run/instant-run-server.jar");
    if (fdrJar == null) {
        throw new RuntimeException("Couldn't find Instant-Run runtime library");
    }
  URLConnection urlConnection = fdrJar.openConnection();
  ···
}
总结

从零开始分析InstantRun确实遇到不少问题:

  1. git代理和全局代理,以后再以后下载一些第三方源码时会大大提高效率;

  2. 反编译使用不同的工具交叉查看

  3. 下载了不同版本的AS,ndk配置问题,graldle下载缓慢问题(3.3解决)

  4. 不同版本Gradle,InstantRun机制不一样,原先使用instant-run.zip,然后被移除,直接弄到cache和dex-temp中,最终AS3.5废弃而使用Apply Changes

  5. 理解的Instant Run,感觉热修复也没那么难啊,但不同框架使用不用的原理需要继续研究。

参考文章

https://blog.csdn.net/qq_33487412/article/details/78458000

https://juejin.im/post/6844903952287268877

https://developer.android.google.cn/studio/run/index.html?authuser=19#apply-changes

https://www.zhihu.com/question/309772986

https://blog.csdn.net/guolin_blog/article/details/51271369

https://github.com/stven0king/InstantRun-ApkParse(非常完整)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4669067.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-06
下一篇 2022-11-06

发表评论

登录后才能评论

评论列表(0条)

保存