Tinker項目(點擊進入)
Tinker是微信官方的Android熱補丁解決方案,它支持動態下發代碼。.so庫以及資源庫,讓應用可以在不須要重複安裝的狀況下實現更新,固然也可使用Tinker來更新你的插件。css
在接入Tinker以前咱們先對Tinker的結構瞭解一下java
Tinker主要包括一下幾個部分:
1.gradle編譯插件:tinker-patch-gradle-plugin。
2.核心SDK庫:tinker-android-lib。
3.非gradle編譯用戶的命令行版本:tinker-patch-cil.jar。android
Tinker的已知問題:
1.Tinker不支持修改AndroidMainfest.xml,Tinker不支持新增四大組件。
2.因爲Google Pay的開發者條款限制,不建議在GP渠道動態更新代碼。
3.在Android N上,補丁對應用啓動時有輕微的影響。
4.不支持部分三星android-21機型,加載補丁時會主動拋出「TinkerRuntimeException:checkDexInstall failed」異常。
5.因爲各個廠商加固實現並不一致,在1.7.6之後的版本,Tinker不在支持加固的動態更新。
6.對於資源替換,不支持修改remoteView,例如transition動畫,notification icon以及桌面圖標。git
Tinker的修復方案跟Hotfix的修復方案大同小異,都是在兩個apk包上做比較而後生成patch。下面對Tinker進行接入。github
在工程目錄下的build.gradle中添加依賴庫
buildscript { repositories { jcenter() }
dependencies { classpath 'com.android.tools.build:gradle:2.2.3' // 編譯插件tinker-patch-gradle-plugin classpath 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7' }
}
allprojects { repositories { jcenter() }
}
task clean(type: Delete) { delete rootProject.buildDir }
在工程app目錄下的build.gradle中添加依賴庫
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
......
//可選,用於生成application類
provided('com.tencent.tinker:tinker-android-anno:1.7.7')
//tinker的核心庫
compile('com.tencent.tinker:tinker-android-lib:1.7.7')
}
API引入
通常咱們都是在Application中onCreate()中作初始化和加載patch,不過Tinker推薦以下寫法。由於程序啓動時會加載默認的Application類,這致使咱們補丁包是沒法對它作修改了。爲了規避這個問題Tinker經過代碼框架的方式來避免,這也是爲了儘可能少的去反射,提高框架的兼容性。web
@DefaultLifeCycle(
application = ".AppContext", flags = ShareConstants.TINKER_ENABLE_ALL
)
public class AppContextLike extends ApplicationLike {
public AppContextLike(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();
TinkerInstaller.install(this);
}
}
代碼中AppContextLike繼承了ApplicationLike,而ApplicationLike並不是集成Application,而是相似於Application的一個類。Tinker建議編寫一個ApplicationLike的子類,能夠當作Application使用,注意頂部的註解:@DefaultLifeCycle,其application屬性,會在編譯期生成一個.AppContext類。因此咱們在AndroidManifest.xml中的application標籤下這樣寫:算法
<application
android:name=".AppContext"
......
</application>
寫完後會報紅,此時只須要Build下便可解決報紅。Application配置就到此結束。接下來生成patch文件。由於patch文件是寫入到SDCrad的,因此咱們須要在AndroidManifest中添加以下權限(注: 6.0及已上系統請動態設置權限或者手動在設置中爲項目設置):api
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Tinker須要在AndroidManifest.xml中指定TINKER_ID微信
<meta-data
android:name="TINKER_ID"
android:value="tinker_id_100" />
Patch生成
patch生成官方提供了兩種接入方式:app
1.基於命令行的方式。
2.gradle編譯的方式。
1.gradle編譯生成patch
微信Tinker的gradle配置也很簡單,先來瀏覽一下Tinker接入指南,點擊進入查看,對使用gradle配置的參數瞭解一下,接下來附上一個相對比較完整的gradle配置。
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.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.tinker.app"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0.0"
// 使用multiDex庫
multiDexEnabled true
// 設置簽名
signingConfig signingConfigs.config
manifestPlaceholders = [TINKER_ID: "${getTinkerIdValue()}"]
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "CLIENTVERSION", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.config
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
signingConfig signingConfigs.config
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:25.1.0'
// 依賴multiDex庫
compile 'com.android.support:multidex:1.0.1'
//可選,用於生成application類
provided ("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"){changing = true}
//Tinker的核心庫
compile ("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"){changing = true}
}
// 指定JDK版本
def getJavaVersion() {
return JavaVersion.VERSION_1_7
}
def bakPath = file("${buildDir}/bakApk/")
/** * ext相關配置 */
ext {
tinkerEnabled = true
// 基礎版本apk
tinkerOldApkPath = "${bakPath}/app-debug-20170217-18-51-30.apk"
// 未開啓混淆的話mapping能夠忽略,若是開啓混淆mapping要保持一致。
tinkerApplyMappingPath = "${bakPath}/"
// 與基礎版本一塊兒生成的R.text文件
tinkerApplyResourcePath = "${bakPath}/app-debug-20170217-18-51-30-R.txt"
// 只用於構建全部的Build,若是不是,此處可忽略。
tinkerBuildFlavorDirectory = "${bakPath}/"
}
// 基礎APK的位置
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
// Mapping的位置
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
// ResourceMapping的位置
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
// 用來獲取TinkerId(當前版本號就是TinkerId)
def getTinkerIdValue() {
return android.defaultConfig.versionName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? 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版本以後版本,參數一、2會自動填寫。 * */
loader = [
// Tinker庫中用於加載補丁包的部分類
"com.tencent.tinker.loader.*",
// 本身定義的Application類;
"com.tinker.app.AppContext",
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.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
from variant.outputs.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"
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"
}
}
}
}
}
}
gradle配置就到此結束了,要注意的地方有如下幾點:
1.ext相關配置,示例中有完整描述。
2.Tinker插件apply plugin: 'com.tencent.tinker.patch'
3.全局信息相關配置tinkerPatch
配置完這些東西之後就能夠調用tinkerPatch命令生成patch補丁文件。tinkerPatch有Debug和Release兩種模式,由於是案例,因此就使用tinkerPatchDebug命令。
注意:調用tinkerPatchDebug命令以前須要修改ext相關配置,ext相關配置已基準apk包爲準。
到此微信Tinker熱修復gradle配置結束。
案例地址:https://github.com/mengjingbo/TinkerApp
本文分享 CSDN - 秦川小將。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。