Bugly热修复 - 接入篇

作者: CapPaw | 来源:发表于2017-02-08 14:47 被阅读1047次

    微信Tinker_从1.7.0开源开始,本人就已经密切关注它的发展,并在1.7.0就已经成功接入并运行(未在生产环境使用),现在Tinker已经更新到1.7.7了。在微信Tinker之前,笔者曾经在阿里andfix的泥潭挣扎许久,现在已放弃。

    注: 本文需要读者需要拥有Tinker接入经验

    Bugly Tinker multidex packer 自定义task


    关于Bugly的热修复

    腾讯Bugly的使用初衷是在线关注APP的错误日志,在Tinker开源并基于稳定后,Bugly就搭上了Tinker的顺风车(虽然它们都是腾讯出品)。

    单独接入过Tinker的猿应该都知道,Tinker仅提供补丁功能,并不支持管理功能,如果决定使用Tinker补丁功能,就必须自己实现后台补丁管理。一般公司,时间就是生命的环境下是不会有这个考虑的。然后就是后来,TinkerPatch平台应运而生,但是并没有解决上述问题,据我了解,TinkerPatch是一个个人项目(虽然是微信的人在维护),而且计划收费,这又是一般公司不能接受的。

    在目前没有项目计划,又需要跟上时代跟上技术的的尴尬大前提下,我选择了免费不需要自己搭建后台的Bugly热修复,并决定应用到生产环境中。

    开始接入

    使用过Bugly的猿都知道,com.tencent.bugly:crashreport_upgrade:x.y.z这个库提供了异常上传,版本更新及热修复功能(如果你不知道或没有接入,请到Bugly SDK【升级SDK包】查看)的功能。笔者就是从这个库接入的。

    前提

    <font color='red'>笔者项目通过Zip comment方式(Tinker力荐)打多渠道包,故接入细节涉及到packer的使用,接入及使用都按照笔者工程的需求来做的,这里仅提供一点思路。</font>

    工程配置

    1. packer配置

    工程下的build.gradle配置依赖 -> 引入packer插件

    classpath 'com.mcxiaoke.gradle:packer-ng:1.0.8'
    

    项目下的build.gradle配置依赖 -> 引入packer操作渠道的工具类

    compile 'com.mcxiaoke.gradle:packer-helper:1.0.8'
    

    项目下的build.gradle配置packer插件

    //混淆配置
    signingConfigs {
            release {
                keyAlias KEY_ALISA
                keyPassword KEY_PASSWORD
                storeFile KEY_STORE_FILE
                storePassword STORE_PASSWORD
                // 1. Gradle版本 >= 2.14.1
                // 2. Android Gradle Plugin 版本 >= 2.2.0
                // 作用是只使用旧版签名,禁用V2版签名模式
                v2SigningEnabled false
            }
        }
    
    
    //插件配置
    apply plugin: 'packer'
    packer {
        def date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date())
        checkSigningConfig = true
        checkZipAlign = true
        archiveOutput = file("archives")
        archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${fileMD5}-' + "${PRODUCT_TYPE}-" + date
    }
    

    在项目目录下新建/复制markets.txt文件,文件中保存了需要打包的渠道信息。如图:

    上述内容配置完成并完成gradle同步后,出现如下图的任务,则说明配置成功,执行任务,就可成功打出需要渠道的apk包。可通过PackerNg.getMarket(context)方式读取渠道类型。

    2. Bugly配置

    这一节基本看着Bugly的接入文档就可以完成,这里只做简单概述。

    项目中的build.gradle中引入依赖 -> 异常上报、升级、热修复

    compile "com.tencent.bugly:crashreport_upgrade:1.2.2"
    

    为了方便Bugly的管理,笔者将Bugly的配置放置到了bugly-support.grale文件中(如果你不懂gradle,请看这里)

    apply from: 'config.gradle'
    
    if (BUGLY_ENABLE) {
        apply plugin: 'bugly'
        bugly {
            appId = BUGLY_APPID
            appKey = BUGLY_APPKEY
            execute = BUGLY_EXECUTE
            upload = BUGLY_UPLOAD
        }
    }
    

    上述内容配置完成并完成gradle同步后,出现如下图的任务,则说明配置成功,如果项目进行混淆、buglyupload=true,运行assembleRelease<font color='red'>及相关</font>任务,则生成的mapping.txt文件将自动上传至bugly后台。

    tinker支持配置

    在配置完成Bugly相关内容后,目前生效的功能仅为异常上报及升级,仍然不能支持热修复。下面将简述(Bugly热修复文档详情)Bugly热修复的配置.

    工程的build.gradle文件中引入依赖 -> tinker支持插件

    classpath "com.tencent.bugly:tinker-support:1.0.3"
    

    项目的build.gradle文件引入依赖 -> 支持dex分包

    compile "com.android.support:multidex:1.0.1"
    

    项目目录下,新建tinker-support.gradle脚本文件

    脚本内容介绍
    • 本地插件,定义一些基本数据及工具函数
    apply from: 'config.gradle'
    apply from: 'utils.gradle'
    
    • 目标文件存储目录
    def bakPath = file("archives")
    def appName = 'base'
    
    • tinker-patch插件配置
    apply plugin: 'com.tencent.bugly.tinker-support'
    
    tinkerSupport {
    
       // 开启tinker-support插件,默认值true
       enable = true
    
       // 指定归档目录,默认值当前module的子目录tinker
       autoBackupApkDir = "${bakPath}"
    
       // 是否启用覆盖tinkerPatch配置功能,默认值false
       // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
       overrideTinkerPatchConfiguration = true
    
       // 编译补丁包时,必需指定基线版本的apk,默认值为空
       // 如果为空,则表示不是进行补丁包的编译
       // @{link tinkerPatch.oldApk }
       baseApk = "${bakPath}/${appName}/app-release.apk"
    
       // 对应tinker插件applyMapping
       baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
    
       // 对应tinker插件applyResourceMapping
       baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
    
       // 唯一标识当前版本
       tinkerId = "${VERSION_NAME}"
    
       // 是否开启代理Application,设置之后无须改造Application,默认为false
       enableProxyApplication = true
    
    }
    
    • 自定义清理任务【packer、tinker生产文件】
    task tinkerClean() {
    
       def destArchives = file("${bakPath}")
       group 'tinker-ext'
       description "Delete files in ${destArchives} include ${destArchives} self."
    
       doFirst {
           delete(destArchives)
       }
    }
    
    • 自定义产出任务【packer渠道包及tinker基础包】
    def dest = file("${bakPath}/${appName}/")
    
    task tinkerPrepare() {
    
       dependsOn 'apkRelease'
       group 'tinker-ext'
       description 'Release only - Generate apk file include: packer,generate tinker bak reouserce fiels.'
    
       doLast {
           copy {
    
               if (!dest.exists()) {
                   dest.mkdirs()
               }
    
               from "${buildDir}/outputs/apk/app-release.apk"
               into dest
    
               def destApk = file("${dest}app-release.apk")
               if (destApk.exists()) {
                   println "Tinker bakApk file ${destApk.absolutePath} done."
               }
    
               from "${buildDir}/outputs/mapping/release/app-mapping.txt"
               into dest
               rename { String fileName ->
                   fileName.replace("app-mapping.txt", "app-release-mapping.txt")
               }
    
               def destMappingFile = file("${dest}app-release-mapping.txt")
               if (destMappingFile.exists()) {
                   println "Tinker mapping file ${destMappingFile.absolutePath} done."
               }
    
               from "${buildDir}/intermediates/symbols/release/R.txt"
               into "${bakPath}/${appName}/"
               rename { String fileName ->
                   fileName.replace("R.txt", "app-release-R.txt")
               }
    
               def destResourceFile = file("${dest}app-release-R.txt")
               if (destResourceFile.exists()) {
                   println "Tinker mapping file ${destResourceFile.absolutePath} done."
               }
    
               bakPath.listFiles(new FilenameFilter() {
                   @Override
                   boolean accept(File dir, String filename) {
                       return filename.startsWith("app-")
                   }
               }).each {
                   if (it.isDirectory()) {
                       it.listFiles().each {
                           it.delete()
                       }
                       it.delete()
                   }
               }
    
               println 'Tinker prepare done.'
           }
       }
    }
    
    • 自定义patch任务【产出tinker补丁】
    task tinkerGo {
    
       dependsOn 'tinkerPatchRelease'
       description "Release only - Generate tinker patch and copy to ${dest} patch."
       group 'tinker-ext'
    
       doLast {
           copy {
               def patch = file("${dest}/patch/")
    
               if (!patch.exists()) {
                   patch.mkdirs()
               }
    
               file("${buildDir}/outputs/patch/release/").listFiles().each {
                   from it.absolutePath
                   into file("${patch}")
               }
    
               bakPath.listFiles(new FilenameFilter() {
                   @Override
                   boolean accept(File dir, String filename) {
                       return filename.startsWith("app-")
                   }
               }).each {
                   if (it.isDirectory()) {
                       it.listFiles().each {
                           it.delete()
                       }
                       it.delete()
                   }
               }
           }
       }
    }
    
    • 完整文件代码
    apply from: 'config.gradle'
    apply from: 'utils.gradle'
    
    
    def bakPath = file("archives")
    def appName = 'base'
    
    if (TINKER_ENABLE) {
    
        apply plugin: 'com.tencent.bugly.tinker-support'
    
        tinkerSupport {
    
            // 开启tinker-support插件,默认值true
            enable = true
    
            // 指定归档目录,默认值当前module的子目录tinker
            autoBackupApkDir = "${bakPath}"
    
            // 是否启用覆盖tinkerPatch配置功能,默认值false
            // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
            overrideTinkerPatchConfiguration = true
    
            // 编译补丁包时,必需指定基线版本的apk,默认值为空
            // 如果为空,则表示不是进行补丁包的编译
            // @{link tinkerPatch.oldApk }
            baseApk = "${bakPath}/${appName}/app-release.apk"
    
            // 对应tinker插件applyMapping
            baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
    
            // 对应tinker插件applyResourceMapping
            baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
    
            // 唯一标识当前版本
            tinkerId = "${VERSION_NAME}"
    
            // 是否开启代理Application,设置之后无须改造Application,默认为false
            enableProxyApplication = true
    
        }
    
        def dest = file("${bakPath}/${appName}/")
    
        task tinkerClean() {
    
            def destArchives = file("${bakPath}")
            group 'tinker-ext'
            description "Delete files in ${destArchives} include ${destArchives} self."
    
            doFirst {
                delete(destArchives)
            }
        }
    
        task tinkerPrepare() {
    
            dependsOn 'apkRelease'
            group 'tinker-ext'
            description 'Release only - Generate apk file include: packer,generate tinker bak reouserce fiels.'
    
            doLast {
                copy {
    
                    if (!dest.exists()) {
                        dest.mkdirs()
                    }
    
                    from "${buildDir}/outputs/apk/app-release.apk"
                    into dest
    
                    def destApk = file("${dest}app-release.apk")
                    if (destApk.exists()) {
                        println "Tinker bakApk file ${destApk.absolutePath} done."
                    }
    
                    from "${buildDir}/outputs/mapping/release/app-mapping.txt"
                    into dest
                    rename { String fileName ->
                        fileName.replace("app-mapping.txt", "app-release-mapping.txt")
                    }
    
                    def destMappingFile = file("${dest}app-release-mapping.txt")
                    if (destMappingFile.exists()) {
                        println "Tinker mapping file ${destMappingFile.absolutePath} done."
                    }
    
                    from "${buildDir}/intermediates/symbols/release/R.txt"
                    into "${bakPath}/${appName}/"
                    rename { String fileName ->
                        fileName.replace("R.txt", "app-release-R.txt")
                    }
    
                    def destResourceFile = file("${dest}app-release-R.txt")
                    if (destResourceFile.exists()) {
                        println "Tinker mapping file ${destResourceFile.absolutePath} done."
                    }
    
                    bakPath.listFiles(new FilenameFilter() {
                        @Override
                        boolean accept(File dir, String filename) {
                            return filename.startsWith("app-")
                        }
                    }).each {
                        if (it.isDirectory()) {
                            it.listFiles().each {
                                it.delete()
                            }
                            it.delete()
                        }
                    }
    
                    println 'Tinker prepare done.'
                }
            }
        }
    
        task tinkerGo {
    
            dependsOn 'tinkerPatchRelease'
            description "Release only - Generate tinker patch and copy to ${dest} patch."
            group 'tinker-ext'
    
            doLast {
                copy {
                    def patch = file("${dest}/patch/")
    
                    if (!patch.exists()) {
                        patch.mkdirs()
                    }
    
                    file("${buildDir}/outputs/patch/release/").listFiles().each {
                        from it.absolutePath
                        into file("${patch}")
                    }
    
                    bakPath.listFiles(new FilenameFilter() {
                        @Override
                        boolean accept(File dir, String filename) {
                            return filename.startsWith("app-")
                        }
                    }).each {
                        if (it.isDirectory()) {
                            it.listFiles().each {
                                it.delete()
                            }
                            it.delete()
                        }
                    }
                }
            }
        }
    }
    

    上述的fucking code最终提供如下图的任务

    最终笔者通过自定义的3个任务tinkerClean tinkerGo tinkerPrepare来进行项目的打包(多渠道)发布,及补丁的生成

    • tinkerPrepare效果
    • tinkerGo效果

    上述过程已经完成了Bugly热修复接入的全部工作(至少可以以正常流程来打补丁了),下面将再次简述工程中的代码接入流程。

    代码接入

    Bugly提供了tinker原生的接入方式(TinkerApplication或者ApplicationLike方式)及一键接入,这里介绍下一键接入方式

    设置tinker-support支持使用代理Application

    // 是否开启代理Application,设置之后无须改造Application,默认为false
    enableProxyApplication = true
    

    加载tinker组件

    @Override
    protected void attachBaseContext(Context base) {
       super.attachBaseContext(base);
       MultiDex.install(this);
       Beta.installTinker();
       //设置自定义report监听
       TinkerManager.getInstance().setTinkerReport(new TinkerReporterImpl());
    }
    

    配置bugly

    
    @Override
    public void onCreate(){
        BuglyStrategy strategy = new BuglyStrategy();
        strategy.setAppChannel(PackerNg.getMarket(this));
        logger.i(TAG, "Channel: %s", strategy.getAppChannel());
        Bugly.init(this, "xxx", Config.DEBUG_MODE, strategy);
        Bugly.setIsDevelopmentDevice(this, !Config.DEBUG_MODE);
    }
    

    最后

    文章写到这里,或许已经偏离的我下笔时的初衷,这一方面是我自己水平不足,另一方面也是工程+热修复这一个过程融合的难度所致。

    我希望:

    • 文章对您有所帮助
    • 请您不吝赐教
    • 接下来有提高篇和深入篇

    CatPaw 2017-01-20 于京昆高铁

    相关文章

      网友评论

      • Charleshhh:文章里用到的两个本地插件 utils.gradle和config.gradle 方便贴出来下吗
        CapPaw:建议看看这篇文章 http://www.infoq.com/cn/articles/android-in-depth-gradle ,可以提高对 gradle 的理解及使用。
        CapPaw:config.gradld 简单配置了些版本信息及 bugly 信息,内容为 ext {...}
        utils.gradle 里有些工具函数,主要用到了 delete 方法

        def delete(path) {

        final String METHOD_NAME = "utils.gradle#delete";

        if (path == null || "".equalsIgnoreCase(path)) {
        return
        }
        def dest = null
        if (path instanceof String) {
        dest = file(path)
        } else if (path instanceof File) {
        dest = path
        }

        if (dest == null) {
        throw new NullPointerException("${METHOD_NAME} process null object!!!")
        }

        if (dest.isFile()) {
        dest.delete()
        } else {
        dest.listFiles().each {
        delete(it)
        }
        dest.delete()
        }
        }

        ext {
        delete = this.&delete
        }

      本文标题:Bugly热修复 - 接入篇

      本文链接:https://www.haomeiwen.com/subject/mknvittx.html