热修复 | Tinker 的实现原理

4,455 阅读10分钟

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

热补丁方案的比较

TinkerQZoneAndFix(阿里)Robust(美团)
类替换yesyesnono
So替换yesnonono
资源替换yesyesnono
全平台支持yesyesyesyes
即时生效nonoyesyes
性能损耗较小较大较小较小
补丁包大小较小较大一般一般
开发透明yesyesnono
复杂度较低较低复杂复杂
gradle支持yesnonono
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的使用

  1. 引入插件依赖
dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") 		{
            changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
            exclude group: 'com.android.tools.build', module: 'gradle'
        }
    }
configurations.all {
        it.resolutionStrategy.cacheDynamicVersionsFor(5, 'minutes')
        it.resolutionStrategy.cacheChangingModulesFor(0, 'seconds')
    }

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

TINKER_VERSION=1.9.14.17
TINKER_ID=1063
#是否开启tinker
TINKER_ENABLE=true
#是否是gradle 3.x的版本
GRADLE_3=true
  1. 引入tinker核心库
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }

    // Maven local cannot handle transist dependencies.
    implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }

    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno-support:${TINKER_VERSION}") { changing = true }
    //多分包依赖
    implementation "androidx.multidex:multidex:2.0.1"

defaultConfig 配置

    defaultConfig {
        ......
        /**
         * you can use multiDex and install it in your ApplicationLifeCycle implement
         * 开启多分包
         */
        multiDexEnabled true
        //tinker混淆情况处理
        multiDexKeepProguard file("tinker_multidexkeep.pro")
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         * 配置patch信息
         */
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
        /**
         * client version would update with patch
         * so we can get the newly git version easily!
         * 配置TINKER_ID
         */
        buildConfigField "String", "TINKER_ID", "\"${TINKER_ID}\""
        /**
        * 支持全平台
        */
        buildConfigField "String", "PLATFORM", "\"all\""
    }
    //recommend 开启jumboMode
    dexOptions {
        jumboMode = true
    }
  1. 创建ApplicationLike代理类
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",//自动帮助生成SampleApplication类这个类就是Application
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
    private static final String TAG = "Tinker.SampleApplicationLike";

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }
    
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        SampleApplicationContext.application = getApplication();
        SampleApplicationContext.context = getApplication();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }
}
  1. 创建Tinker管理类TinkerManager,在Tinker的Demo中提供了这个类,直接拿过来即可 在这里
  2. ApplicationLike中的onBaseContextAttached执行TinkerManager.installedTinker(this)
 		TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //optional set logIml, or you can use default debug log
        TinkerInstaller.setLogIml(new MyLogImp());

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);
        Tinker tinker = Tinker.with(getApplication());
  1. 配置Application,编译项目会得到一个自动生成的SampleApplication的class
public class SampleApplication extends TinkerApplication {

    public SampleApplication() {
        super(15, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false, false);
    }

}

然后设置到manifest中

    <application
        android:name=".app.SampleApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
    ......        
  1. 生成基础apk包

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

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

def bakPath = file("${buildDir}/bakApk/")

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //基准apk路径
    tinkerOldApkPath = "${bakPath}/app-debug-0830-17-11-57.apk"
    //未开启混淆,则不需要填写
    tinkerApplyMappingPath = "${bakPath}/"
    //基准apk中的R文件路径
    tinkerApplyResourcePath = "${bakPath}/app-debug-0830-17-11-57-R.txt"
    //如果你修复了res文件,需要指定你bug版本的R.txt文件
    tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0830-17-11-57-R.txt"

}

//获取旧版本路径
def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

//获取mapping文件路径
def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

//获取资源mapping路径
def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

//Tinker是否可用,是否
def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

//获取tinker差分包目录
def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}



