Tinker 热修复的优点和缺点,以及为什么选择Tinker热修复和Tinker的实现原理

热补丁方案的比较

**
Tinker QZone AndFix(阿里) Robust(美团)
类替换 yes yes no no
So替换 yes no no no
资源替换 yes yes no no
全平台支持 yes yes yes yes
即时生效 no no yes yes
性能损耗 较小 较大 较小 较小
补丁包大小 较小 较大 一般 一般
开发透明 yes yes no no
复杂度 较低 较低 复杂 复杂
gradle支持 yes no no no
Rom体积 较大 较小 较小 较小
成功率 较高 较高 一般 最高
  1. AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;在native动态替换java层的方法,通过native层hook java层的代码。

image.png

  1. Robust(字节码插装技术)兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;在对每个函数都在编译打包阶段自动的插入了一段代码,类似于代理,将方法的执行的代码重定向到其他方法中。

image.png

  1. Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。Qzone和Tinker的原理类似 ClassLoader的双亲委派机制(比如java.String(系统) com.String(自定义) 会加载java.String这个类,使自定义的优先于系统的这样就只会加载自定义的类 dex -> pathList的顶部 -> App
  2. Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk的dex的区别,补丁包中的内容即为两者差分,运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件(Tinker 必须要重启App后才能生效)

image.png

特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。

Tinker的限制

由于原理与系统限制,Tinker有以下已知问题:

  1. Tinker不支持修改AndroidManifest.xmlTinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification ``icon以及桌面图标
  6. 社区人员维护的,由于开源免费,gradle最新版本不支持兼容性存在一定的问题,目前必须4.0以下的gradle才可以。集成项目中需要大量的时间去校验和验证改成错误等。

    Tinker的使用

  7. 引入插件依赖

    1. dependencies {
    2. classpath 'com.android.tools.build:gradle:3.5.3'
    3. classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
    4. changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
    5. exclude group: 'com.android.tools.build', module: 'gradle'
    6. }
    7. }
    8. configurations.all {
    9. it.resolutionStrategy.cacheDynamicVersionsFor(5, 'minutes')
    10. it.resolutionStrategy.cacheChangingModulesFor(0, 'seconds')
    11. }

    gradle.properties中设置tinker的版本和ID(当生成dex包时,顺序+1即可)

    1. TINKER_VERSION=1.9.14.17
    2. TINKER_ID=1063
    3. #是否开启tinker
    4. TINKER_ENABLE=true
    5. #是否是gradle 3.x的版本
    6. GRADLE_3=true
  8. 引入tinker核心库

    1. api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    2. // Maven local cannot handle transist dependencies.
    3. implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
    4. annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    5. compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    6. compileOnly("com.tencent.tinker:tinker-android-anno-support:${TINKER_VERSION}") { changing = true }
    7. //多分包依赖
    8. implementation "androidx.multidex:multidex:2.0.1"

    defaultConfig 配置

    1. defaultConfig {
    2. ......
    3. /**
    4. * you can use multiDex and install it in your ApplicationLifeCycle implement
    5. * 开启多分包
    6. */
    7. multiDexEnabled true
    8. //tinker混淆情况处理
    9. multiDexKeepProguard file("tinker_multidexkeep.pro")
    10. /**
    11. * buildConfig can change during patch!
    12. * we can use the newly value when patch
    13. * 配置patch信息
    14. */
    15. buildConfigField "String", "MESSAGE", "\"I am the base apk\""
    16. /**
    17. * client version would update with patch
    18. * so we can get the newly git version easily!
    19. * 配置TINKER_ID
    20. */
    21. buildConfigField "String", "TINKER_ID", "\"${TINKER_ID}\""
    22. /**
    23. * 支持全平台
    24. */
    25. buildConfigField "String", "PLATFORM", "\"all\""
    26. }
    27. //recommend 开启jumboMode
    28. dexOptions {
    29. jumboMode = true
    30. }
  9. 创建ApplicationLike代理类

    1. @DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",//自动帮助生成SampleApplication类这个类就是Application
    2. flags = ShareConstants.TINKER_ENABLE_ALL,
    3. loadVerifyFlag = false)
    4. public class SampleApplicationLike extends DefaultApplicationLike {
    5. private static final String TAG = "Tinker.SampleApplicationLike";
    6. public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
    7. long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
    8. super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    9. }
    10. @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    11. @Override
    12. public void onBaseContextAttached(Context base) {
    13. super.onBaseContextAttached(base);
    14. //you must install multiDex whatever tinker is installed!
    15. MultiDex.install(base);
    16. SampleApplicationContext.application = getApplication();
    17. SampleApplicationContext.context = getApplication();
    18. }
    19. @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    20. public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
    21. getApplication().registerActivityLifecycleCallbacks(callback);
    22. }
    23. }
  10. 创建Tinker管理类TinkerManager,在Tinker的Demo中提供了这个类,直接拿过来即可 在这里

  11. ApplicationLike中的onBaseContextAttached执行TinkerManager.installedTinker(this)

    1. TinkerManager.setTinkerApplicationLike(this);
    2. TinkerManager.initFastCrashProtect();
    3. //should set before tinker is installed
    4. TinkerManager.setUpgradeRetryEnable(true);
    5. //optional set logIml, or you can use default debug log
    6. TinkerInstaller.setLogIml(new MyLogImp());
    7. //installTinker after load multiDex
    8. //or you can put com.tencent.tinker.** to main dex
    9. TinkerManager.installTinker(this);
    10. Tinker tinker = Tinker.with(getApplication());
  12. 配置Application,编译项目会得到一个自动生成的SampleApplication的class ```groovy public class SampleApplication extends TinkerApplication {

    public SampleApplication() {

    1. super(15, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false, false);

    }

}

  1. 然后设置到manifest
  2. ```groovy
  3. <application
  4. android:name=".app.SampleApplication"
  5. android:icon="@mipmap/ic_launcher"
  6. android:label="@string/app_name"
  7. android:theme="@style/AppTheme">
  8. ......
  1. 生成基础apk包

