Tinker爬坑之路
目的
熱修復去年年底出的時候,變成了今年最火的技術之一。依舊記得去年面試的時候統一的MVP,然而今年卻變成了RN,熱修復。這不得不導致我們需要隨時掌握最新的技術。不然可能隨時會被淘汰。記得剛進公司,技術總監就給我們佈置了學習RN和熱修復的任務。不過因為臨時調整去了產品部。學習的時間變少了。最近抽空看了關於熱修復。於是,便有了此文。
概述
關於熱修復,說白了就是線上修復,這個基本就不用多解釋了。因為如果出了緊急bug。天天更新。使用者不嫌煩我都嫌煩。而且頻繁更新會導致使用者的損失。
現在熱修復的技術基本上有阿里的AndFix/Hotfix、QZone的方案、美團提出的思想方案以及騰訊的Tinker等。
我記得andfix我花了半天,hotfix花了半小時,tinker花了半個月。總之踩了蠻多的坑,走了蠻多的彎路的。
andfix算是比較簡單的把。不過相容性太差。hotfix用的阿里百川的後臺,使用方法也不難。QZone的思路可以去看看。至於美團,應該是相容性最好的了。據說,已經能相容99%的手機了。最後,就是tinker了。tinker官方介紹有這麼一句話:
Tinker已執行在微信的數億Android裝置上,那麼為什麼你不使用Tinker呢?
其實我當時感覺hotfix就夠用了。不過當我看了這句話,我就準備去看看了。不然到時候被人瞧不起就完了。“什麼?你除了tinker其他都不會?現在基本都是隻要會個tinker就行了。”不管它,不管他,我們先入坑在說。
入坑Tinker
tinker的接入方式有2種,一種的命令列接入,一種是gradle接入。
命令列接入
命令列的接入和andfix以及hotfix的使用方法是差不多的。我也沒去試~畢竟tinker是推薦用gradle的。好了。我們看看如何使用命令列接入的把。
專案依賴
我們需要在2個build.gradle進行
- moudle的gradle中:
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11' )
}
}
- app的gradle中:
//apply tinker外掛
apply plugin: 'com.tencent.tinker.patch'
...
...
dependencies {
//可選,用於生成application類
provided('com.tencent.tinker:tinker-android-anno:1.7.11')
//tinker的核心庫
compile('com.tencent.tinker:tinker-android-lib:1.7.11')
}
然後我們進行sync now,會有如下一句話:
tinkerId什麼鬼?我也不知道。然後發現我們接入了gradle還要對其進行配置。好了。那麼,我們接下來進行配置咯~
gradle配置
gradle有一屁股的引數。腦闊疼。我們先看看到底是幹啥的:
引數 | 預設值 | 描述 |
---|---|---|
tinkerPatch |
全域性資訊相關的配置項 | |
tinkerEnable | true | 是否開啟tinker的功能。 |
oldApk | null | 基準apk包的路徑,必須輸入,否則會報錯。 |
newApk | null | 選填,用於編譯補丁apk路徑。如果路徑合法,即不再編譯新的安裝包,使用oldApk與newApk直接編譯。 |
outputFolder null | 選填,設定編譯輸出路徑。預設在build/outputs/tinkerPatch 中 |
|
ignoreWarning | false | 如果出現以下的情況,並且ignoreWarning為false,我們將中斷編譯。因為這些情況可能會導致編譯出來的patch包帶來風險: 1. minSdkVersion小於14,但是 dexMode 的值為”raw”;2. 新編譯的安裝包出現新增的四大元件(Activity, BroadcastReceiver…); 3. 定義在dex.loader用於載入補丁的類不在main dex中; 4. 定義在dex.loader用於載入補丁的類出現修改; 5. resources.arsc改變,但沒有使用applyResourceMapping編譯。 |
useSign | true | 在執行過程中,我們需要驗證基準apk包與補丁包的簽名是否一致,我們是否需要為你簽名。 |
buildConfig |
編譯相關的配置項 | |
applyMapping | null | 可選引數;在編譯新的apk時候,我們希望通過保持舊apk的proguard混淆方式,從而減少補丁包的大小。這個只是推薦設定,不設定applyMapping也不會影響任何的assemble編譯 。 |
applyResourceMapping | null | 可選引數;在編譯新的apk時候,我們希望通過舊apk的R.txt 檔案保持ResId的分配,這樣不僅可以減少補丁包的大小 ,同時也避免由於ResId改變導致remote view異常 。 |
tinkerId | null | 在執行過程中,我們需要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。這個是決定補丁包能執行在哪些基準包上面,一般來說我們可以使用git版本號、versionName等等。 |
keepDexApply | false | 如果我們有多個dex,編譯補丁時可能會由於類的移動導致變更增多。若開啟keepDexApply 模式,補丁包將根據基準包的類分佈來編譯。 |
isProtectedApp | false | 是否使用加固模式,僅僅將變更的類合成補丁。注意,這種模式僅僅可以用於加固應用中。 |
dex |
dex相關的配置項 | |
dexMode | jar | 只能是’raw’或者’jar’。 對於’raw’模式,我們將會保持輸入dex的格式。 對於’jar’模式,我們將會把輸入dex重新壓縮封裝到jar。如果你的minSdkVersion小於14,你必須選擇‘jar’模式,而且它更省儲存空間,但是驗證md5時比’raw’模式耗時。預設我們並不會去校驗md5,一般情況下選擇jar模式即可。 |
pattern | [] | 需要處理dex路徑,支援*、?萬用字元,必須使用’/’分割。路徑是相對安裝包的,例如assets/… |
loader | [] | 這一項非常重要,它定義了哪些類在載入補丁包的時候會用到。這些類是通過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會自動填寫。 |
lib |
lib相關的配置項 | |
pattern | [] | 需要處理lib路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/… |
res |
res相關的配置項 | |
pattern | [] | 需要處理res路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如assets/…,務必注意的是,只有滿足pattern的資源才會放到合成後的資源包。
|
ignoreChange | [] | 支援*、?萬用字元,必須使用’/’分割。若滿足ignoreChange的pattern,在編譯時會忽略該檔案的新增、刪除與修改。 最極端的情況,ignoreChange與上面的pattern一致,即會完全忽略所有資源的修改。 |
largeModSize | 100 | 對於修改的資源,如果大於largeModSize,我們將使用bsdiff演算法。這可以降低補丁包的大小,但是會增加合成時的複雜度。預設大小為100kb |
packageConfig |
用於生成補丁包中的’package_meta.txt’檔案 | |
configField | TINKER_ID, NEW_TINKER_ID | configField(“key”, “value”), 預設我們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動寫入configField。在這裡,你可以定義其他的資訊,在執行時可以通過TinkerLoadResult.getPackageConfigByName得到相應的數值。但是建議直接通過修改程式碼來實現,例如BuildConfig。 |
sevenZip |
7zip路徑配置項,執行前提是useSign為true | |
zipArtifact | null | 例如”com.tencent.mm:SevenZip:1.1.10”,將自動根據機器屬性獲得對應的7za執行檔案,推薦使用。 |
path | 7za | 系統中的7za路徑,例如”/usr/local/bin/7za”。path設定會覆蓋zipArtifact,若都不設定,將直接使用7za去嘗試。 |
好了。胡亂一通介紹完了。然後官方來了句具體參考sample的配置。於是乎,我當時興沖沖的把他的配置給複製過來了= =然後居然還是剛才的問題。我相信官方的demo他們敢放出來肯定沒問題的。那麼是我的問題咯?那就想辦法改咯。最後問了好多小夥伴,終於。build success了~神器!!!那麼為了讓更多小夥伴不再入我的坑位,我這邊貼出我的build.gradle的配置:
/**Tinker 相關配置----------------開始-----------------------------------*/
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'")
}
}
def javaVersion = JavaVersion.VERSION_1_7
def bakPath = file("${buildDir}/bakApk/")
/**
* 你可以使用組裝版本構建你的基礎apk
* you can use assembleRelease to build you base apk
* 使用修補補丁釋出
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* 新增apk到build/bakApk
* 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
*/
/**
* 老的apk去構建補丁的apk
* old apk file to build patch apk
*/
tinkerOldApkPath = "${bakPath}/app-debug-0605-16-34-49.apk"
/**
* 構建補丁apk的混淆對映檔案
* proguard mapping file to build patch apk
*/
tinkerApplyMappingPath = "${bakPath}/app-debug-0605-16-34-49-mapping.txt"
/**
* 構建補丁apk時,R.txt必須輸入資原始檔是否改變
* resource R.txt to build patch apk, must input if there is resource changed
*/
tinkerApplyResourcePath = "${bakPath}/app-debug-0605-16-34-49-R.txt"
/**
* 只是用於構建所有的特點,如果不,可以忽略這一領域
* only use for build all flavor, if not, just ignore this field
*/
tinkerBuildFlavorDirectory = "${bakPath}/app-0605-16-34-49"
}
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
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* 必要的,預設為null
* 舊的apk路徑,從build/bakApk中,用不同於新的apk去構建新增apk
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* 可選的,預設為false
* 這邊有一些情況,我們或許會得到警告
* 如果它為true,我們會維護補丁的過程
* 情形1:最小sdk小於14,但你用原生的敏捷模式,當載入時,他必定會奔潰
* 情形2:在manifest中新建的android元件,當載入時,他必定會崩潰
* 情形3:在dex.loader不能保證在main dex中載入classes,他必須讓tinker無效
* 情形4:在dex.loader載入classes檔案改變時,去載入補丁時,改變他們是不會奔潰的,但這些變化不影響,你可以忽視他們
* 情形5:資原始檔改變是,我們不能用應用資源去對映
* 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 = true
/**
* 可選的,預設為true
* 是否簽署的補丁檔案,如果沒有,你必須自定義,否則不能成功的載入補丁,在你建立型別時,我們將使用構建簽訂
* 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
/**
* 可選的,預設為true
* 是否使用tinker建立
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = true
/**
* 警告,應用對映將影響android的構建
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* 可選的,預設為null
* 如果我們用tinker補丁去構建補丁包,如果minifyEnabled啟用了,你最好依賴舊的apk對映檔案
* 警告:
* 你一定要小心,他講影響正常的構建
* 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()
/**
* 可選的,預設為null
* 他從R.txt中很好的儲存資源id,減少java的變化
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* 必要的,預設為null
* 因為我們不想在執行時用md5檢查基礎的apk檔案(因為太慢了)
* 當嘗試應用補丁時,tinkerId來識別獨特的基礎apk
* 我們用gif,svn或者簡單的版本控制,我們將在你的清單中自動建立tinkerId
* 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 = "1.0"
/**
* 如果它為true,他將參考舊的apk
* 開啟他可以減少dex檔案的大小
* 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 {
/**
* 可選的,預設為jar
* 只能為raw或者jar,
* optional,default 'jar',為raw時,我們保持原來的格式
* 為jar時,我們會重新打包dexes和zip
* 如果你的support小於14,你必須使用jar
* 或者你想檢查更快的話,你必須使用raw
* 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"
/**
* 必要的,預設為[]
* dex的apk預計處理或修補補丁麼?
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* 必要的,預設為[]
* 警告,這是非常非常重要的,載入classes時不能改變補丁,因此,他們講從補丁dex中移除
* 你必須把這些類放入主的dex
* 簡單,你應該新增自己的application,自己的tinkerloader和自己使用的classes
* 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 {
/**
* 可選的,預設[]
*apk預處理修復補丁
* 在assets的library中,我們就會在補丁目錄中恢復他們
* 你可以在tinker中得到他的結果
* 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 {
/**
* 可選的,預設[]
* apk中資原始檔預計處理修理補丁
* 在這個apk中,你必須包括你的所有資源,否則,他們不會重新打包到新的apk中
* 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"]
/**
* 預設1000kb
* 為了資源修改,如果它比‘largemodsize’大,我們會使用bsdiff演算法去減少補丁檔案的大小
* 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 {
/**
* 可選的,預設..........
* 包檔案的gen路徑是在assets/package_meta.txt 的補丁檔案
* 你可以似乎用安全檢查。在你的包中檢查方法或者在載入結果中獲取包的屬性
* 我們將會從舊的apk中得到tinkerID,其他配置檔案(如補丁訊息下面)是不必要的
* 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")
/**
* 只是一個正常的情況下,你可以用sdk版本,渠道....
* 你可以在樣本補丁監聽器解析它,然後你就有條件可以使用補丁
* 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")
/**
* 如果你不使用zip,我們只是嘗試使用7za
* 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
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("MMdd-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"
}
}
}
}
}
}
差不多就這樣,原諒我蹩腳的英文,為了讓自己搞明白,翻譯了一個下午= = 翻譯好難。。。一般來說TinkerId是你軟體當前的版本號。然後我們進行sync now。build success。binggo搞定了。
這是我們需要進行gradle打包。具體怎麼打包,我們後面說,打包完成之後會在你的app/build/outputs/tinkerPatch生成一些檔案。
檔名 | 描述 |
---|---|
patch_unsigned.apk | 沒有簽名的補丁包 |
patch_signed.apk | 簽名後的補丁包 |
patch_signed_7zip.apk | 簽名後並使用7zip壓縮的補丁包,也是我們通常使用的補丁包。但正式釋出的時候,最好不要以.apk 結尾,防止被運營商挾持。 |
log.txt | 在編譯補丁包過程的控制檯日誌 |
dex_log.txt | 在編譯補丁包過程關於dex的日誌 |
so_log.txt | 在編譯補丁包過程關於lib的日誌 |
tinker_result | 最終在補丁包的內容,包括diff的dex、lib以及assets下面的meta檔案 |
resources_out.zip | 最終在手機上合成的全量資源apk,你可以在這裡檢視是否有檔案遺漏 |
resources_out_7z.zip | 根據7zip最終在手機上合成的全量資源apk |
tempPatchedDexes | 在Dalvik與Art平臺,最終在手機上合成的完整Dex,我們可以在這裡檢視dex合成的產物。 |
這些都是官方的介紹。我記得當時我不會gradle打包的時候,執行完成後發現並沒有那些路徑。最後你拿在app/build/bakApk的old.apk和在app/build/outputs/tinkerPatch的new.apk進行對比。執行如下:
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
關於tinker_config.xml配置看官方介紹:tinker_config.xml
在執行命令前別忘了我們需要在AndroidMainfest.xml進行配置thinkerId:
<meta-data android:name="TINKER_ID" android:value="版本號"/>
最後我們進行載入就行了。載入方式我們下面會說。
Gradle接入
對於gradle的接入,官方還是比較推薦的,不過因為有很多人因為懶所以進行命令列接入。其實如果命令列接入你還不如用hotfix。下面我們繼續說下如何進行gradle接入。至於build.gradle的配置和命令列沒有什麼區別。所以我就不說第二遍 了。下面我們具體來看程式碼。
我們需要在Application進行初始化:
/**
* com.angel.MyApplication才是真正的App,Tinker會自動生成一個
*/
@DefaultLifeCycle(application = "com.angel.MyApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class MyApplication extends DefaultApplicationLike {
private static final String APATCH_PATH = "/test.apk";
public MyApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
public void onBaseContextAttached(Context context) {
super.onBaseContextAttached(context);
//you must install multiDex whatever tinker is installed!
MultiDex.install(context);
TinkerInstaller.install(this);
}
}
MainActivity很簡單,我們來用一個button進行模擬熱修復:
public void click(View view) {
// textView.setText("一個不小心就被我熱修復了。");
String patchFileString = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
File file = new File(patchFileString);
if (file.exists()) {
TinkerInstaller.onReceiveUpgradePatch(getApplication(), patchFileString);
Log.i("TAG", "補丁包存在>>>>" + patchFileString);
} else {
Log.i("TAG", "補丁包不存在");
}
}
執行完如下:
我們點選按鈕之後會發現,嗯哼,補丁包不存在。這就對了。
現在我們需要修改一些配置:
把gradle裡的配置成bakApk下的時間。配置完再說。
好了。配置完成。下面是重點了,gradle的打包。跟著我的圖,一步兩步慢動作,兩步一步慢動作重播。
當然至於debug和relase就隨便你了哈。
這樣我們進行打包完成了。然後我們會在這個路徑:app\build\outputs\tinkerPatch\debug生成對應的檔案。這個我前面講過了。然後我們把包扔到你指定的目錄中,例如我這邊是根目錄。就直接扔進去。然後我們來點選按鈕。(無需重啟點選)。會列印如下內容:
成功了。然後他會直接閃退,官方所說的重啟我是沒碰到過。進入後我們在次點選按鈕。最後效果如下:
這樣修復就算完成了。
爬坑Tinker
前面我留了一個坑,不知道已經會的是否發現,我剛才只說把包扔到目錄裡。但我們的包有3個,到底是哪個。你們一個個的試會發現只有7z包列印的時候會是isSuccess:ture
而其他兩個全是false。至於為什麼,我也並不瞭解。我記得官方好像沒給答案。當然還有剛才說的他不會重啟。這tm是個很尷尬的事啊~我也沒搞懂為什麼。
說說我之前遇到的幾個坑把,怎麼說呢,其實並不算坑,只是平時碼程式碼的一些細節。一種情況是第一次沒加讀取許可權,第二次你加了之後是沒有用的,因為他要在第一個上進行讀取進行修復的。導致返回碼是-2。特地去官方看了這是因為補丁包不存在。我當時就矇蔽了。好吧,自己細節沒有注意= =
第二個自然就是gradle的接入了。我記得好像官方說是利用gradle進行打包。其實我並不懂= = 看來還是對gradle不熟悉哈。
總結
Tinker的爬坑之路算是結束了。其實發現只要配置完了,後面很簡單,主要還是配置的坑比較多。對比其他的熱修復,tinker肯定是首選,如果想簡單不改程式碼的話用hotfix也可以。不過還是建議看下QZone和美團的原理。聽說會讓你容易理解模組化的~~tinker的使用基本就這樣了。歡迎入坑,當然有問題可以一起來爬坑。
相關推薦
Tinker爬坑之路
目的 熱修復去年年底出的時候,變成了今年最火的技術之一。依舊記得去年面試的時候統一的MVP,然而今年卻變成了RN,熱修復。這不得不導致我們需要隨時掌握最新的技術。不然可能隨時會被淘汰。記得剛進公司,技術總監就給我們佈置了學習RN和熱修復的任務。不過因為臨時調整
Tinker + Bugly + Jenkins 爬坑之路
eset down 文件中 text 空間 ack dsi In 無需 前陣子 Android 端的線上崩潰比較多,熱修復被提上日程。實現方案是 Tinker,Jenkins 打包,最後補丁包上傳到 Bugly 進行分發。主要在 Jenkins 打包這一塊爬了不少坑,現記
angular4之爬坑之路(2)angular腳手引入第三方類庫
scrip 1-1 sta 引入 logs .json 類比 ima 第三方 如何在angular4腳手架中引入第三方類庫呢比如jquery、swiper、bootstrap。。。。。。 例如引入jquery:(其他類庫類比jquery即可) 第一步:在我們的項目目錄下
react爬坑之路(一)--報錯output.path不是絕對路徑
bpa file 文件 開始 put pac 這就是 文件頭部 之前 之前,一直在糾結是學習angular好,學習vue好,還是學習react好,網上一搜索,也是各種對比,各種互噴,看過之後更糾結。就跟小時候一樣糾結長大了是上清華好,還是上北大好,最後證明我想多了。總之
爬坑之路——用Fuel部署Openstack
電源 並且 blog src 下載 百度雲 pro open 組件 前段時間看到這個NASA發起這個開源的雲計算管理平臺項目,挺感興趣的。索性看了一些概念和原理就準備操作了。但是網上搭建完整版openstack組件的文檔,要不是網路源有問題就是環境有問題。很難流暢的安裝。我
Vue 爬坑之路(一)—— 使用 vue-cli 搭建項目 (增補)
web http class 模板 clas ebp 搭建 com png cd 指定好安裝目錄 vue init webpack 項目名稱 執行 vue vue list 查看可應用模板 vue init webpack +名字
Vue 爬坑之路(四)—— 與 Vuex 的第一次接觸
參數 之間 scrip span 變量 tle 技術分享 管理 tool 在 Vue.js 的項目中,如果項目結構簡單, 父子組件之間的數據傳遞可以使用 props 或者 $emit 等方式 http://www.cnblogs.com/wisewrong/p/62660
Vue 爬坑之路(一)—— 使用 vue-cli 搭建項目
wrong run 自己 文件的 ima tca 來安 mage 前綴 vue-cli 是一個官方發布 vue.js 項目腳手架,使用 vue-cli 可以快速創建 vue 項目,GitHub地址是:https://github.com/vuejs/vue-cli 一、
Vue 爬坑之路(二)—— 組件之間的數據傳遞
組件 圖片 NPU tar bubuko 9.png com clas entry Vue 的組件作用域都是孤立的,不允許在子組件的模板內直接引用父組件的數據。必須使用特定的方法才能實現組件之間的數據傳遞。 首先用 vue-cli 創建一個項目,其中 App.vue 是父組
Vue 爬坑之路(三)—— 使用 vue-router 跳轉頁面
cli 分類 ace local outer log 適合 創建 start 使用 Vue.js 做項目的時候,一個頁面是由多個組件構成的,所以在跳轉頁面的時候,並不適合用傳統的 href,於是 vue-router 應運而生。 官方文檔: https://router.v
vue的爬坑之路(八)之----IOS:Safari不兼容Javascript中的Date問題
找到 () 方案 ace 格式 nbsp 後臺 get repl 在IOS5以上版本(不包含IOS5)中的Safari瀏覽器能正確解釋出Javascript中的 new Date(‘2013-10-21‘) 的日期對象。 但是在IOS5版本裏面的Safar
新手pythoner爬坑之路
sort pri 數字 次數 添加 .so 定位 rem pytho 列表的各種基本操作 fruits_list=[‘蘋果‘,‘梨子‘,‘香蕉‘,‘西紅柿‘] print(fruits_list[1:3])#列表的切片,list[a:b:c]a,b表示切片的始末,c表示
Vue 爬坑之路(十二)—— vue-cli 3.x 搭建項目
cas sas 環境變量 npm pack 全局 開發模式 文件 預編譯 Vue Cli 3 官方文檔:https://cli.vuejs.org/zh/guide/ 一、安裝 @vue/cli 更新到 3.x 之後,vue-cli 的包名從 vue-cli 改
java爬坑之路(一)——編譯可以正常編譯,執行時報unsupported major.minor version 52.0
寫了一個很簡單的helloworld,編譯可以正常編譯,執行時報unsupported major.minor version 52.0錯誤。 java程式碼如下: 用javac Hello.java命令編譯時,是沒有問題的,用java Hello執行時,怎麼也執行不了 後
Vue 爬坑之路 — vue-cli 3.x 搭建專案
一、安裝 @vue/cli 更新到 3.x 之後,vue-cli 的包名從vue-cli 改成了 @vue/cli 如果之前全域性安裝了舊版本的vue-cli (1.x 或 2.x),首先需要使用以下命令解除安裝掉 // 如果沒有安裝舊版本的 vue-cli 可以跳過解除安裝直接
專案實戰之vue爬坑之路:vue框架中如何註冊全域性公共過濾器filter、全域性公共外掛、全域性公共元件component
專案需求: 在我們的實際專案的開發中,經常會遇到一些可以重複利用的方法和元件,比如:我們在一個專案中可能會經常用到格式化時間的方法。這個時候就需要我們把這個方法封裝起來,註冊成一個全域性的過濾器,在整個專案中都可以使用,這樣既可以提高程式碼的使用效率,也便於我們
vue+ele 爬坑之路(一)—class
上手Vue已將近三月,在這裡記錄下我自己遇到的一些問題吧! class名的修改 1、第一種就是直接繫結 <div class="main-header"></div> 這種樣式方便大家直接給標籤加樣式,不足之處在於無法動態控制class 2、動
vue+ele 爬坑之路tree(二)
後臺管理系統中,我們有個很必需的功能,那就是組織機構樹,我當時用這個的時候,不小心陷入了個誤區,導致我走了很多彎路,在這裡我就分享下我遇到的問題吧,希望和我一樣遇到這個問題的人可以一起探討下 在使用的時候,因為要區分一級和二級選單許可權,所以也有不同的判斷,其他的不多
Mint-ui infinite scroll 無限載入爬坑之路
問題一:下拉不會觸發loadMore事件 解決:我們的ul或者設定滾動的元素必須設定高度和溢位滾動的屬性 因為我的介面沒有分頁所以用slice模擬資料分頁載入 CSS程式碼: .newsList { max-height: 35rem;
Vue 爬坑之路(二)—— 元件之間的資料傳遞
Vue 的元件作用域都是孤立的,不允許在子元件的模板內直接引用父元件的資料。必須使用特定的方法才能實現元件之間的資料傳遞。 首先用 vue-cli 建立一個專案,其中 App.vue 是父元件,components 資料夾下都是子元件。 一、父元件向子元件傳遞資料