Hot fix for Tinker access issues and fixes for 2022

background:

Because the project needs to access the hot fix function, Tencent Bugly was chosen at the beginning. After all, there is a user base as large as WeChat for endorsement, and a version management platform is provided to facilitate version maintenance. With the help of official documents and Github's Issues, it took a lot of effort and finally the access was successful, and the benchmark package installation and upload patch package were successfully repaired.

        I was very happy at first, but...we use the arctic fox version of AndroidStudio, and the Gradle and AGP used by the project are both version 7.0. At that time, the official version of Tinker did not support AGP7.0. Fortunately, after waiting for a week, WeChat updated version v1.9.14.19 in January 2022  to support AGP7.0 and R8. However, the last update of Bugly was in the first half of 2021, and the update support is no longer known. So I switched to only access WeChat Tinker, and this article only describes the problems and solutions encountered in this access. Version management and maintenance on the server will not be processed for now.

Development environment:

  • Window10-64 bit (Because it is also developed with Mac M1, the solution to the problem also involves the M1 chip.)
  • AndroidStudio BumbleBee build February 2,2022
  • Gradle Version 7.2
  • AGP Version 7.1.0
  • The project compileSdk and targetSdk are both 31 (involved there is no permission problem to load the patch package)

Formal access:

1. SDK access:

(1) Plug-in dependencies, add the following content to the project's build.gradle file:

buildscript {
    dependencies {
        classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:1.9.14.19")
    }
}

⚠️Note:

(2) build.gradle in the app directory introduces the following dependencies:

    //dex分包
    implementation "androidx.multidex:multidex:2.0.1"
    //可选,用于生成application类
    annotationProcessor('com.tencent.tinker:tinker-android-anno:1.9.14.19')
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.14.19')
    //tinker的核心库
    implementation('com.tencent.tinker:tinker-android-lib:1.9.14.19')

⚠️ Note: The dependent version is usually the same as the plug-in version in the first step. For the specific version, please check the link dependent version .

(3) Introduce the plug-in:

apply plugin: 'com.tencent.tinker.patch'

(4) Add the following properties to the gradle.properties file of the project:

TINKER_ID = 1.0

⚠️Note: The tinkerId is generally the version number of each release, and the patch package is matched with the benchmark package according to the tinkerId. This property will then be referenced in the app's build.gradle. 

(5) Copy the file [tinker_multidexkeep.pro] in the official demo to the app directory of your own project.

⚠️Note: There is a place in this file that needs to be modified to your own content. You need to replace this file with the application you declared in the class inherited from "DefaultApplicationLike". as follows:

(6) In the official demo, all packaged configurations are written in the build.gradle under the app. Here, these contents are separated and the tinker.gradle file is created under the app directory. And introduce in build.gradle under app:

// 引入tinker配置
apply from: "tinker.gradle"

Because some of the gradle syntax attributes in the official demo have been discarded, the specific content of the [tinker.gradle] file is as follows:

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

/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //打补丁包时候这里配置基准包的全称
    tinkerOldApkPath = "${bakPath}/app-debug-0211-11-33-02.apk"
    //打Release补丁包时候这里配置基准包时候生成的mapping文件proguard mapping file to build //patch apk
    tinkerApplyMappingPath = "${bakPath}/"
    //打补丁包时候这里配置基准包时候生成的文件全称resource R.txt to build patch apk, must input //if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0211-11-33-02-R.txt"

    //多渠道包时候的输出目录only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

