站在巨人的肩上——Android热更新框架Tinker探索之旅

Posted ykb19891230

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了站在巨人的肩上——Android热更新框架Tinker探索之旅相关的知识,希望对你有一定的参考价值。

参考资料:
1.Tinker
2.Android 热修复 Tinker接入及源码浅析
如果大家对tinker比较陌生,请先去看看tinker的wiki鸿洋_大神的这篇文章

现在比较流行的热修复框架的优缺点和特色,在Tinker的wiki下都可以看到,这里就不在赘述了,Tinker只用了这句话来展现自己的优势:
Tinker已运行在微信的数亿android设备上,那么为什么你不使用Tinker呢?

Tinker分为gradle接入和命令行接入,但是我这种菜鸟玩不转命令行,所以只能照搬gradle接入Tinker了。但是我感觉,命令行算不上一种接入,仅仅是多了一种编译差分包的方式而已,当然,这仅仅是个人见解哈,至于我为什么这么说,大家往下看就知道了。

首先,在project的build.gradle文件里需要添加Tinker的插件依赖

dependencies 
        classpath 'com.android.tools.build:gradle:1.3.0'
        ...
        //Tinker
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7"
    

其次就是主module的build.gradle文件的配置,主要有下面几个地方

1.引入Tinker依赖

dependencies 
    //tinker必须要引入多dex打包支持
    compile 'com.android.support:multidex:1.0.1'
    //可选,用于注解生成application类
    provided("com.tencent.tinker:tinker-android-anno:1.7.7"
    //tinker依赖版本
    compile("com.tencent.tinker:tinker-android-lib:1.7.7")
 

2.tinker的一些配置

        //-------------------------tinker config start-------------------------
        buildConfigField "String","TINKER_VERSION","\\"1.7.7\\""
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
        buildConfigField "String", "MESSAGE", "\\"I am the base 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\\""
        //-------------------------tinker config end-------------------------

3.编译差分包的开关和老版本文件位置确定

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 
    def oldPrefixFormat="app_beta1_$appVersionName_$new Date().format("yyyy_MM_dd")_%s_build$appVersionCode_%s"
    def oldApkPath=String.format(oldPrefixFormat,"10_34_38","tinker.apk")
    def applyMappingPath=String.format(oldPrefixFormat,"10_34_38","mapping.txt")
    def resourcePath=String.format(oldPrefixFormat,"10_34_38","R.txt")
    //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/$oldApkPath"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "$bakPath/$applyMappingPath"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "$bakPath/$resourcePath"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "$bakPath/app-patch-$releaseTime()"

4.tinker编译差分包的task配置

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

        /**
         * 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
        

        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
                    "com.simpletour.client.tinker.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.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
//        path = "/usr/local/bin/7za"
        
    

5.拷贝和自定义apk、mapping、R文件的别名

   /**
     * bak apk and mapping
     */
    android.applicationVariants.all  variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyy_MM_dd_HH_mm_ss")
        def buildTypeName=variant.baseName+""
        def isRelease=buildTypeName=="release"
        tasks.all 
            if ("assemble$taskName.capitalize()".equalsIgnoreCase(it.name)) 

                it.doLast 
                    copy 
                    //正常情况下生成的apk文件的文件名前缀
                        def fileNamePrefix = "$project.name-$buildTypeName"
                        //自定义apk文件的别名
                        def newFileNamePrefix="app_beta1_$isRelease?"$buildTypeName_":""$appVersionName_$date_build$appVersionCode"
                        //自定义apk、mapping、R文件存放的目录
                        def destPath = hasFlavors ? file("$bakPath/$project.name-$date/$variant.flavorName") : bakPath
                        //复制apk文件和重命名
                        from variant.outputs.outputFile
                        into destPath
                        rename  String fileName ->
                            fileName.replace("$fileNamePrefix.apk", "$newFileNamePrefix_tinker.apk")
                        
                        //复制和重命名mapping文件
                        from "$buildDir/outputs/mapping/$variant.dirName/mapping.txt"
                        into destPath
                        rename  String fileName ->
                            fileName.replace("mapping.txt", "$newFileNamePrefix_mapping.txt")
                        
                        //复制和重命名R文件
                        from "$buildDir/intermediates/symbols/$variant.dirName/R.txt"
                        into destPath
                        rename  String fileName ->
                            fileName.replace("R.txt", "$newFileNamePrefix_R.txt")
                        
                    
                
            
        
    

有了上面这些基本的配置,基本上tinker的引入就算完成了。这里要说一下引入multidex的目的不仅是解决65536的限制,还有就是因为tinker的修复原理是dex合并,所以必须支持multidex。还有就是编译差分包的时候,老版本apk、mapping、R文件的路径一定要配正确,不然tinkerPatch任务会执行失败。由于tinker不支持dex加固后的热修复,所以采用dex加固加密的apk,就不能使用tinker来做热更新了。最后要注意的就是,tinker补丁安装完成后,必须重启你的应用才能生效,在华为手机上甚至需要清除缓存的应用进程后重启才能生效。

下面是完整的app-build.gradle文件

apply plugin: 'com.android.application'
def appVersionCode
def appVersionName
android 
    lintOptions 
        abortOnError false
    
    //tinker recommend
    dexOptions
        jumboMode true;
    
    signingConfigs 
        release 
            keyAlias '你的签名key的别名'
            keyPassword '你的签名key的密码'
            storeFile file('你的签名文件')
            storePassword '你的签名文件的密码'
        
        debug 
              keyAlias '你的签名key的别名'
            keyPassword '你的签名key的密码'
            storeFile file('你的签名文件')
            storePassword '你的签名文件的密码'
        
    
    compileSdkVersion 23
    buildToolsVersion "23.0.1"
    defaultConfig 
        applicationId "你的工程主module包名"
        minSdkVersion 15
        targetSdkVersion 23
        multiDexEnabled = true
        versionCode 68
        versionName "2.4.0"
        signingConfig signingConfigs.release
        //-------------------------tinker config start-------------------------
        appVersionCode=versionCode
        appVersionName="v".concat(versionName)
        buildConfigField "String","TINKER_VERSION","\\"1.7.7\\""
        /**
         * buildConfig can change during patch!
         * we can use the newly value when patch
         */
        buildConfigField "String", "MESSAGE", "\\"I am the base 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\\""
        //-------------------------tinker config end-------------------------

    
    buildTypes 
        debug 
            // 不显示log
            buildConfigField "boolean", "ENABLE_DEBUG", "true"
            //不启用混淆编译
            minifyEnabled false
            //移除无用的资源文件
            shrinkResources true
            //Zipalign优化
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        
        release 
            // 不显示log
            buildConfigField "boolean", "ENABLE_DEBUG", "false"
            //不启用混淆编译
            minifyEnabled false
            //移除无用的资源文件
            shrinkResources true
            //Zipalign优化
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        
    
    //解决编译时as内存溢出
    dexOptions 
        preDexLibraries = false
        javaMaxHeapSize "4g"   //这个改大
    


repositories 
    mavenCentral()
    flatDir 
        dirs 'libs'
    


def releaseTime() 
    return new Date().format("yyyy_MM_dd")


dependencies 
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:multidex:1.0.1'
    //tinker recommend options ,可选,用于生成application类
    provided("com.tencent.tinker:tinker-android-anno:1.7.7"

//-------------------------tinker build start-------------------------
//生成tinker_id,在app/build/intermediates/manifests/full/debug(或release)下的Manifest.xml
//文件里可以看到
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'")
    

//自定义重命名apk包和R.txt以及mapping.txt的存放目录
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 
    //这个是我自定义打包apk的名字的格式化字符串(关于自定义输出apk文件的名字那些,参考后面的"bak apk and     //mapping"注释那里),这里找到上次编译的apk、mapping、R文件,用来和本次新编译的文件做diff生成补丁包
    def oldPrefixFormat="app_beta1_$appVersionName_$new Date().format("yyyy_MM_dd")_%s_build$appVersionCode_%s"
    def oldApkPath=String.format(oldPrefixFormat,"10_34_38","tinker.apk")
    def applyMappingPath=String.format(oldPrefixFormat,"10_34_38","mapping.txt")
    def resourcePath=String.format(oldPrefixFormat,"10_34_38","R.txt")
    //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/$oldApkPath"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "$bakPath/$applyMappingPath"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "$bakPath/$resourcePath"

    //only use for build all flavor, if not, just ignore this field
    //这个是你自定义的差分包相关文件的输出路径
    tinkerBuildFlavorDirectory = "$bakPath/app-patch-$releaseTime()"


//下面这些是一些自定义的方法,用来获取old apk的路径、old mapping的路径、生成tinker_id等
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") ? TINKER_ENABLE : ext.tinkerEnabled


def getTinkerBuildFlavorDirectory() 
    return ext.tinkerBuildFlavorDirectory

//重点来了,这里是使用tinker编译的许多配置信息
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

        /**
         * 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
        

        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
                    "com.simpletour.client.tinker.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.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
//        path = "/usr/local/bin/7za"
        
    

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each flavor ->
        flavors.add(flavor.name)
    
    boolean hasFlavors = flavors.size() > 0
    //自定义apk输出别名请看这里
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all  variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("yyyy_MM_dd_HH_mm_ss")
        def buildTypeName=variant.baseName+""
        def isRelease=buildTypeName=="release"
        tasks.all 
            if ("assemble$taskName.capitalize()".equalsIgnoreCase(it.name)) 

                it.doLast 
                    copy 
                    //正常情况下生成的apk文件的文件名前缀
                        def fileNamePrefix = "$project.name-$buildTypeName"
                        //自定义apk文件的别名
                        def newFileNamePrefix="app_beta1_$isRelease?"$buildTypeName_":""$appVersionName_$date_build$appVersionCode"
                        //自定义apk、mapping、R文件存放的目录
                        def destPath = hasFlavors ? file("$bakPath/$project.name-$date/$variant.flavorName") : bakPath
                        //复制apk文件和重命名
                        from variant.outputs.outputFile
                        into destPath
                        rename  String fileName ->
                            fileName.replace("$fileNamePrefix.apk", "$newFileNamePrefix_tinker.apk")
                        
                        //复制和重命名mapping文件
                        from "$buildDir/outputs/mapping/$variant.dirName/mapping.txt"
                        into destPath
                        rename  String fileName ->
                            fileName.replace("mapping.txt", "$newFileNamePrefix_mapping.txt")
                        
                        //复制和重命名R文件
                        from "$buildDir/intermediates/symbols/$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"
                    

                
            
        
    

//-------------------------tinker build end-------------------------

额。。。是不是看起来好乱的样子。。。但是不要担心,一般都能编译过的。

开始的时候我说:命令行算不上一种接入,仅仅是多了一种编译差分包的方式而已。这里就来解释这个问题。
生成差分包的方式有两种:一种是刚才我们在build文件里配的tinkerpatch任务,配置好老版本的apk、mapping(release版才需要)、R文件,直接在build.gradle任务列表里执行tinkerpatchdebug或tinkerpatchrelease任务即可生成对应的差分包;还有一种,要先clone tinker源码,编译tinker-build/tinker-patch-cli项目,得到tinker-patch-cli-***jar,然后,每次把老版本的apk、mapping(release版才需要)、R文件拷贝下来存储好,apk文件命名为old.apk ,然后编译新版apk,新apk命名为new.apk,把old.apk和new.apk拷贝到tinker-build/tinker-patch-cli/tool_output目录下,修改一下tinker_config文件里的几个地方:

value的值必须用英文引号包起来

1.签名文件配置,

   <issue id="sign">
        <!--the signature file path, in window use \\, in linux use /, and the default path is the running location-->
        <path value="你的签名文件的路径"/>
        <!--storepass-->
        <storepass value="签名文件密码"/>
        <!--keypass-->
        <keypass value="签名key密码"/>
        <!--alias-->
        <alias value="签名文件别名"/>
    </issue>

2.自定义继承自TinkeApplication的你的apiilication类

    <issue id="dex">
        <!--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 value="jar"/>

        <!--what dexes in apk are expected to deal with tinkerPatch-->
        <!--it support * or ? pattern.-->
        <pattern value="classes*.dex"/>
        <pattern value="assets/secondary-dex-?.jar"/>

        <!--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 @code SampleTinkerLoader, and the classes you use in them-->
        <loader value="com.tencent.tinker.loader.*"/>
        **<loader value="包名.继承自TinkeApplication的你的apiilication类"/>**
    </issue>

然后执行下面的命令:

java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output

这样会在tinker-build/tinker-patch-cli/tool_output生成一个out_put文件夹,里面包含一些文件,主要的就是patch_signed.apk和patch_signed_7zip.apk(优化过后的差分包),一般不建议直接用apk文件做补丁后缀名。

生成文件如下

到这里,是否感觉到命令行方式有点小麻烦?所以我一般都是备份好上次发布版本的apk等文件,然后拷贝回工程的bak文件夹下,用gradle task编译。

在来讲讲代码吧,我没有使用tinker推荐的注解方式来生成application类,因为我们的项目已经迭代了很多版本,我们的Application里面有很多自定义的东西,所以只能继承自TinkerApplication关联对应的DefaultApplicationLike来实现了

application文件

/**
 * 包名:com.simpletour.client.app
 * 描述:主Application
 * 创建者:yankebin
 * 日期:2015/12/3
 * MApplication继承自TinkerApplication
 */
public class SimpletourApp extends MApplication 
    private static volatile SimpletourApp app;
    private DisplayImageOptions avatarOptions;
    private DisplayImageOptions assistantOptions;
    //无参构造
    public SimpletourApp() 
    //这几个参数的含义请参考[接入指南](https://github.com/Tencent/tinker/wiki/Tinker-接入指南)
        this(ShareConstants.TINKER_ENABLE_ALL, "com.simpletour.client.app.SimpleTourAppLike",//这个是自定义的DefaultApplicationLike
                "com.tencent.tinker.loader.TinkerLoader", false);
    
    //TinkerApplication的构造方法
    protected SimpletourApp(int tinkerFlags, String delegateClassName, String loaderClassName, boolean tinkerLoadVerifyFlag)
        super(tinkerFlags,delegateClassName,loaderClassName,tinkerLoadVerifyFlag);
    
    public DisplayImageOptions getAvatarOptions() 
        return avatarOptions;
    

    public DisplayImageOptions getAssistantOptions() 
        return assistantOptions;
    

    public static synchronized SimpletourApp getInstance() 
        return app;
    

    @Override
    public void onCreate() 
        super.onCreate();
        app = this;
        //初始化服务器URL,测试环境才需要
        if (SDKConfig.DEVELOP_MODE) 
            Constant.initUrlConfig(this);
        
        //服务端接口请求api初始化
        RetrofitApi.RetrofitConfig config = new RetrofitApi.RetrofitConfig.RequestBuilder()
                .defaultConfig(Constant.URL.URL_BASE_URL_).build();
        RetrofitApi.getInstance().init(config);
        //图片加载器
        ToolImage.init(this, Constant.BASE_CACHE_DIR_NAME.concat("/cache/img"),
                R.drawable.default_img_retange);
        //日志工具
        SLog.init(BuildConfig.ENABLE_DEBUG, Constant.BASE_CACHE_DIR_NAME.concat("/cache/log"));
        //友盟统计
        MobclickAgent.openActivityDurationTrack(false);
        //友盟推送
        PushAgent.getInstance(this).setDebugMode(BuildConfig.ENABLE_DEBUG);
        //启动主服务
        startMainService();
        //初始化又拍云上传图片sdk
        initUpLoadSdk();
        //初始化友盟分享sdk
        initShareSdk();
        //崩溃日志
        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
        //初始化百度地图sdk
        SDKInitializer.initialize(this);
        //初始化美洽sdk
        initMeiQiaSdk();
        //初始化头像显示配置
        initAvatarOptions();
        //清除缓存
        checkCache();
    

    /**
     * 初始化头像显示的配置
     */
    private void initAvatarOptions() 
        avatarOptions = new DisplayImageOptions.Builder()
                .showImageOnLoading(R.drawable.header_login)
                .showImageForEmptyUri(R.drawable.header_login)// 空uri时的默认图片
                .showImageOnFail(R.drawable.header_login)// 加载失败时的默认图片
                .cacheInMemory(true)// 是否缓存到内存
                .cacheOnDisk(true)// 是否缓存到磁盘
                .bitmapConfig(Bitmap.Config.RGB_565)// 图片格式比RGB888少消耗2倍内存
                .imageScaleType(ImageScaleType.EXACTLY)// 图片缩放方式
                .resetViewBeforeLoading(true)//有效避免oom的方式之一
                .build();

        assistantOptions = new DisplayImageOptions.Builder()
                .showImageOnLoading(R.drawable.assistant_gray_avatar)
                .showImageForEmptyUri(R.drawable.assistant_gray_avatar)// 空uri时的默认图片
                .showImageOnFail(R.drawable.assistant_gray_avatar)// 加载失败时的默认图片
                .cacheInMemory(true)// 是否缓存到内存
                .cacheOnDisk(true)// 是否缓存到磁盘
                .bitmapConfig(Bitmap.Config.RGB_565)// 图片格式比RGB888少消耗2倍内存
                .imageScaleType(ImageScaleType.EXACTLY)// 图片缩放方式
                .resetViewBeforeLoading(true)//有效避免oom的方式之一
                .build();
    

    /**
     * 初始化美洽sdk
     */
    private void initMeiQiaSdk() 
        MQConfig.init(this, Constant.KEY.MEI_QIA_SDK_APP_KEY, new OnInitCallback() 
            @Override
            public void onSuccess(String clientId) 
                SLog.d("meiq

以上是关于站在巨人的肩上——Android热更新框架Tinker探索之旅的主要内容,如果未能解决你的问题,请参考以下文章

7. 如何构建主题域模型原则之站在巨人的肩上IBM-FSDM主题域模型划分

8. 如何构建主题域模型原则之站在巨人的肩上NCR FS-LDM主题域模型划分

Android 框架学习5:微信热修复框架 Tinker 从使用到 patch 加载生成合成原理分析

android开发利器--站在巨人肩膀上前行

站在巨人的肩膀上 -- Retrofit源码解析

站在巨人的肩膀上 -- Retrofit源码解析