背景
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的改动
-
如果应用的minSdkVersion小于21,可能多数的Instant Run功能会挂掉,这里提供一个解决方法,通过product flavor建立一个minSdkVersion大于21的新分支,用来debug。
-
Instant Run目前只能在主进程里运行,如果应用是多进程的,类似微信,把webView抽出来单独一个进程,那热、温拔插会被降级为冷拔插。后面的版本好像就只能在主进程中了,冷插拔都没了
-
在Windows下,Windows Defender Real-Time Protection可能会导致Instant Run挂掉,可用通过添加白名单列表解决。
-
暂时不支持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) { OptionalapplicationOptional = 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确实遇到不少问题:
-
git代理和全局代理,以后再以后下载一些第三方源码时会大大提高效率;
-
反编译使用不同的工具交叉查看
-
下载了不同版本的AS,ndk配置问题,graldle下载缓慢问题(3.3解决)
-
不同版本Gradle,InstantRun机制不一样,原先使用instant-run.zip,然后被移除,直接弄到cache和dex-temp中,最终AS3.5废弃而使用Apply Changes
-
理解的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(非常完整)
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)