def gitSha() {
    try {
        String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
    } catch (Exception e) {
        throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
    }
}

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

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

    tinkerPatch {
        /**
         * necessary,default 'null'
         * the old apk path, use to diff with the new apk to build
         * add apk from the build/bakApk
         */
        oldApk = getOldApkPath()
        /**
         * optional,default 'false'
         * there are some cases we may get some warnings
         * if ignoreWarning is true, we would just assert the patch process
         * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
         *         it must be crash when load.
         * case 2: newly added Android Component in AndroidManifest.xml,
         *         it must be crash when load.
         * case 3: loader classes in dex.loader{} are not keep in the main dex,
         *         it must be let tinker not work.
         * case 4: loader classes in dex.loader{} changes,
         *         loader classes is ues to load patch dex. it is useless to change them.
         *         it won't crash, but these changes can't effect. you may ignore it
         * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
         */
        ignoreWarning = false
        /**
         * 报错解决:Warning:ignoreWarning is false,
         * but we found loader classes are found in old secondary dex
         */
        allowLoaderInAnyDex = true
        removeLoaderForAllDex = true
        /**
         * optional,default 'true'
         * whether sign the patch file
         * if not, you must do yourself. otherwise it can't check success during the patch loading
         * we will use the sign config with your build type
         */
        useSign = true

        /**
         * optional,default 'true'
         * whether use tinker to build
         */
        tinkerEnable = buildWithTinker()

        /**
         * Warning, applyMapping will affect the normal android build!
         */
        buildConfig {
            /**
             * optional,default 'null'
             * if we use tinkerPatch to build the patch apk, you'd better to apply the old
             * apk mapping file if minifyEnabled is enable!
             * Warning:
             * you must be careful that it will affect the normal assemble build!
             */
            applyMapping = getApplyMappingPath()
            /**
             * optional,default 'null'
             * It is nice to keep the resource id from R.txt file to reduce java changes
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * necessary,default 'null'
             * because we don't want to check the base apk with md5 in the runtime(it is slow)
             * tinkerId is use to identify the unique base apk when the patch is tried to apply.
             * we can use git rev, svn rev or simply versionCode.
             * we will gen the tinkerId in your manifest automatic
             */
            tinkerId = getTinkerIdValue()

            /**
             * if keepDexApply is true, class in which dex refer to the old apk.
             * open this can reduce the dex diff file size.
             */
            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 {
            /**
             * optional,default 'jar'
             * only can be 'raw' or 'jar'. for raw, we would keep its original format
             * for jar, we would repack dexes with zip format.
             * if you want to support below 14, you must use jar
             * or you want to save rom or check quicker, you can use raw mode also
             */
            dexMode = "jar"

            /**
             * necessary,default '[]'
             * what dexes in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * necessary,default '[]'
             * Warning, it is very very important, loader classes can't change with patch.
             * thus, they will be removed from patch dexes.
             * you must put the following class into main dex.
             * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             * optional,default '[]'
             * what library in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * for library in assets, we would just recover them in the patch directory
             * you can get them in TinkerLoadResult with Tinker
             */
            pattern = ["lib/*/*.so"]
        }

        res {
            /**
             * optional,default '[]'
             * what resource in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * you must include all your resources in apk here,
             * otherwise, they won't repack in the new apk resources.
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * the resource file exclude patterns, ignore add, delete or modify resource change
             * it support * or ? pattern.
             * Warning, we can only use for files no relative with resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * default 100kb
             * for modify resource, if it is larger than 'largeModSize'
             * we would like to use bsdiff algorithm to reduce patch file size
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
             * package meta file gen. path is assets/package_meta.txt in patch file
             * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
             * or TinkerLoadResult.getPackageConfigByName
             * we will get the TINKER_ID from the old apk manifest for you automatic,
             * other config files (such as patchMessage below)is not necessary
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             * just a sample case, you can use such as sdkVersion, brand, channel...
             * you can parse it in the SamplePatchListener.
             * Then you can use patch conditional!
             */
            configField("platform", "all")
            /**
             * patch version via packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //or you can add config filed outside, or get meta value from old apk
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * if you don't use zipArtifact or path, we just use 7za to try
         */
        sevenZip {
            /**
             * optional,default '7za'
             * the 7zip artifact path, it will use the right 7za with your platform
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.2.17"
            /**
             * 设置path之后会覆盖zipArtifact的配置,使用本地
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
            path = "/Users/pary/.gradle/caches/modules-2/files-2.1/com.tencent.mm/SevenZip/1.2.17"
        }
    }

    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().variantOutput.outputFileName.get())
                                } catch (Exception e) {
                                    from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().variantOutput.outputFileName.get())
                                }
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            from variant.outputs.first().outputFile
                        }

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

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

                        from "${buildDir}/intermediates/symbols/${dirName}/R.txt"
                        from "${buildDir}/intermediates/symbol_list/${dirName}/R.txt"
                        from "${buildDir}/intermediates/runtime_symbol_list/${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"
                    }

                }
            }
        }
    }
}

⚠️Note: In the above file, compared with the official demo, three contents have been modified, because if you don’t modify it, the package will report an error. The specific content of the modification is listed below:

  • Add the following two attribute configurations in tinkerPatch{}, if not added, an error message will be reported: Warning:ignoreWarning is false, but we found loader classes are found in old secondary dex.

    allowLoaderInAnyDex = true
    removeLoaderForAllDex = true
  •  The attributes in the official demo are modified in it.doLast{}:
    //官方demo报错的属性方法:
    variant.outputs.first().apkData.outputFileName
    //可以正常使用的方法:
    variant.outputs.first().variantOutput.outputFileName.get()
  • The SevenZip configuration used when patching the package configured in sevenZip{}:
    //官方demo的原来配置如下:
    zipArtifact = "com.tencent.mm:SevenZip:1.1.10"

    In this way, patching the package will report the following error:

    This is because the version 1.1.10 no longer exists. For the latest version, you can view the latest version under the following link: Version link  . After replacing it with the latest version 1.2.17, the following error is reported:

    Because SevenZip will select the corresponding exe executable file according to the system, but  there is no executable program corresponding to the system in the version directory link in this directory, so an error is reported. Since there is no corresponding exe file remotely, we can use the local path.

  • Enter "open .gradle" on the Mac console to open the local gradle path and find [com.tencent.mm], check whether there is a SevenZip folder and a specific version folder under its path, as shown in the figure:

  • Windows system can find the corresponding folder under [system disk/user/account name/.gradle]

If so, you can configure the following path in sevenZip{} of tinker.gradle, which will override the online configuration of zipArtifact :

path = "/Users/pary/.gradle/caches/modules-2/files-2.1/com.tencent.mm/SevenZip/1.2.17"

At this time, if the patch package is applied according to the benchmark package, it will be successfully built, but there will still be an error message output, as follows:

 Because there is no corresponding SevenZip program under the local path set above, so 7za compresses and packs the apk error, and the patch_signed.apk file is not generated in the corresponding directory. But at this time there is already a patch package, in this directory:

This apk is also the generated patch package. 7za mainly compresses the size of the generated patch package to reduce the volume, but if 7za accidentally reverse-compresses it, Tinker will also prompt you to use a smaller apk file.

(7) Modify the build.gradle content under the app, the content that needs to be added is as follows:

  • Add the following to android{}

    // tinker支持大工程文件配置
    dexOptions {
            jumboMode = true
    }
  • Add the following content in defaultConfig{}
            multiDexEnabled true
            multiDexKeepProguard file("tinker_multidexkeep.pro")
            buildConfigField "String", "MESSAGE", "\"I am the base apk\""
            buildConfigField "String", "TINKER_ID", "\"${TINKER_ID}\""
            buildConfigField "String", "PLATFORM", "\"all\""
     

2. Code access:

(1) Copy the following files from the official demo to your own project:

  • Crash\reporter\service All files in the three folders, and introduce SampleResultService in the manifest file

    <service
                android:name=".service.SampleResultService"
                android:permission="android.permission.BIND_JOB_SERVICE"
                android:exported="false"/>

  • The BuildInfo class inside the app package.
  • Utils and TinkerManager are two classes inside the util package.

(2) Custom Application proxy class:    

  • Create a class that inherits from DefaultApplicationLike, as follows:
import android.app.Application;
import android.content.Context;
import android.content.Intent;

import androidx.multidex.MultiDex;

import com.tencent.tinker.anno.DefaultLifeCycle;
import com.tencent.tinker.entry.DefaultApplicationLike;
import com.tencent.tinker.lib.tinker.Tinker;
import com.tencent.tinker.loader.shareutil.ShareConstants;

//注解关联MyApplicaiton:这里的applicaiton不能为MyApplication,否则会报已存在类。
@DefaultLifeCycle(application = "com.example.tinkerwei.Application",
        flags = ShareConstants.TINKER_ENABLE_ALL)
public class SampleApplicationLike extends DefaultApplicationLike {

    public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 可以在这里初始化SDK
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        MultiDex.install(base);
        TinkerManager.setTinkerApplicationLike(this);
        TinkerManager.setUpgradeRetryEnable(true);
        TinkerManager.installTinker(this);
        Tinker.with(getApplication());
    }
}
  • Create a class that inherits from TinkerApplication, as follows:
import com.tencent.tinker.loader.app.TinkerApplication;
import com.tencent.tinker.loader.shareutil.ShareConstants;

public class MyApplication  extends TinkerApplication {

    public MyApplication() {
        //注意第二个参数为自定义的SampleApplicationLike的路径
        super(ShareConstants.TINKER_ENABLE_ALL,
                "com.example.tinkerwei.app.SampleApplicationLike",
                "com.tencent.tinker.loader.TinkerLoader",
                false, false);
    }
}
  • Create a class that inherits from TinkerApplication, as follows:

3. Write the code to load the patch package: 

(1) Check the read and write file permissions of the application, if there is no permission to request permission. Example:

    // 请求读文件权限
    private void requestPermission() {
        if (!checkPermission()) {
            ActivityCompat.requestPermissions(this,
                    new String[] {Manifest.permission.READ_EXTERNAL_STORAGE, 
                            Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
        }
    }

    // 检查是否有读文件权限
    private boolean checkPermission() {
        final int res = ContextCompat.checkSelfPermission(this.getApplicationContext(),
                Manifest.permission.WRITE_EXTERNAL_STORAGE);
        return res == PackageManager.PERMISSION_GRANTED;
}

⚠️Note: If the following error is reported when loading the patch (mainly for Android 10+ version):

open failed:EACCES(Permission denied)

You need to add the following attributes under the application tag in the manifest file:

android:requestLegacyExternalStorage="true"

(2) Click the button to load the local patch package code, as follows:

findViewById(R.id.btnLoad).setOnClickListener(v -> {

            String pathRootPatchApk = Environment.getExternalStorageDirectory() +
                    File.separator + "app-debug-patch_signed.apk";
            File fileApk = new File(pathRootPatchApk);
            Log.i("Tinker", "补丁文件路径:" + pathRootPatchApk);
            Log.i("Tinker", "补丁文件是否存在:" + fileApk.exists());
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), pathRootPatchApk);
        });

⚠️ Note: Because this is the apk patch file in the root directory of the SDK to be read, so for Android 10+ versions, there will be an error that the open file does not have permission in the previous step. 

Four, Gradle packaging: 

(1) Create a benchmark package, as shown in the figure:

After the packaging is complete, the following content will be generated in the build folder: 

The content in the bakApk package needs to be configured in tinker.gradle before patching, as shown in the figure below: 

The red and blue boxes are where the content of the bakApk folder needs to be replaced, and the blue box is where the mapping file will be replaced after enabling obfuscation.

The green box is the output file directory of the multi-channel package.

(2) Apply the bug fix patch package, before applying the patch package, you can code to set the print content or display a control.

After packaging, the corresponding patch package will be generated under the app/build folder. The specific path is as follows: 

 Because I typed the debug package, the patch package app-debug-patch_signed.apk under the debug folder is generated here.

Then copy this patch package to the location corresponding to the path defined when writing and loading the patch package code on the phone.

If the patch is loaded successfully, the following message will appear:

If successful, the tinker sdk on the application will also toast prompt to restart the application process to run the latest application. (The red error in the above picture is because the patch package will be automatically deleted after the patch is loaded successfully. Here, the deletion is not successful, so the error report does not affect the successful application of the patch.) 

Guess you like

Origin blog.csdn.net/nsacer/article/details/122868334