Tinker 脚本配置 tinker.gradle,一般我们只需要配置ext中的tinkerEnabled、tinkerOldApkPath、tinkerApplyMappingPath、tinkerApplyResourcePath、tinkerBuildFlavorDirectory这几个选项,其他的配置一般都不要改动。然后将Tinker脚本文件引入即可:apply from: 'tinker.gradle'

平时开发不用开启tinkerEnabled 设置为FALSE即可,如果需要热修复就需要开启tinkerEnabled打基准包(平时一定要做好app的版本tag,当app上线后即使打tag,以后根据tag的代码来打基准包)

  1. def bakPath = file("${buildDir}/bakApk/")
  2. ext {
  3. //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
  4. tinkerEnabled = true
  5. //基准apk路径
  6. tinkerOldApkPath = "${bakPath}/app-debug-0830-17-11-57.apk"
  7. //未开启混淆,则不需要填写
  8. tinkerApplyMappingPath = "${bakPath}/"
  9. //基准apk中的R文件路径
  10. tinkerApplyResourcePath = "${bakPath}/app-debug-0830-17-11-57-R.txt"
  11. //如果你修复了res文件,需要指定你bug版本的R.txt文件
  12. tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0830-17-11-57-R.txt"
  13. }
  14. //获取旧版本路径
  15. def getOldApkPath() {
  16. return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
  17. }
  18. //获取mapping文件路径
  19. def getApplyMappingPath() {
  20. return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
  21. }
  22. //获取资源mapping路径
  23. def getApplyResourceMappingPath() {
  24. return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
  25. }
  26. //Tinker是否可用,是否
  27. def buildWithTinker() {
  28. return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
  29. }
  30. //获取tinker差分包目录
  31. def getTinkerBuildFlavorDirectory() {
  32. return ext.tinkerBuildFlavorDirectory
  33. }
  34. if (buildWithTinker()) {
  35. apply plugin: 'com.tencent.tinker.patch'
  36. tinkerPatch {
  37. /**
  38. * 默认为null
  39. * 将旧的apk和新的apk建立关联
  40. * 从build / bakApk添加apk
  41. */
  42. oldApk = getOldApkPath()
  43. /**
  44. * 可选,默认'false'
  45. *有些情况下我们可能会收到一些警告
  46. *如果ignoreWarning为true,我们只是断言补丁过程
  47. * case 1:minSdkVersion低于14,但是你使用dexMode与raw。
  48. * case 2:在AndroidManifest.xml中新添加Android组件,
  49. * case 3:装载器类在dex.loader {}不保留在主要的dex,
  50. * 它必须让tinker不工作。
  51. * case 4:在dex.loader {}中的loader类改变,
  52. * 加载器类是加载补丁dex。改变它们是没有用的。
  53. * 它不会崩溃,但这些更改不会影响。你可以忽略它
  54. * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
  55. */
  56. ignoreWarning = true
  57. /**
  58. * 可选,默认为“true”
  59. * 是否签名补丁文件
  60. * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
  61. * 我们将使用sign配置与您的构建类型
  62. */
  63. useSign = true
  64. /**
  65. * 可选,默认为“true”
  66. * 是否使用tinker构建
  67. */
  68. tinkerEnable = buildWithTinker()
  69. /**
  70. * 警告,applyMapping会影响正常的android build!
  71. */
  72. buildConfig {
  73. /**
  74. * 可选,默认为'null'
  75. * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
  76. * apk映射文件如果minifyEnabled是启用!
  77. * 警告:你必须小心,它会影响正常的组装构建!
  78. */
  79. applyMapping = getApplyMappingPath()
  80. /**
  81. * 可选,默认为'null'
  82. * 最好保留R.txt文件中的资源id,以减少java更改
  83. */
  84. applyResourceMapping = getApplyResourceMappingPath()
  85. /**
  86. * 必需,默认'null'
  87. * 因为我们不想检查基地apk与md5在运行时(它是慢)
  88. * tinkerId用于在试图应用补丁时标识唯一的基本apk。
  89. * 我们可以使用git rev,svn rev或者简单的versionCode。
  90. * 我们将在您的清单中自动生成tinkerId
  91. */
  92. tinkerId = TINKER_ID.toInteger()
  93. /**
  94. * 如果keepDexApply为true,则表示dex指向旧apk的类。
  95. * 打开这可以减少dex diff文件大小。
  96. */
  97. keepDexApply = false
  98. /**
  99. * optional, default 'false'
  100. * Whether tinker should treat the base apk as the one being protected by app
  101. * protection tools.
  102. * If this attribute is true, the generated patch package will contain a
  103. * dex including all changed classes instead of any dexdiff patch-info files.
  104. */
  105. isProtectedApp = false
  106. /**
  107. * optional, default 'false'
  108. * Whether tinker should support component hotplug (add new component dynamically).
  109. * If this attribute is true, the component added in new apk will be available after
  110. * patch is successfully loaded. Otherwise an error would be announced when generating patch
  111. * on compile-time.
  112. *
  113. * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
  114. */
  115. supportHotplugComponent = false
  116. }
  117. dex {
  118. /**
  119. * 可选,默认'jar'
  120. * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
  121. * 对于jar,我们将使用zip格式重新包装dexes。
  122. * 如果你想支持下面14,你必须使用jar
  123. * 或者你想保存rom或检查更快,你也可以使用原始模式
  124. */
  125. dexMode = "jar"
  126. /**
  127. * 必需,默认'[]'
  128. * apk中的dexes应该处理tinkerPatch
  129. * 它支持*或?模式。
  130. */
  131. pattern = ["classes*.dex",
  132. "assets/secondary-dex-?.jar"]
  133. /**
  134. * 必需,默认'[]'
  135. * 警告,这是非常非常重要的,加载类不能随补丁改变。
  136. * 因此,它们将从补丁程序中删除。
  137. * 你必须把下面的类放到主要的dex。
  138. * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
  139. * 自己的tinkerLoader,和你使用的类
  140. */
  141. loader = [
  142. //use sample, let BaseBuildInfo unchangeable with tinker
  143. "tinker.sample.android.app.BaseBuildInfo"
  144. ]
  145. }
  146. lib {
  147. /**
  148. * 可选,默认'[]'
  149. * apk中的图书馆应该处理tinkerPatch
  150. * 它支持*或?模式。
  151. * 对于资源库,我们只是在补丁目录中恢复它们
  152. * 你可以得到他们在TinkerLoadResult与Tinker
  153. */
  154. pattern = ["lib/*/*.so"]
  155. }
  156. res {
  157. /**
  158. * 可选,默认'[]'
  159. * apk中的什么资源应该处理tinkerPatch
  160. * 它支持*或?模式。
  161. * 你必须包括你在这里的所有资源,
  162. * 否则,他们不会重新包装在新的apk资源。
  163. */
  164. pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
  165. /**
  166. * 可选,默认'[]'
  167. * 资源文件排除模式,忽略添加,删除或修改资源更改
  168. * *它支持*或?模式。
  169. * *警告,我们只能使用文件没有relative与resources.arsc
  170. */
  171. ignoreChange = ["assets/sample_meta.txt"]
  172. /**
  173. * 默认100kb
  174. * *对于修改资源,如果它大于'largeModSize'
  175. * *我们想使用bsdiff算法来减少补丁文件的大小
  176. */
  177. largeModSize = 100
  178. }
  179. packageConfig {
  180. /**
  181. *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
  182. * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
  183. * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
  184. * 或TinkerLoadResult.getPackageConfigByName
  185. * 我们将从旧的apk清单为您自动获取TINKER_ID,
  186. * 其他配置文件(如下面的patchMessage)不是必需的
  187. */
  188. configField("patchMessage", "tinker is sample to use")
  189. /**
  190. *只是一个例子,你可以使用如sdkVersion,品牌,渠道...
  191. * 你可以在SamplePatchListener中解析它。
  192. * 然后你可以使用补丁条件!
  193. */
  194. configField("platform", "all")
  195. /**
  196. * 补丁版本通过packageConfig
  197. */
  198. configField("patchVersion", "1.0.2")
  199. }
  200. //或者您可以添加外部的配置文件,或从旧apk获取元值
  201. //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
  202. //project.tinkerPatch.packageConfig.configField("test2", "sample")
  203. /**
  204. * 如果你不使用zipArtifact或者path,我们只是使用7za来试试
  205. */
  206. sevenZip {
  207. /**
  208. * 可选,默认'7za'
  209. * 7zip工件路径,它将使用正确的7za与您的平台
  210. */
  211. zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
  212. /**
  213. * 可选,默认'7za'
  214. * 你可以自己指定7za路径,它将覆盖zipArtifact值
  215. */
  216. // path = "/usr/local/bin/7za"
  217. }
  218. }
  219. List<String> flavors = new ArrayList<>();
  220. project.android.productFlavors.each { flavor ->
  221. flavors.add(flavor.name)
  222. }
  223. boolean hasFlavors = flavors.size() > 0
  224. def date = new Date().format("MMdd-HH-mm-ss")
  225. /**
  226. * bak apk and mapping
  227. */
  228. android.applicationVariants.all { variant ->
  229. /**
  230. * task type, you want to bak
  231. */
  232. def taskName = variant.name
  233. tasks.all {
  234. if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
  235. it.doLast {
  236. copy {
  237. def fileNamePrefix = "${project.name}-${variant.baseName}"
  238. def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
  239. def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
  240. if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
  241. def packageAndroidArtifact = variant.packageApplicationProvider.get()
  242. if (packageAndroidArtifact != null) {
  243. try {
  244. from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
  245. } catch (Exception e) {
  246. from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
  247. }
  248. } else {
  249. from variant.outputs.first().mainOutputFile.outputFile
  250. }
  251. } else {
  252. from variant.outputs.first().outputFile
  253. }
  254. into destPath
  255. rename { String fileName ->
  256. fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
  257. }
  258. from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
  259. into destPath
  260. rename { String fileName ->
  261. fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
  262. }
  263. from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
  264. from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
  265. from "${buildDir}/intermediates/runtime_symbol_list/${variant.dirName}/R.txt"
  266. into destPath
  267. rename { String fileName ->
  268. fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
  269. }
  270. }
  271. }
  272. }
  273. }
  274. }
  275. project.afterEvaluate {
  276. //sample use for build all flavor for one time
  277. if (hasFlavors) {
  278. task(tinkerPatchAllFlavorRelease) {
  279. group = 'tinker'
  280. def originOldPath = getTinkerBuildFlavorDirectory()
  281. for (String flavor : flavors) {
  282. def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
  283. dependsOn tinkerTask
  284. def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
  285. preAssembleTask.doFirst {
  286. String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
  287. project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
  288. project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
  289. project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
  290. }
  291. }
  292. }
  293. task(tinkerPatchAllFlavorDebug) {
  294. group = 'tinker'
  295. def originOldPath = getTinkerBuildFlavorDirectory()
  296. for (String flavor : flavors) {
  297. def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
  298. dependsOn tinkerTask
  299. def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
  300. preAssembleTask.doFirst {
  301. String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
  302. project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
  303. project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
  304. project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
  305. }
  306. }
  307. }
  308. }
  309. }
  310. }