if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /**
         * 默认为null
         * 将旧的apk和新的apk建立关联
         * 从build / bakApk添加apk
         */
        oldApk = getOldApkPath()
        /**
         * 可选,默认'false'
         *有些情况下我们可能会收到一些警告
         *如果ignoreWarning为true,我们只是断言补丁过程
         * case 1:minSdkVersion低于14,但是你使用dexMode与raw。
         * case 2:在AndroidManifest.xml中新添加Android组件,
         * case 3:装载器类在dex.loader {}不保留在主要的dex,
         *          它必须让tinker不工作。
         * case 4:在dex.loader {}中的loader类改变,
         *          加载器类是加载补丁dex。改变它们是没有用的。
         *          它不会崩溃,但这些更改不会影响。你可以忽略它
         * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
         */
        ignoreWarning = true

        /**
         * 可选,默认为“true”
         * 是否签名补丁文件
         * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
         * 我们将使用sign配置与您的构建类型
         */
        useSign = true

        /**
         * 可选,默认为“true”
         * 是否使用tinker构建
         */
        tinkerEnable = buildWithTinker()

        /**
         * 警告,applyMapping会影响正常的android build!
         */
        buildConfig {
            /**
             * 可选,默认为'null'
             * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
             * apk映射文件如果minifyEnabled是启用!
             * 警告:你必须小心,它会影响正常的组装构建!
             */
            applyMapping = getApplyMappingPath()
            /**
             * 可选,默认为'null'
             * 最好保留R.txt文件中的资源id,以减少java更改
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * 必需,默认'null'
             * 因为我们不想检查基地apk与md5在运行时(它是慢)
             * tinkerId用于在试图应用补丁时标识唯一的基本apk。
             * 我们可以使用git rev,svn rev或者简单的versionCode。
             * 我们将在您的清单中自动生成tinkerId
             */
            tinkerId = TINKER_ID.toInteger()

            /**
             * 如果keepDexApply为true,则表示dex指向旧apk的类。
             * 打开这可以减少dex diff文件大小。
             */
            keepDexApply = false

            /**
             * optional, default 'false'
             * Whether tinker should treat the base apk as the one being protected by app
             * protection tools.
             * If this attribute is true, the generated patch package will contain a
             * dex including all changed classes instead of any dexdiff patch-info files.
             */
            isProtectedApp = false

            /**
             * optional, default 'false'
             * Whether tinker should support component hotplug (add new component dynamically).
             * If this attribute is true, the component added in new apk will be available after
             * patch is successfully loaded. Otherwise an error would be announced when generating patch
             * on compile-time.
             *
             * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
             */
            supportHotplugComponent = false
        }

        dex {
            /**
             * 可选,默认'jar'
             * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
             * 对于jar,我们将使用zip格式重新包装dexes。
             * 如果你想支持下面14,你必须使用jar
             * 或者你想保存rom或检查更快,你也可以使用原始模式
             */
            dexMode = "jar"

            /**
             * 必需,默认'[]'
             * apk中的dexes应该处理tinkerPatch
             * 它支持*或?模式。
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * 必需,默认'[]'
             * 警告,这是非常非常重要的,加载类不能随补丁改变。
             * 因此,它们将从补丁程序中删除。
             * 你必须把下面的类放到主要的dex。
             * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
             * 自己的tinkerLoader,和你使用的类
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             * 可选,默认'[]'
             * apk中的图书馆应该处理tinkerPatch
             * 它支持*或?模式。
             * 对于资源库,我们只是在补丁目录中恢复它们
             * 你可以得到他们在TinkerLoadResult与Tinker
             */
            pattern = ["lib/*/*.so"]
        }

        res {
            /**
             * 可选,默认'[]'
             * apk中的什么资源应该处理tinkerPatch
             * 它支持*或?模式。
             * 你必须包括你在这里的所有资源,
             * 否则,他们不会重新包装在新的apk资源。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             *  可选,默认'[]'
             *  资源文件排除模式,忽略添加,删除或修改资源更改
             * *它支持*或?模式。
             * *警告,我们只能使用文件没有relative与resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             *  默认100kb
             * *对于修改资源,如果它大于'largeModSize'
             * *我们想使用bsdiff算法来减少补丁文件的大小
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
             * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
             * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
             * 或TinkerLoadResult.getPackageConfigByName
             * 我们将从旧的apk清单为您自动获取TINKER_ID,
             * 其他配置文件(如下面的patchMessage)不是必需的
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             *只是一个例子,你可以使用如sdkVersion,品牌,渠道...
             * 你可以在SamplePatchListener中解析它。
             * 然后你可以使用补丁条件!
             */
            configField("platform", "all")
            /**
             * 补丁版本通过packageConfig
             */
            configField("patchVersion", "1.0.2")
        }
        //或者您可以添加外部的配置文件,或从旧apk获取元值
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * 如果你不使用zipArtifact或者path,我们只是使用7za来试试
         */
        sevenZip {
            /**
             * 可选,默认'7za'
             * 7zip工件路径,它将使用正确的7za与您的平台
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 可选,默认'7za'
             * 你可以自己指定7za路径,它将覆盖zipArtifact值
             */
//        path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath

                        if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
                            def packageAndroidArtifact = variant.packageApplicationProvider.get()
                            if (packageAndroidArtifact != null) {
                                try {
                                    from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
                                } catch (Exception e) {
                                    from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
                                }
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            from variant.outputs.first().outputFile
                        }

                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/runtime_symbol_list/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }

                }
            }
        }
    }
}

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

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

image.png

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

修改tinker执行脚本

    tinkerOldApkPath = "${bakPath}/app-release-0906-17-18-35.apk"
    tinkerApplyMappingPath = "${bakPath}/app-release-0906-18-18-35-mapping.txt"
    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