簡(jiǎn)介
Tinker是適用于Android的修補(bǔ)程序庫(kù)解取,它支持dex溢谤,庫(kù)和資源更新媳危,而無(wú)需重新安裝apk络凿。更新完成后重新啟動(dòng)即可
Tinker github官方地址
添加依賴
1.app的build.gradle
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
}
}
2.在module的gradle中
dependencies {
//optional, help to generate the final application
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//tinker's main Android lib
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
Gradle版本大于2.3的這么寫(xiě)
dependencies {
implementation "com.android.support:multidex:1.0.1"
//tinker的核心庫(kù)
implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }
//可選骡送,用于生成application類
annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
}
...
開(kāi)啟multiDex
defaultConfig {
...
multiDexEnabled true
}
...
最后別忘了添加apply
apply plugin: 'com.tencent.tinker.patch'
Tinker的配置及任務(wù)
1. Tinker文檔中推薦將jumboMode設(shè)置為true
android {
dexOptions {
// 支持大工程模式
jumboMode = true
}
...
}
2 配置Tinker與任務(wù)
將下面的配置全部復(fù)制粘貼到app的gradle文件(app/build.gradle)末尾昂羡,內(nèi)容很多,但現(xiàn)在只需要看懂bakPath與ext括號(hào)內(nèi)的東東就好了摔踱。
// Tinker配置與任務(wù)
def bakPath = file("${buildDir}/bakApk/")
ext {
// 是否使用Tinker(當(dāng)你的項(xiàng)目處于開(kāi)發(fā)調(diào)試階段時(shí)虐先,可以改為false)
tinkerEnabled = true
// 基礎(chǔ)包文件路徑(名字這里寫(xiě)死為old-app.apk。用于比較新舊app以生成補(bǔ)丁包派敷,不管是debug還是release編譯)
tinkerOldApkPath = "${bakPath}/old-app.apk"
// 基礎(chǔ)包的mapping.txt文件路徑(用于輔助混淆補(bǔ)丁包的生成赴穗,一般在生成release版app時(shí)會(huì)使用到混淆,所以這個(gè)mapping.txt文件一般只是用于release安裝包補(bǔ)丁的生成)
tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
// 基礎(chǔ)包的R.txt文件路徑(如果你的安裝包中資源文件有改動(dòng)膀息,則需要使用該R.txt文件來(lái)輔助生成補(bǔ)丁包)
tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}
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 : android.defaultConfig.versionName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
// 全局信息相關(guān)的配置項(xiàng)
tinkerPatch {
tinkerEnable = buildWithTinker()// 是否打開(kāi)tinker的功能般眉。
oldApk = getOldApkPath() // 基準(zhǔn)apk包的路徑,必須輸入潜支,否則會(huì)報(bào)錯(cuò)甸赃。
ignoreWarning = false // 是否忽略有風(fēng)險(xiǎn)的補(bǔ)丁包。這里選擇不忽略冗酿,當(dāng)補(bǔ)丁包風(fēng)險(xiǎn)時(shí)會(huì)中斷編譯埠对。
useSign = true // 在運(yùn)行過(guò)程中,我們需要驗(yàn)證基準(zhǔn)apk包與補(bǔ)丁包的簽名是否一致裁替,我們是否需要為你簽名项玛。
// 編譯相關(guān)的配置項(xiàng)
buildConfig {
applyMapping = getApplyMappingPath()
// 可選參數(shù);在編譯新的apk時(shí)候弱判,我們希望通過(guò)保持舊apk的proguard混淆方式襟沮,從而減少補(bǔ)丁包的大小。這個(gè)只是推薦設(shè)置昌腰,不設(shè)置applyMapping也不會(huì)影響任何的assemble編譯开伏。
applyResourceMapping = getApplyResourceMappingPath()
// 可選參數(shù);在編譯新的apk時(shí)候遭商,我們希望通過(guò)舊apk的R.txt文件保持ResId的分配固灵,這樣不僅可以減少補(bǔ)丁包的大小,同時(shí)也避免由于ResId改變導(dǎo)致remote view異常劫流。
tinkerId = getTinkerIdValue()
// 在運(yùn)行過(guò)程中巫玻,我們需要驗(yàn)證基準(zhǔn)apk包的tinkerId是否等于補(bǔ)丁包的tinkerId。這個(gè)是決定補(bǔ)丁包能運(yùn)行在哪些基準(zhǔn)包上面祠汇,一般來(lái)說(shuō)我們可以使用git版本號(hào)仍秤、versionName等等。
keepDexApply = false
// 如果我們有多個(gè)dex,編譯補(bǔ)丁時(shí)可能會(huì)由于類的移動(dòng)導(dǎo)致變更增多座哩。若打開(kāi)keepDexApply模式徒扶,補(bǔ)丁包將根據(jù)基準(zhǔn)包的類分布來(lái)編譯粮彤。
isProtectedApp = false // 是否使用加固模式根穷,僅僅將變更的類合成補(bǔ)丁姜骡。注意,這種模式僅僅可以用于加固應(yīng)用中屿良。
supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本開(kāi)始才有的新功能)
}
// dex相關(guān)的配置項(xiàng)
dex {
dexMode = "jar"
// 只能是'raw'或者'jar'圈澈。 對(duì)于'raw'模式,我們將會(huì)保持輸入dex的格式尘惧。對(duì)于'jar'模式康栈,我們將會(huì)把輸入dex重新壓縮封裝到j(luò)ar。如果你的minSdkVersion小于14喷橙,你必須選擇‘jar’模式啥么,而且它更省存儲(chǔ)空間,但是驗(yàn)證md5時(shí)比'raw'模式耗時(shí)贰逾。默認(rèn)我們并不會(huì)去校驗(yàn)md5,一般情況下選擇jar模式即可悬荣。
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
// 需要處理dex路徑,支持*疙剑、?通配符氯迂,必須使用'/'分割。路徑是相對(duì)安裝包的言缤,例如assets/...
loader = [
// 定義哪些類在加載補(bǔ)丁包的時(shí)候會(huì)用到嚼蚀。這些類是通過(guò)Tinker無(wú)法修改的類,也是一定要放在main dex的類管挟。
// 如果你自定義了TinkerLoader轿曙,需要將它以及它引用的所有類也加入loader中;
// 其他一些你不希望被更改的類僻孝,例如Sample中的BaseBuildInfo類拳芙。這里需要注意的是,這些類的直接引用類也需要加入到loader中皮璧≈墼或者你需要將這個(gè)類變成非preverify。
]
}
// lib相關(guān)的配置項(xiàng)
lib {
pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
// 需要處理lib路徑悴务,支持*睹限、?通配符,必須使用'/'分割讯檐。與dex.pattern一致, 路徑是相對(duì)安裝包的羡疗,例如assets/...
}
// res相關(guān)的配置項(xiàng)
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
// 需要處理res路徑,支持*别洪、?通配符叨恨,必須使用'/'分割。與dex.pattern一致, 路徑是相對(duì)安裝包的挖垛,例如assets/...痒钝,務(wù)必注意的是秉颗,只有滿足pattern的資源才會(huì)放到合成后的資源包。
ignoreChange = [
// 支持*送矩、?通配符蚕甥,必須使用'/'分割。若滿足ignoreChange的pattern栋荸,在編譯時(shí)會(huì)忽略該文件的新增菇怀、刪除與修改。 最極端的情況晌块,ignoreChange與上面的pattern一致爱沟,即會(huì)完全忽略所有資源的修改。
"assets/sample_meta.txt"
]
largeModSize = 100
// 對(duì)于修改的資源匆背,如果大于largeModSize钥顽,我們將使用bsdiff算法。這可以降低補(bǔ)丁包的大小靠汁,但是會(huì)增加合成時(shí)的復(fù)雜度蜂大。默認(rèn)大小為100kb
}
// 用于生成補(bǔ)丁包中的'package_meta.txt'文件
packageConfig {
// configField("key", "value"), 默認(rèn)我們自動(dòng)從基準(zhǔn)安裝包與新安裝包的Manifest中讀取tinkerId,并自動(dòng)寫(xiě)入configField。
// 在這里蝶怔,你可以定義其他的信息奶浦,在運(yùn)行時(shí)可以通過(guò)TinkerLoadResult.getPackageConfigByName得到相應(yīng)的數(shù)值。
// 但是建議直接通過(guò)修改代碼來(lái)實(shí)現(xiàn)踢星,例如BuildConfig澳叉。
configField("platform", "all")
configField("patchVersion", "1.0")
// configField("patchMessage", "tinker is sample to use")
}
// 7zip路徑配置項(xiàng),執(zhí)行前提是useSign為true
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
def date = new Date().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
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.first().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"
}
}
}
}
}
}
其中沐悦,有幾點(diǎn)配置在這里說(shuō)明一下成洗,方便理解后續(xù)的操作(當(dāng)tinkerEnabled = true的情況下):
- app的生成目錄是:主Module(一般是名為app)/build/bakApk文件夾。
- 補(bǔ)丁包的生成路徑:主Module(一般是名為app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk藏否。
- 基礎(chǔ)包的名字:old-app.apk瓶殃,放于bakApk文件夾下。
- 基礎(chǔ)包的mapping.txt和R.txt文件一般在編譯release簽名的apk時(shí)才會(huì)用到副签。
- 在用到mapping.txt文件時(shí)遥椿,需要重命名為old-app-mapping.txt,放于bakApk文件夾下淆储。
- 在用到R.txt文件時(shí)冠场,需要重命名為old-app-R.txt,放于bakApk文件夾下本砰。
Tinker準(zhǔn)備工作
1. 添加一些類
1.這里面有一些我找到的文件碴裙,需要加到工程中,當(dāng)然這些工具類也可以自己寫(xiě)
這些類的作用大概如下
SampleUncaughtExceptionHandler:Tinker的全局異常捕獲器。
MyLogImp:Tinker的日志輸出實(shí)現(xiàn)類舔株。
SampleLoadReporter:加載補(bǔ)丁時(shí)的一些回調(diào)莺琳。
SamplePatchListener:過(guò)濾Tinker收到的補(bǔ)丁包的修復(fù)、升級(jí)請(qǐng)求督笆。
SamplePatchReporter:修復(fù)或者升級(jí)補(bǔ)丁時(shí)的一些回調(diào)。
SampleTinkerReport:修復(fù)結(jié)果(成功诱贿、沖突娃肿、失敗等)。
SampleResultService::patch補(bǔ)丁合成進(jìn)程將合成結(jié)果返回給主進(jìn)程的類珠十。
TinkerManager:Tinker管理器(安裝料扰、初始化Tinker)。
TinkerUtils:拓展補(bǔ)丁條件判定焙蹭、鎖屏或后臺(tái)時(shí)應(yīng)用重啟功能的工具類晒杈。
對(duì)于這些自定義類及錯(cuò)誤碼的詳細(xì)說(shuō)明,請(qǐng)參考:「Tinker官方Wiki:可選的自定義類」
2. 清單文件中添加服務(wù)
<service
android:name="com.lqr.tinker.service.SampleResultService"
android:exported="false"/>
編寫(xiě)Application代理
Tinker表示孔厉,Application無(wú)法動(dòng)態(tài)修復(fù)拯钻,所以有兩種選擇:
- 使用「繼承TinkerApplication + DefaultApplicationLike」。
- 使用「DefaultLifeCycle注解 + DefaultApplicationLike」撰豺。
我們?cè)谶@里使用第二種方式
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application類名粪般。只能用字符串,這個(gè)MyApplication文件是不存在的污桦,但可以在AndroidManifest.xml的application標(biāo)簽上使用(name)
flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我們這里使用默認(rèn)即可!(可不寫(xiě))
loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {
private Application mApplication;
private Context mContext;
private Tinker mTinker;
// 固定寫(xiě)法
public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
// 固定寫(xiě)法
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
mApplication = getApplication();
mContext = getApplication();
initTinker(base);
// 可以將之前自定義的Application中onCreate()方法所執(zhí)行的操作搬到這里...
}
private void initTinker(Context base) {
// tinker需要你開(kāi)啟MultiDex
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
// 設(shè)置全局異常捕獲
TinkerManager.initFastCrashProtect();
//開(kāi)啟升級(jí)重試功能(在安裝Tinker之前設(shè)置)
TinkerManager.setUpgradeRetryEnable(true);
//設(shè)置Tinker日志輸出類
TinkerInstaller.setLogIml(new MyLogImp());
//安裝Tinker(在加載完multiDex之后亩歹,否則你需要將com.tencent.tinker.**手動(dòng)放到main dex中)
TinkerManager.installTinker(this);
mTinker = Tinker.with(getApplication());
}
}
在Application中添加TinkerApplicationLike
public class TinkerApplicationLike extends DefaultApplicationLike {
...
@Override
public void onCreate() {
super.onCreate();
// 將之前自定義的Application中onCreate()方法所執(zhí)行的操作搬到這里...
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
mApplication = getApplication();
mContext = getApplication();
initTinker(base);
// 或搬到這里...
}
}
使用TinkerApplication
通過(guò)上面的注釋,MyApplication會(huì)自動(dòng)生成凡橱,我使用了自定義的Application使用方法如下
public class MyApplicatoin extends TinkerApplication {
public MyApplicatoin(){
super(ShareConstants.TINKER_ENABLE_ALL,TinkerApplicationLike.class.getName(),TinkerLoader.class.getName(),false);
}
}
Tinker的使用
1.編譯基礎(chǔ)包
在Terminal中使用命令行./gradlew assembleDebug之后再module的build目錄下會(huì)有個(gè)bakApk目錄小作,在這目錄中就會(huì)生成基礎(chǔ)包
其實(shí)在配置完Tinker的配置后,make項(xiàng)目稼钩,就看這個(gè)目錄下已經(jīng)存在基礎(chǔ)包了
如果這個(gè)apk文件是release簽名且是要放到應(yīng)用市場(chǎng)上的顾稀,那么你必須將apk與R.txt(如果有使用混淆的話,還會(huì)有一個(gè)mapping.txt)這幾個(gè)文件保存好坝撑,切記础拨。
2.制作補(bǔ)丁包
將要修復(fù)的基礎(chǔ)包apk重命名為old-app.apk
點(diǎn)擊tinkerPatchDebug
在output文件夾中生成分叉包
在這里我遇到了一個(gè)問(wèn)題
在編譯的時(shí)候補(bǔ)丁包的時(shí)候總是不成功報(bào)
com.tencent.tinker.build.util.TinkerPatchException:
解決方法如下
3.將生成的補(bǔ)丁包patch_signed_7zip.apk放置指定目錄下
執(zhí)行TinkerInstaller.onReceiveUpgradePatch方法進(jìn)行安裝
之后成功修復(fù)
以上步驟可以修復(fù)java、so 绍载、資源文件
加載SO注意的地方
上圖是Tinker官網(wǎng)Wiki的文檔部分截圖诡宗,從紅線部分可以知道,因?yàn)椴糠质謾C(jī)判斷abi并不準(zhǔn)確(可能因?yàn)锳ndroid碎片化比較嚴(yán)重吧)击儡,Tinker沒(méi)有區(qū)分abi塔沃,自然也不會(huì)在app啟動(dòng)時(shí),自動(dòng)加載對(duì)應(yīng)的so庫(kù)阳谍,這需要開(kāi)發(fā)者自己判斷蛀柴。
需要調(diào)用如下方法
public void load_library_hack(View view) {
String CPU_ABI = android.os.Build.CPU_ABI;
// 將tinker library中的 CPU_ABI架構(gòu)的so 注冊(cè)到系統(tǒng)的library path中螃概。
TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}
常用API
1、請(qǐng)求打補(bǔ)丁
TinkerInstaller.onReceiveUpgradePatch(context, 補(bǔ)丁包的本地路徑);
2鸽疾、卸載補(bǔ)丁
Tinker.with(getApplicationContext()).cleanPatch();// 卸載所有的補(bǔ)丁
Tinker.with(getApplicationContext()).cleanPatchByVersion(版本號(hào))// 卸載指定版本的補(bǔ)丁
復(fù)制代碼
3吊洼、殺死應(yīng)用的其他進(jìn)程
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
復(fù)制代碼
4、Hack方式修復(fù)so
TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
abi:cpu架構(gòu)類型
5制肮、非Hack方式修復(fù)so
TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so庫(kù)的模塊名); // 加載任意abi庫(kù)
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so庫(kù)的模塊名); // 只適用于加載armeabi庫(kù)
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so庫(kù)的模塊名); // 只適用于加載armeabi-v7a庫(kù)
復(fù)制代碼
loadArmLibrary()與loadArmV7Library()本質(zhì)是調(diào)用了loadLibraryFromTinker()冒窍,有興趣的可以查看下源碼。
對(duì)于Tinker所有API的詳細(xì)說(shuō)明豺鼻,請(qǐng)參考:「Tinker官方Wiki:Tinker-API概覽」综液。
Tinker原理
Tinker的原理,類加載方案基于Dex分包方案儒飒,什么是Dex分包方案呢谬莹?這個(gè)得先從65536限制和LinearAlloc限制說(shuō)起。
65536限制
隨著應(yīng)用功能越來(lái)越復(fù)雜桩了,代碼量不斷地增大附帽,引入的庫(kù)也越來(lái)越多,可能會(huì)在編譯時(shí)提示如下異常:
com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
這說(shuō)明應(yīng)用中引用的方法數(shù)超過(guò)了最大數(shù)65536個(gè)井誉。產(chǎn)生這一問(wèn)題的原因就是系統(tǒng)的65536限制士葫,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法調(diào)用指令invoke-kind索引為16bits送悔,最多能引用 65535個(gè)方法慢显。
LinearAlloc限制
在安裝時(shí)可能會(huì)提示INSTALL_FAILED_DEXOPT。產(chǎn)生的原因就是LinearAlloc限制欠啤,DVM中的LinearAlloc是一個(gè)固定的緩存區(qū)荚藻,當(dāng)方法數(shù)過(guò)多超出了緩存區(qū)的大小時(shí)會(huì)報(bào)錯(cuò)。
為了解決65536限制和LinearAlloc限制洁段,從而產(chǎn)生了Dex分包方案应狱。Dex分包方案主要做的是在打包時(shí)將應(yīng)用代碼分成多個(gè)Dex,將應(yīng)用啟動(dòng)時(shí)必須用到的類和這些類的直接引用類放到主Dex中祠丝,其他代碼放到次Dex中疾呻。當(dāng)應(yīng)用啟動(dòng)時(shí)先加載主Dex,等到應(yīng)用啟動(dòng)后再動(dòng)態(tài)的加載次Dex写半,從而緩解了主Dex的65536限制和LinearAlloc限制岸蜗。
在ClassLoader的加載過(guò)程,其中一個(gè)環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法叠蝇,如下所示璃岳。
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element內(nèi)部封裝了DexFile,DexFile用于加載dex文件,因此每個(gè)dex文件對(duì)應(yīng)一個(gè)Element铃慷。
多個(gè)Element組成了有序的Element數(shù)組dexElements单芜。當(dāng)要查找類時(shí),會(huì)在注釋1處遍歷Element數(shù)組dexElements(相當(dāng)于遍歷dex文件數(shù)組)犁柜,注釋2處調(diào)用Element的findClass方法洲鸠,其方法內(nèi)部會(huì)調(diào)用DexFile的loadClassBinaryName方法查找類。如果在Element中(dex文件)找到了該類就返回馋缅,如果沒(méi)有找到就接著在下一個(gè)Element中進(jìn)行查找扒腕。
根據(jù)上面的查找流程,我們將有bug的類Key.class進(jìn)行修改股囊,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar袜匿,放在Element數(shù)組dexElements的第一個(gè)元素更啄,這樣會(huì)首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class稚疹,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委托模式就不會(huì)被加載,這就是類加載方案祭务,如下圖所示内狗。
類加載方案需要重啟App后讓ClassLoader重新加載新的類,為什么需要重啟呢义锥?這是因?yàn)轭愂菬o(wú)法被卸載的柳沙,因此要想重新加載新的類就需要重啟App,因此采用類加載方案的熱修復(fù)框架是不能即時(shí)生效的拌倍。
雖然很多熱修復(fù)框架采用了類加載方案赂鲤,但具體的實(shí)現(xiàn)細(xì)節(jié)和步驟還是有一些區(qū)別的,比如QQ空間的超級(jí)補(bǔ)丁和Nuwa是按照上面說(shuō)得將補(bǔ)丁包放在Element數(shù)組的第一個(gè)元素得到優(yōu)先加載柱恤。微信Tinker將新舊apk做了diff,得到patch.dex梗顺,然后將patch.dex與手機(jī)中apk的classes.dex做合并泡孩,生成新的classes.dex,然后在運(yùn)行時(shí)通過(guò)反射將classes.dex放在Element數(shù)組的第一個(gè)元素寺谤。餓了么的Amigo則是將補(bǔ)丁包中每個(gè)dex 對(duì)應(yīng)的Element取出來(lái)仑鸥,之后組成新的Element數(shù)組,在運(yùn)行時(shí)通過(guò)反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組变屁。
采用類加載方案的主要是以騰訊系為主眼俊,包括微信的Tinker、QQ空間的超級(jí)補(bǔ)丁粟关、手機(jī)QQ的QFix泵琳、餓了么的Amigo和Nuwa等等。