一定要通过 gradle的命令打基础包
image.png
在bakApk生成三个文件:

  1. apk 文件就是基准包
  2. mapping.txt 是开启混淆后,混淆的映射文件
  3. R.txt 是资源的映射文件

image.png

  1. 生成差异apk文件的Gradle脚本

修改tinker执行脚本

  1. tinkerOldApkPath = "${bakPath}/app-release-0906-17-18-35.apk"
  2. tinkerApplyMappingPath = "${bakPath}/app-release-0906-18-18-35-mapping.txt"
  3. tinkerApplyResourcePath = "${bakPath}/app-release-0906-18-18-35-R.txt"

执行tinkerPatchRelease/tinkerPatchDebug一个是打debug环境的patch,一个是打release环境的patch
image.png
生成如下的patch包:一般都是用patch_signed_7zip.apk,将这个文件上传服务器即可
image.png

Tinker的原理

  • 服务端:补丁包管理
  • 用户端:执行热修复
  • 开发端:生成补丁包

热修复要解决的问题:

  1. 补丁包是什么
  2. 如何生成补丁包?
  3. 开启混淆后会有什么影响?
  4. 手动生成的补丁包对比自动生成补丁包(Gradle自动生成的补丁包)
  5. 什么时候执行热修复?
  6. 怎么执行热修复使用补丁包?
  7. Android版本兼容的问题

    ClassLoader的机制

    双亲委托机制:某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己加载。

    1. 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次
    2. 安全性考虑,防止核心库的API被随意篡改

