Android Tinker热修复 一

公司项目过去一年里面出现几次严重的bug,导致APP Crash,不得已特定的机器只能交由售后处理,所以热修复计划被提上了日程。

现在还在维护的热修复框架还有三个,一个是微信的Thinker,微信好几亿的用户使用,还是很牛逼的!还有一个是阿里的Sophix,但是这个没开源还收费,不考虑。还有一个美团的Robust,大致看了下,修复方法的时候需要加注解,不修复时候就删掉,感觉有点麻烦,先都试试再决定用哪个!

Tinker
可以看下官方接入文档:Tinker-接入指南

还有官方的例子Demo

接入很简单,按照接入指南和官方给的例子添加依赖:

 //optional, help to generate the final application
    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 }

    implementation "androidx.multidex:multidex:2.0.1"

将例子中的gradle中打包文件全部copy过来:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"


    compileOptions {
        sourceCompatibility getJavaVersion()
        targetCompatibility getJavaVersion()
    }

    // Tinker推荐模式
    dexOptions {
        jumboMode = true
    }

    // 关闭aapt对png优化
    aaptOptions {
        cruncherEnabled false
    }

    signingConfigs {
        try {
            config {
                keyAlias 'testres'
                keyPassword 'testres'
                storeFile file('./keystore/release.keystore')
                storePassword 'testres'
            }
        } catch (ex) {
            throw new InvalidUserDataException(ex.toString())
        }
    }

    defaultConfig {
        applicationId "com.ums.hotfix_tinker"
        minSdkVersion 18
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        signingConfig signingConfigs.config
        /**
         * you can use multiDex and install it in your ApplicationLifeCycle implement
         */
        multiDexEnabled true
        multiDexKeepProguard file("tinker_multidexkeep.pro")
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
//        buildConfigField "String", "MESSAGE", "\"I am the patch apk\""
        /**
         * client version would update with patch
         * so we can get the newly git version easily!
         */
        buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
        buildConfigField "String", "PLATFORM", "\"all\""

        manifestPlaceholders = [TINKER_ID: "${getTinkerIdValue()}"]

        flavorDimensions "comm"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    //用于多渠道打包
    productFlavors{
        dev{
            buildConfigField 'boolean', 'isProduct', 'false'
            dimension 'comm'
        }
        pro{
            buildConfigField 'boolean', 'isProduct', 'true'
            dimension  "comm"
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    //optional, help to generate the final application
    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 }

    implementation "androidx.multidex:multidex:2.0.1"
}

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'")
    }
}
// 指定JDK版本
def getJavaVersion() {
    return JavaVersion.VERSION_1_7
}
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
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-20200323-14-35-40.apk"
    //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-release-20200323-14-35-40-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/"
}


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()
//}
//
// 用来获取TinkerId(当前版本号就是TinkerId)
def getTinkerIdValue() {
    return android.defaultConfig.versionName
}


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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    // Tinker插件
    apply plugin: 'com.tencent.tinker.patch'
    /**
     * 全局信息相关配置
     */
    tinkerPatch {
        // 基准apk包的路径,必须输入,否则会报错。
        oldApk = getOldApkPath()
        /**
         * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。
         * 因为这些情况可能会导致编译出来的patch包带来风险:
         * case 1: minSdkVersion小于14,但是dexMode的值为"raw";
         * case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
         * case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
         * case 4:  定义在dex.loader用于加载补丁的类出现修改;
         * case 5: resources.arsc改变,但没有使用applyResourceMapping编译。
         */
        ignoreWarning = false

        /**
         * 运行过程中需要验证基准apk包与补丁包的签名是否一致,是否需要签名。
         */
        useSign = true

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

        /**
         * 编译相关的配置项
         */
        buildConfig {
            /**
             * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。
             * 这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
             */
            applyMapping = getApplyMappingPath()
            /**
             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配。
             * 这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
             * 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
             */
            tinkerId = getTinkerIdValue()

            /**
             * 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
             * 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
             */
            keepDexApply = false
        }
        /**
         * dex相关的配置项
         */
        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 对于'raw'模式,我们将会保持输入dex的格式。
             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
             * 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。
             * 默认我们并不会去校验md5,一般情况下选择jar模式即可。
             */
            dexMode = "jar"

            /**
             * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。
             * 这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
             * 这里需要定义的类有:
             * 1. 你自己定义的Application类;
             * 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
             * 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
             * 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。
             *    这里需要注意的是,这些类的直接引用类也需要加入到loader中。
             *    或者你需要将这个类变成非preverify。
             * 5. 使用1.7.6版本之后版本,参数1、2会自动填写。
             *
             */
            loader = [
                    // Tinker库中用于加载补丁包的部分类
                    "com.tencent.tinker.loader.*",
                    // 自己定义的Application类;
                    "com.ums.hotfix_tinker.App",
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "com.ums.hotfix_tinker.info.BaseBuildInfo"
            ]
        }
        /**
         * lib相关的配置项
         */
        lib {
            /**
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }
        /**
         * res相关的配置项
         */
        res {
            /**
             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...,
             * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
             * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
             * 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
             */
            largeModSize = 100
        }
        /**
         * 用于生成补丁包中的'package_meta.txt'文件
         */
        packageConfig {
            /**
             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
             * 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
             * 但是建议直接通过修改代码来实现,例如BuildConfig。
             */
            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补丁版本
             */
            configField("patchVersion", "1.0.0")
        }
        /**
         * 7zip路径配置项,执行前提是useSign为true
         */
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。
             */
            // path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyyMMdd-HH-mm-ss")

        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"
                    }

                }
            }
        }
    }
}



task sortPublicTxt() {
    doLast {
        File originalFile = project.file("public.txt")
        File sortedFile = project.file("public_sort.txt")
        List<String> sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")
        }
    }
}


还有
在这里插入图片描述
当然还有其中的一些类文件
在这里插入图片描述
都需要copy过来,当然如果不想搞的话,可以自己下载下来我捡的demo。

其中需要注意的还是application,Tinker建议不要自己写Application,用代码在编译期自动生成:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".App",
                  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);
    }


    public static int flat = 1;

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @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);


        int i = 0;
        i++;
        SampleApplicationContext.application = getApplication();
        SampleApplicationContext.context = getApplication();
        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());
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
    }
}

@DefaultLifeCycle这个生成Application的注解,如果不想自动生成,也可以自己搞,但是不建议,因为如果打包的时候,自己搞的application有改动的话,就生成不了补丁了,自己搞的话,也简单:

import com.tencent.tinker.loader.app.TinkerApplication;

public class MyApp extends TinkerApplication {


    public MyApp() {
        super(15, "com.ums.hotfix_tinker.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }
}

继承自TinkerApplication ,实现一个构造方法,方法里面实现反射调用SampleApplicationLike即可,当然需要把SampleApplicationLike这个类上面的注解删除掉。
这样基本就可以,剩下的就是打包,生成补丁了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟红尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值