Gradle實戰2:微信Tinker項目中的tinker-patch-gradle-plugin模塊解析

引言

上一篇,咱們學習了《微信Tinker項目中的maven-publish封裝》,瞭解到了在一個成熟項目中,maven相關gradle的通用封裝,進而鞏固前面學習的gradle相關理論知識java

接下來,咱們將對Tinker項目中的tinker-patch-gradle-plugin模塊進行解析,進一步感覺Gradle在億級應用中散發的魅力android

PS1:本章主要是跟蹤《tinker-patch-gradle-plugin模塊》實現,來鞏固gradle相關知識,具體熱修相關安卓知識的話不會展開git

PS2:因爲tinker的官方工程比較大,對於鞏固gradle知識干擾比較大,因此本章的代碼工程是閹割了官方的代碼展開,更加聚焦,同時實現博客和源碼配套的模式github

簡介

tinker-patch-gradle-plugin模塊,是開發者使用Thinker入口;微信

若是咱們app集成thinker,其實就是對這個模塊的使用,由於thinker的實現markdown

都以插件的方式被封裝到了這個模塊,具體官方代碼位置戳這裏>>>app

上面說到,咱們會對官方工程進行裁剪,裁剪後對應的模塊位置戳這裏>>>less

解析過程

應用模塊

1.png 此圖爲咱們的app模塊引入thinker的步驟maven

1)模塊所在位置,其實就是咱們app模塊的gradle文件ide

2)插件引入,經過classpath關鍵字引入封裝好的thinker插件,其中插件的maven發佈咱們發佈到了本地,因此用的時候咱們maven指向了本地的 ‘../repo’

3)插件的使用,經過apply引入,而後tinkerPatch,buildConfig都是插件的自定義拓展,具體實如今下面步驟講解

模塊之工程定義

2.png

1)插件實現工程目錄,能夠看出這是一個gradle插件的標準目錄,具體詮釋見往期教程

2)插件實現工程gradle文件,這裏除了有自定義插件的依賴外,還用到了上一章講解的maven-publish封裝

模塊之自定義拓展

上面有提到tinkerPatch,buildConfig關鍵字爲自定義拓展,這兩個關鍵字只是thinker

中的拓展之一,咱們來看下thinker的自定義拓展全貌:

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

            /** * 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.1.10"
            /** * optional,default '7za' * you can specify the 7za path yourself, it will overwrite the zipArtifact value */
// path = "/usr/local/bin/7za"
        }
    }
複製代碼

精簡後大體結構以下:

tinkerPatch {
        oldApk = getOldApkPath()
        buildConfig {
            supportHotplugComponent = false
        }
        dex {
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
        }
        lib {
            pattern = ["lib/*/*.so"]
        }
        res {
            largeModSize = 100
        }
        packageConfig {
            configField("patchVersion", "1.0")
        }
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
 }
複製代碼

這裏具體的配置是什麼意思,先不用關注,由於配置的意思涉及到thinker自己業務和一些安卓熱修相關知識點,咱們主要關注下若是咱們要實現這樣的結構拓展,應該怎麼作?下面咱們來看看thinker是怎麼作的:

3.png

1)插件入口,gradle插件的代碼類入口

2)和 3),拓展調用和定義,在定義中能夠看到經過‘project.extensions.create’ 來建立自定義拓展,層級嵌套拓展經過追加方式,如:先自定義tinkerPatch,而後在tinkerPatch中嵌套buildConfig

其中,細心的朋友會看到,爲何有些create是2個參數,有些是3個參數

4.png

根據官方定義能夠看出,前面兩個是定義拓展的key和value,而後後面的可變參數是 要傳給value拓展的參數,舉個例子:

project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project)
複製代碼

key爲‘buildConfig’,value爲‘TinkerBuildConfigExtension’

而後project則是傳遞參數給到了‘TinkerBuildConfigExtension’,具體見TinkerBuildConfigExtension的定義以下:

6.png

其餘拓展以此類推,這裏不一一展開

模塊之配置android屬性

7.png

1)preDexLibraries 設置爲false

默認狀況下,preDexLibraries是爲true的,做用主要是用來確認是否對Lib作preDexing操做,操做了的話帶來的好處提升增量構建的速度;

這裏設置爲false,猜想是thinker涉及了多dex,爲避免和庫工程衝突

另外preDexLibraries是dexOptions屬性之一,dexoptions是一個gradle對象,這個對象用來設置從java代碼向.dex文件轉化的過程當中的一些配置選項;

更多dexOptions屬性能夠戳這裏>>>

2)jumboMode 設置爲true

jumboMode設置爲true,意識是忽略方法數限制的檢查

這樣作的缺點是apk沒法再低版本的設備上面安裝,會出現錯誤:INSTALL_FAILED_DEXOPT

具體細節戳這裏>>>

3)關閉 ENABLE_DEX_ARCHIVE

void disableArchiveDex(Project project) {
        println 'disableArchiveDex -->'
        try {
            def booleanOptClazz = Class.forName('com.android.build.gradle.options.BooleanOption')
            def enableDexArchiveField = booleanOptClazz.getDeclaredField('ENABLE_DEX_ARCHIVE')
            enableDexArchiveField.setAccessible(true)
            def enableDexArchiveEnumObj = enableDexArchiveField.get(null)
            def defValField = enableDexArchiveEnumObj.getClass().getDeclaredField('defaultValue')
            defValField.setAccessible(true)
            defValField.set(enableDexArchiveEnumObj, false)
        } catch (Throwable thr) {
            // To some extends, class not found means we are in lower version of android gradle
            // plugin, so just ignore that exception.
            if (!(thr instanceof ClassNotFoundException)) {
                project.logger.error("reflectDexArchiveFlag error: ${thr.getMessage()}.")
            }
        }
    } 
複製代碼

ENABLE_DEX_ARCHIVE 這個功能主要是減小dex的大小

這裏關閉主要避免破壞multidex的maindex規則,進而實現多dex的場景

4)keepRuntimeAnnotatedClasses 設置爲 false

keepRuntimeAnnotatedClasses 主要做用是帶有運行時註解的類,保留在主dex中

thinker關閉,主要下降主dex大小,兼容5.0如下的狀況

模塊之aapt2和資源固定相關

1.png 2.png

該部分功能實現:使用導出的符號表進行資源id的固定

爲何要進行資源ID的固定?具體戳這裏>>,背景細節不在這展開

其中,這裏實現邏輯爲:

1)判斷當前《Android Gradle Plugin》是否啓動aapt2,若是沒有啓動跳過,若是啓動了則進行aapt2的資源固定適配

2)aapt2的資源固定適配操做,經過指定穩定的資源id映射文件,同時結合「--stable-ids」命令進行固定

源碼

戳這裏>>>

小結

到這裏gradle的學習接下來就要告一斷落了,前面主要是學習了系列基礎理論,而後結合了兩篇實戰進行鞏固,剩下的就要靠本身項目中不斷的使用,才能進入到下一步高級的境界;

結尾

哈哈,該篇就寫到這裏(一塊兒體系化學習,一塊兒成長)

相關文章
相關標籤/搜索