image.png
常用的ClassLoader的类图:
image.png

PathClassLoader :加载App应用程序的类。 DexClassLoader: 没有安装到app中的代码,一般通过DexClassLoader去加载外部的类,加载外部的dex文件或者apk文件。(DexClassLoader如果发现是apk文件就会解压,解压之后就会把dex文件都会加载一遍)

例如:java.lang.String.class 系统的类,假如在插件包dex中进行加载,首先会通过:
检测父类有没有加载而不是检测DexClassLoader有没有加载:

DexClassLoader -> BaseDexClassLoader -> ClassLoader (先看ClassLoader有没有加载,肯定是已经加载的String是系统的类,不会再次重新加载)

ClassLoader加载过了,将结果返回ClassLoader -> BaseDexClassLoader -> DexClassLoader

再比如:MyClass.class 在内存中没有被加载的,丢给DexClassLoader,而后检测BaseDexClassLoader有没有被加载,没有检测ClassLoader有没有加载,如果都没有丢给BootClassLoader进行加载,加载完之后在依次返回给DexClassLoader.

热修复就是基于:pathList:DexPathList 不同版本的源码有 makeXXElements 进行反射,如下dex的加载流程

image.png

热修复的流程

  1. 获取到当前应用的PathClassLoader
  2. 反射获取到DexPathList属性对象pathList
  3. 反射修改pathList的dexElements数组
    1. 把补丁包patch.dex转化为Element[] (patch)
    2. 获得pathListdexElements属性(old)
    3. patch + dexElements合并,并反射赋值给pathListdexElements,这样patch.dex就会在dexElements的第一个,首先会先加载Key.class,如果遇到旧的Class2.dex中的Key.class就不会在加载了,这样新的class就替换了旧的class。

image.png