1. 打开 Instant Run,首次运行,会用到 Transform API 修改字节码。
2. 会输出 Instant Run 产出的相关类,在 Application/build/incremental/transforms/instantRun/debug/folders/1/5。
3. 所有源码中的方法:都会加上 $change 代码段,$change 是 IncrementalChange 接口类型。会判断是否为 null,不为 null,然后 调用 $change 的 access$dispatch
。参数为方法签名字符串和方法参数数组,否则调用原逻辑。
4. 后面的 运行:会生成补丁类 dex。输出目录为:Application/build/incremental/transforms/instantRun/debug/folders/4000。
5. 该目录中会有 你修改类的 $override 类。比如修改了 MainActivity 的源代码,就会生成 A$override implements IncrementalChange 类。
6. 然后,IncrementalChange 是接口,得实现 IncrementalChange 接口的 access$dispatch
方法,然后根据第 3. 传来的 方法签名 和 参数 调用改方法。
7. 最后会在 3. 那的 $change 修改为 MainActivity$change(例子),这样不为 null 的话,就会走到 A$change.access$dispatch 达到 Hook Fix 效果 。
8. 4000/5/xxx/com/android/tools/fd/runtime/ 中,会找到 AppPatchesLoaderImpl 类,该类记录了所有 改动类。也继承了 AbstractPatchesLoaderImpl。
1. 为生成的 class 添加 $change 占位字段。
2. $change 未来可能赋的值是,通过 AppPatchesLoaderImpl 内的记录非第一次运行后的所有改动类 ,然后供 load
方法支持设置被修改原类 $change 字段,当收到补丁通知时,只需新建一个 DexClassLoader,去反射加载补丁 dex 中的 AppPatchesLoaderImpl 类,调用 load
方法即可,load
方法中会去加载全部补丁类,并赋值给对应原类的 $change。
Instant Run 的项目,构建的 Application 不是项目的 RealApplication,而是 BootstrapApplication。只是被后来 Hook 替换为 RealApplication 了。
在 Application/build/incremental/bundles/debug/instant-run/AndroidManifest.xml 可以查看到 替换后的 xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.camnter.instantrunresearch"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="25" />
<application
android:name="com.android.tools.fd.runtime.BootstrapApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<activity android:name="com.camnter.instantrunresearch.MainActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
application>
manifest>
1. 会执行 BootstrapApplication 的 attachBaseContext。
2. attachBaseContext 的 createResources(...)
,主要是判断资源 resource.ap_ 是否改变,然后保存 resource.ap_ 的路径到 externalResourcePath 中。
3. attachBaseContext 的 setupClassLoaders(...)
。进行对 Application 的 ClassLoader.parent 的 Hook 操作。Hook 为插件的 DexClassLoader(IncrementalClassLoader),这里封装了一个 IncrementalClassLoader 就可以通过 nativeLibraryPath 路径加载 dex。然后替换 Application 的 ClassLoader.parent 为 IncrementalClassLoader。将 PathClassLoader -> BootClassLoader 转换为 PathClassLoader -> IncrementalClassLoader -> BootClassLoader。
4. attachBaseContext 的 createRealApplication(...)
。会通过 动态生成的 AppInfo 拿到 App 自定义的 RealApplication name。假如有自定义的 RealApplication,反射拿到自定义的 RealApplication。
5. 因为 XML 内的替换为 BootstrapApplication,App 实际上是实例化的是 BootstrapApplication。然而,我们期望在的行为自定义在 RealApplication。
6. 所以,会在 attachBaseContext(...)
内拿到 3. 反射得到的自定义 RealApplication。然后再反射调用自定义 RealApplication.attachBaseContext(...)
完成自定义行为。其实,也是 反射代理。
7. 执行 BootstrapApplication 的 attachBaseContext
之后,会执行到 BootstrapApplication 的 onCreate
。
8. BootstrapApplication 的 onCreate
主要分为三个模块:
- - 8.1 替换 ActivityThread 内所有的 BootstrapApplication 为 RealApplication。
- - 8.2 如果 resource.ap_ 文件有改变,那么新建一个 AssetManager 对象 newAssetManager,然后用 newAssetManager 对象 替换 所有当前 Resource、Resource.Theme 的 mAssets 成员变量。
- - 8.3 如果当前的已经有 Activity 启动了,还需要替换所有 Activity 中 mAssets 成员变量。
- - 8.4 启动 Server,开启 Socket,开始读取数据,当读到 MESSAGE_PATCHES 时,获取代码变化的 ApplicationPatch 列表,然后调用 handlePatches
来处理代码的变化,执行 热温冷 部署的分发。
1. createResources: 创建资源 - 本质就是 copy resources.ap_:
- - 1.1 /data/data/…/files/instant-run/inbox/resources.ap_ 是否存在。
- - 1.2 存在的话,复制到 /data/data/…/files/instant-run/left(or right)/resources.ap_ 下。
- - 1.3 判断是否 复制成功,即有文件。
- - 1.4 判断 2. 路径下的 resources.ap_ 是否没被修改( 0L = 不存在 ),并且如果资源文件的修改时间小于 APP 的 APK 修改时间的话,那么说明这是一个 旧的资源文件( 失效的旧的 resources.ap_ ),应该 忽略( externalResourcePath = null )。
2. setupClassLoaders: HOOK BootstrapApplication 的 ClassLoader 的 类加载机制:
- - 2.1 获取 /data/data/…/files/instant-run/dex 下的所有 .dex 路径( List )。
- - 2.2 如果有 dex 路径 List 没有内容,直接 return。
- - 2.3 获取加载 BootstrapApplication 的 ClassLoader。
- - 2.4 反射该 ClassLoader 的 getLdLibraryPath 方法拿到 nativeLibraryPath。
- - - - 2.4.1 如果成功,则直接复制给 nativeLibraryPath。
- - - - 2.4.2 如果失败,捕获异常,打印 Log 后,设置 nativeLibraryPath = /data/data/…/lib。
- - 2.5 调用了 静态方法 IncrementalClassLoader.inject(….) 后,直接 HOOK 了 该 ClassLoader 的加载模式为:BootClassLoader -> incrementalClassLoader -> classLoader。
- - 2.6 这样的话 BootstrapApplication 的 ClassLoader 的加载 Class 机制就会先走插件 incrementalClassLoader。
3. createRealApplication: 创建 真正的 Application( App 内自定义的 Application ):
- - 3.1 AppInfo 中取出,真正 Application 的 packageName,forName(…) 实例化一个 Class
1. monkeyPatchApplication( Hook BootstrapApplication ):
- - 1.1 Hook 掉 ActivityThread 内的所有 BootstrapApplication 为 RealApplication。
- - 1.2 Hook 掉 ActivityThread 内的所有 LoadedApk 内部的:
- - - - 1.2.1 BootstrapApplication 为 RealApplication。
- - - - 1.2.2 mResDir 为 externalResourceFile。
2. monkeyPatchExistingResources( 加载补丁资源,并 Hook 进 App 内 ):
- - 2.1 反射调用 AssetManager.addAssetPath 方法加载 补丁资源。
- - 2.2 Hook Resource or ResourcesImpl 中的 mAssets,Hook 为 补丁资源。
- - 2.3 Hook Resource or ResourcesImpl 内 Theme or ThemeImpl 中的 mAssets,Hook 为 补丁资源。
- - 2.4 Hook Activity( ContextThemeWrapper )的 initializeTheme 方法去初始化 Theme。
- - 2.5 如果 < 7.0, 先 Hook AssetManager 的 createTheme 方法去创建一个 补丁 Theme。然后 Hook Activity 的 Theme 的 mTheme Field 为 补丁 Theme。
- - 2.6 调用 pruneResourceCaches(@NonNull Object resources) 方法去删除 资源缓存。
3. pruneResourceCache( 由于 hook 进来了 newAssetManager,所以需要把原来运行 Activity 的资源缓存清空 ):
- - 3.1 删除 Resource 内部的 TypedArrayPool 的资源缓存。
- - 3.2 删除 Resource 图片、动画、颜色等资源缓存。
- - 3.3 删除 ResourceImpl 图片、动画、颜色等资源缓存。
可以理解 IncrementalClassLoader 就是 插件 class 的 classLoader
1. 提供了静态方法 inject(ClassLoader classLoader,String nativeLibraryPath,String codeCacheDir,List dexes) 方便将目标 classLoader 的 parent 替换为 IncrementalClassLoader。
2. 这样的话 parent 的父加载的 class 和 res 就走的是 IncrementalClassLoader 的加载。BootClassLoader -> classLoader 就会变为 BootClassLoader -> IncrementalClassLoader -> classLoader。
3. 然而 IncrementalClassLoader 的加载的逻辑又靠 DelegateClassLoader。DelegateClassLoader 是 BaseDexClassLoader 的子类,覆写了 findClass 方法,但是,只是为了打点 Log。
handle(DataInputStream input, DataOutputStream output):
1. 资源校验( res/resources.ap_ )。
2. 处理补丁:
- - 2.1 dex 结尾的格式,就执行 handleColdSwapPatch(…) 冷部署。
- - 2.2 dex 结尾的格式 并且 名字为 “classes.dex.3” 则记录为 热部署。
- - 2.3 名字为 “classes.dex.3” 直接执行热部署 handleHotSwapPatch(…)。
- - 2.4 “res/resources.ap_” 那么直接处理资源补丁 handleResourcePatch(…)。
- - 2.5:
- - - - 2.5.1 温部署 加载补丁 ( 处理资源补丁 ):调用 FileManager.writeAaptResources(…) 处理资源补丁。
- - - - 2.5.2 热部署 加载补丁:
- - - - - - 2.5.2.1 将补丁文件 保存为 /data/data/…/files/instant-run/dex-temp/reload0x?04x.dex。
- - - - - - 2.5.2.2 然后 通过 此 dex 去创建一个 DexClassLoader。
- - - - - - 2.5.2.3 通过创建的 DexClassLoader 去寻找内部的 AppPatchesLoaderImpl 类。
- - - - - - 2.5.2.4 进而获取 getPatchedClasses 方法,得到 String[] classes。
- - - - - - 2.5.2.5 然后打 String[] classes 的 Log。
- - - - - - 2.5.2.6 AppPatchesLoaderImpl 向上转为 PatchesLoader 类型。
- - - - - - 2.5.2.7 调用 ( AppPatchesLoaderImpl )PatchesLoader.load() 方法打上 $override 和 $change 标记位。
- - - - 2.5.3 冷部署 加载补丁:
- - - - - - 2.5.3.1 判断补丁是否是 slice- 开头。
- - - - - - 2.5.3.2 将补丁保存在 /data/data/…/files/instant-run/dex/ 目录下。
3. 重启流程处理:
- - 3.1 热部署:如果更新模式 是 None 或者 热部署。如果要显示 toast。获取前台 Activity,然后用 前台 Activity 显示 toast,然后返回。
- - 3.2 冷部署:
- - - - 3.2.1 获取所有没有 paused 的 Activity。
- - - - 3.2.2 获取外部资源文件路径 /data/data/…/files/instant-run/left(right)/resources.ap_。
- - - - 3.2.3 如果不存在资源文件:MonkeyPatcher.monkeyPatchApplication + MonkeyPatcher.monkeyPatchExistingResources;如果存在存在资源文件:设置更新模式 - 冷部署。
- - 3.3 温部署:
- - - - 3.3.1 先拿到前台显示的 Activity。
- - - - 3.3.2 如果是 温部署 :
- - - - - - 3.3.2.1 然后反射获取 onHandleCodeChange 方法,进而传入 0L 为参数,进行反射调用。
- - - - - - 3.3.2.2 如果,刚才的 handledRestart 标记为 true,那么继续显示 toast,然后重启 Activity 后返回。
- - - - - - 3.3.2.3 最后将更新模式设置为 冷部署。
- - - - 3.3.3 判断更新模式如果是冷部署则返回( 证明没成功调用 onHandleCodeChange )。
1. restartActivityOnUiThread:只在 UiThread 线程执行 updateActivity(…)
2. restartActivity:重启 Activity。
- - 2.1 拿到该 Activity 的最顶层 Parent Activity。
- - 2.2 然后用 最顶层 Parent Activity 执行 recreate 方法。
3. restartApp:重启 App。
- - 3.1 判断 activities 是否没有内容。
- - - - 3.1.1 没有的话,这个方法就不做任何事情。
- - - - 3.1.2 有的话,继续。
- - 3.2 获取前台 Activity。
- - - - 3.2.1 前台 Activity 为 null,那么就拿到 activities 的第一个 Activity 打 Toast,然后直接关闭 App( 杀死进程 )。
- - - - 3.2.2 前台 Activity 为 存在,那么就拿 前台 Activity 打 Toast,然后继续。
- - 3.3 定制了一个 PendingIntent 是为了在未来打开这个 前台 Activity。
- - 3.4 获取 AlarmManager,设置定时任务,再未来的 100ms 后,通过 PendingIntent 打开这个 前台 Activity。
- - 3.5 杀死进程,等待 3.4 的定时任务执行,并打开 前台 Activity,实现重启 App 的效果。
4. showToast:显示 toast。
- - 4.1 尝试获取 activity 的 base context。
4.1.1 拿不到的话,return。
- - 4.2 如果如果 Toast 的内容大于 60 或者有换行( \n ),那么持续时间长。否则,短。
- - 4.3 调用 Toast.makeText(…).show() 显示 Toast。
5. getForegroundActivity:获取前台显示的 Activity,也就是获取全部没有 paused 的 Activity,然后从这个取第一个。
6. getActivities:获取没有 paused 的 Activity。
- - 6.1 反射获取 ActivityThread 的 mActivities Field。
- - 6.2 获取 mActivities 的值,根据版本兼容:
- - - - 6.2.1 拿不到的话,return。
- - - - 6.2.2 如果 > 4.4 && 是 ArrayMap 的话,转。
- - - - 6.2.3 都不是的话,会返回初始化好,没内容的 list。
- - 6.3 遍历 mActivities 值,拿到每一个 ActivityRecord。
- - - - 6.3.1 判断是否是 foregroundOnly:
- - - - - - 6.3.1.1 true 的话,过滤出 ActivityRecord 的 paused == true 的 ActivityRecord。
- - - - - - 6.3.1.2 false 的话,不走过滤逻辑。
- - 6.4 然后反射 3. 下来的 ActivityRecord 的 activity Field。
- - 6.5 拿到 ActivityRecord 的 activity Field 的值,添加到 list 里。
7. updateActivity:调用 restartActivity 重启 Activity。
8. showToastWhenPossible:如果可能的话,显示 Toast。
- - 8.1 获取前台 Activity。
- - 8.2 如果拿到了,就调用 Restarter.showToast(…);如果没拿到,进入重试方法 showToastWhenPossible(…),根据重试次数,不断尝试显示 Toast。
9. showToastWhenPossible:重试显示 Toast 方法,根据重试次数,不断尝试显示 Toast。
- - 9.1 先实例化一个主线程 Handler,用于与主线程通信( 现在 Toast )。
- - 9.2 然后希望在主线程执行的任务 Runnable 内,拿到获取前台显示 Activity。
- - - - 9.2.1 如果此次拿到了,直接调用 showToast(…) 方法显示 Toast。
- - - - 9.2.2 如果此次拿不到,那么递归到下次,继续尝试拿,一直递归到重试次数大于 0 为止。
1. checkInbox:复制资源文件 resources.ap_( 主要用于 创建资源 )。
- - 1.1 判断 /data/data/( applicationId )/files/instant-run/inbox/resources.ap_ 是否存在。
- - 1.2 存在的话,复制到 /data/data/…/files/instant-run/left(or right)/resources.ap_ 下。
2. getDexList:获取 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合( 主要用于 HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )。
- - 2.1 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹下,最近修改的.dex 文件的更新时间,记录为 newestHotswapPatch。
- - 2.2 获取 File: /data/data/( applicationId )/files/instant-run/dex ,但不一定创建。
- - 2.3 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹:
- - - - 2.3.1 如果不存在,那么会创建该文件夹后,将 instant-run.zip 内的所有 .dex ,加上前缀 “slice-” 复制到 /data/data/( applicationId )/files/instant-run/dex 文件夹 中。最后,获取该文件夹内的所有文件,保存在 File[] dexFiles。
- - - - 2.3.2 如果直接存在,直接获取 /data/data/( applicationId )/files/instant-run/dex 文件夹中的所有文件,保存在 File[] dexFiles。
- - 2.4 如果 2.3 内提取 instant-run.zip:
- - - - 2.4.1 失败了。再次校验 /data/data/( applicationId )/files/instant-run/dex 文件夹。遍历所有文件,如果有一个文件的修改时间小于 APK 的修改时间,证明存在旧的 dex。将 instant-run.zip 内的所有 .dex ,加上前缀”slice-” 复制到 /data/data/( applicationId )/files/instant-run/dex 文件夹 中。然后,清空不是提取复制过来的 dex( 旧 dex )。
- - - - 2.4.2 成功了。判断 1. 中的 dex-temp 文件夹是否存在 dex。存在的话,清空 dex-temp 文件夹。
- - 2.5 最后判断 hotSwap 的时间是不是比 coldSwap 的时间新。实质上就是 dex-temp 文件夹内的 files 和 dex 文件夹内的 files,谁最新!如果 hotSwap 的时间比 coldSwap 的时间新,调用 Restarter.showToastWhenPossible 提示 the app is older。
- - 2.6 返回 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路径集合。
3. extractSlices: 提取 instant-run.zip 的资源( 主要用于获取所有 dex 集合,然后实例化一个补丁 ClassLoader 进而 HOOK BootstrapApplication 的 ClassLoader 的 类加载机制 )。
- - 3.1 Class.getResourceAsStream(“/instant-run.zip”) 去加载 instant-run.zip 的资源。
- - 3.2 提取出 instant-run.zip 内的资源( 内部都是 .dex 文件 ):
- - - - 3.2.1 过滤掉 META-INF。
- - - - 3.2.2 过滤掉有 “/” 的 文件或文件夹。
- - - - 3.2.3 找出所有 .dex 文件,将其文件名加上前缀 “slice-” 保存在 Set sliceNames。
- - - - 3.2.4 再将这些 .dex 文件,加载前缀 “slice-“,复制到 /data/data/( applicationId )/files/instant-run/dex 文件夹中。
- - - - 3.2.5 校验 /data/data/( applicationId )/files/instant-run/dex 文件夹中,是非存在不是 3.2.4 复制过来的文件。如果不是 2.4 复制过来的文件,证明是旧 “slice-” 文件,则删除。
4. getTempDexFile: 获取 dex-temp 文件夹下,下版本要创建的 File ( 主要用于 热部署 )。
- - 4.1 获取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夹。
- - 4.2 校验 dex-temp 文件夹:
- - - - 4.2.1 不存在,则创建。
- - - - 4.2.2 存在,则判断是否要清空。清空的话,则删除该文件夹下的所有 .dex 文件。
- - 4.3 然后遍历 dex-temp 文件夹下的文件:
- - - - 4.3.1 截断 “reload” 和 “.dex” 之间的 十六进制版本号。
- - - - 4.3.2 找出版本号最大的 .dex 文件。
- - 4.4 根据 4.3.2 的找出的最大版本号的基础上,最大版本号+1,然后创建一个 “reload最大版本号.dex” 的 File 返回。
5. writeRawBytes:二进制 生成 文件 ( 所有部署 )。主要将 二进制数据 输出为 resources.ap_ or .dex。
6. extractZip:提取出 instant-run.zip 流 内的 .dex 文件 ( 主要用于 温部署 ) 注: 这提取出的 .dex ,不带 “slice-” 前缀。与 extractSlices 方法不同。
- - 6.1 过滤掉 META-INF。
- - 6.2 如果父路径文件夹不存在,则创建。
7. writeDexShard:生成 dex( 主要用于 冷部署 )。
- - 7.1 校验目录:/data/data/( applicationId )/files/instant-run/dex。没有,则创建。
- - 7.2 通过调用 writeRawBytes 方法,在该目录下保存 dex 文件。
8. writeAaptResources:生成资源文件 resources 或者 resources.ap_ ( 主要用于 温部署 ) 路径一般为 /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_( resources )。
- - 8.1 拿到以上路径后,创建该路径的父文件夹。
- - 8.2 生成资源文件:
- - - - 8.2.1 如果生成 resources.ap_:
- - - - - - 8.2.1.1 如果 USE_EXTRACTED_RESOURCES = true,那么该流为 instant-run.zip 的数据,直接复制出内部的 dex 到 /data/data/( applicationId )/files/instant-run/left( right )目录下。
- - - - - - 8.2.1.2 如果 USE_EXTRACTED_RESOURCES = false,生成 /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_。
- - - - 8.2.2 如果生成 resources,那么直接写出 /data/data/( applicationId )/files/instant-run/left( right )/resources。
9. writeTempDexFile:在 dex-temp 文件夹下 生成 dex ( 主要用于 热部署 )。
10. purgeTempDexFiles:清空 dex-temp 下的 .dex 文件 ( 用于清空 热部署 产生的 dex-temp 文件夹中的 dex )。
11. readRawBytes:读取 inbox/resources.ap_ ( 主要用于创建资源时,读取 inbox/resources.ap_ )。
- - 11.1 路径为: /data/data/( applicationId )/files/instant-run/inbox/resources.ap_。
- - 11.2 为了复制到 /data/data/…/files/instant-run/left(or right)/resources.ap_。
1. 遍历所有 被修改的 类名。
2. 拼接出 ???$override 类型。
3. 通过 ClassLoader 加载 ???$override 类。
4. 反射实例化一个 ???$override 类 的实例。
5. 加载 被修改的 类。
6. 反射 被修改的 类 的 $change Field。
7. 反射获取 被修改的 类 的 $change Field 的值。
8. 判断 被修改的 类 的 $change Field 的值。
- - 8.1 如果存在值,反射获取其 $obsolete Field,如果不为 null,则设置为 true。
9. HOOK 被修改的 类 的 $change Field = 4. 实例化好的 ???$override 类。
10. 如果这写过程中抛出异常,返回 false。否则,返回 true。
Instant Run