本篇文章基于Android Q 和 Tinker 1.9.14.7初肉。
經(jīng)過(guò)前面的分析,已經(jīng)初步了解了Tinker的整個(gè)執(zhí)行流程,對(duì)整個(gè)脈絡(luò)有了清晰的認(rèn)識(shí)。那么本篇文章就來(lái)深挖一個(gè)點(diǎn)漠秋,從加載dex補(bǔ)丁看Tinker動(dòng)態(tài)加載插件過(guò)程。
首先來(lái)回顧Tinker加載dex補(bǔ)丁過(guò)程
整個(gè)dex補(bǔ)丁加載過(guò)程分兩部分:
如果系統(tǒng)ota升級(jí)后抵屿,因?yàn)閛dex失效了庆锦,所以Tinker自己會(huì)觸發(fā)一次dex加載,而這個(gè)加載過(guò)程在Q之前是會(huì)走dex2oat編譯的轧葛,Q開(kāi)始如果odex失效搂抒,當(dāng)前l(fā)oadDex過(guò)程不進(jìn)行dex2oat編譯,轉(zhuǎn)而走解釋模式執(zhí)行尿扯。另外求晶,這里區(qū)分了版本,對(duì)25及以上的版本使用PathClassLoader來(lái)加載衷笋,而低版本直接使用DexFile.loadDex芳杏。
dex補(bǔ)丁包加載實(shí)際上是通過(guò)hook的方式將修復(fù)后的dex插入到的DexPathList的Elements[]中最前面去,從而替代之前的基準(zhǔn)包辟宗,替代原理是雙親委派和類(lèi)緩存保證了相同類(lèi)不能被重復(fù)加載爵赵,前面加載過(guò)了后面的dex就失效了,從而達(dá)到替換的目的泊脐。
那么本篇文章主要分析如下三點(diǎn)問(wèn)題:
- PathClassLoader與DexFile.loadDex加載類(lèi)的區(qū)別
- dex加載過(guò)程
- Apk加載過(guò)程與Tinker動(dòng)態(tài)加載dex補(bǔ)丁包整體順序流程
一空幻、PathClassLoader與DexFile.loadDex加載類(lèi)的區(qū)別
流程主要包含兩個(gè)部分:
PathClassLoader經(jīng)過(guò)一系列初始化,最終由DexPathList觸發(fā)DexFile.loadDex來(lái)加載dex容客。
通過(guò)report將插件路徑上報(bào)到DexManager統(tǒng)一管理氛悬。
乍一看,這兩種方式本質(zhì)上只有有個(gè)上報(bào)的差別耘柱,那么這個(gè)上報(bào)到底有什么意義如捅?另外,在之前老版本Tinker中调煎,這里統(tǒng)一是由DexFile.loadDex來(lái)加載的镜遣,之后為什么新增PathClassLoader加載方式?所以就很有必要研究下上報(bào)是干了什么。
在ActivityThread handleBindApplication會(huì)通過(guò)LoadedApk進(jìn)行Apk加載操作悲关,在這里會(huì)設(shè)置BaseDexClassLoader的reporter:
if (SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false)) {
BaseDexClassLoader.setReporter(DexLoadReporter.getInstance());
}
看看這個(gè)系統(tǒng)屬性:
cepheus:/ $ getprop | grep "dalvik.vm.usejitprofiles"
[dalvik.vm.usejitprofiles]: [true] 開(kāi)關(guān)默認(rèn)是打開(kāi)的
下面來(lái)研究下整個(gè)上報(bào)過(guò)程:
最終dexPath歸DexManager統(tǒng)一管理谎僻。系統(tǒng)有個(gè)jobService:BackgroundDexService它在SystemServer執(zhí)行startOtherServices時(shí)觸發(fā)schedule,在idle場(chǎng)景下觸發(fā)插件和主apk包的dex2oat編譯,簡(jiǎn)單流程如下圖所示:
也就是說(shuō)寓辱,如果上報(bào)了艘绍,當(dāng)前插件會(huì)被DexManager統(tǒng)一管理,在idle狀態(tài)下系統(tǒng)會(huì)通過(guò)一個(gè)jobService觸發(fā)對(duì)所有已安裝app的主apk以及插件的dex2oat的編譯秫筏,通常方式為speed-profile诱鞠。在Q之前,因?yàn)镈exFile.loadDex本身會(huì)走編譯流程这敬,所以也不需要上報(bào)之后統(tǒng)一編譯航夺,而Q之后DexFile.loadDex不走編譯流程了,一旦判斷沒(méi)有有效odex崔涂,則走解釋執(zhí)行阳掐。所以依賴(lài)上報(bào)讓系統(tǒng)幫忙來(lái)對(duì)插件做編譯。
注:使用dex2oat進(jìn)行AOT編譯的compile filter:
- verify:只運(yùn)行 DEX 代碼驗(yàn)證冷蚂。
- quicken:運(yùn)行 DEX 代碼驗(yàn)證缭保,并優(yōu)化一些 DEX 指令,以獲得更好的解釋器性能蝙茶。
- speed-profile:運(yùn)行 DEX 代碼驗(yàn)證涮俄,并對(duì)配置文件中列出的方法進(jìn)行 AOT 編譯。
- speed:運(yùn)行 DEX 代碼驗(yàn)證尸闸,并對(duì)所有方法進(jìn)行 AOT 編譯。
另外 附上系統(tǒng)觸發(fā)dex2oat編譯時(shí)機(jī):
路徑 | 描述 | 編譯方式 | 編譯內(nèi)容 |
---|---|---|---|
Install | 應(yīng)用安裝觸發(fā)的編譯 | speed-profile | 主apk |
ota | 系統(tǒng)升級(jí)觸發(fā)的編譯 | verify | 主apk |
loadDex | 動(dòng)態(tài)加載插件觸發(fā)的編譯 | quicken | 插件 |
postboot | 開(kāi)機(jī)1分鐘后孕锄,jobService觸發(fā)的編譯 | verify | 主apk |
idle&charge | 同時(shí)滿足充電吮廉、idle狀態(tài)且24小時(shí)內(nèi)只觸發(fā)一次 | speed-profile | 主apk 和 插件 |
從這表中也可以明顯看出,Q上取消了load dexFile的插件編譯畸肆,那么現(xiàn)在剩下的唯一的原生的編譯場(chǎng)景為idle&charge了宦芦。
二、dex加載過(guò)程
那么轴脐,我們接下來(lái)詳細(xì)分析下DexFile.loadDex的整體邏輯调卑。
之前先來(lái)了解下編譯相關(guān)文件:
.dex 編譯的源文件,符合android虛擬機(jī)規(guī)范的字節(jié)碼文件大咱,APK中包含一個(gè)或多個(gè)classes.dex恬涧。
.vdex 存儲(chǔ)預(yù)先驗(yàn)證的dex文件,在有效期內(nèi)能減少不必要的dex文件驗(yàn)證碴巾。
.odex 是dex2oat對(duì)dex編譯后的優(yōu)化產(chǎn)物溯捆,節(jié)省從apk中加載dex過(guò)程和編譯優(yōu)化過(guò)程,執(zhí)行速度比解釋執(zhí)行快厦瓢,但是該文件有有效期提揍,失效需要重新編譯
- .art 對(duì)熱點(diǎn)函數(shù)進(jìn)行對(duì)應(yīng)類(lèi)的預(yù)加載啤月,編譯過(guò)程會(huì)觸發(fā)預(yù)加載,預(yù)加載后的類(lèi)可以直接map到內(nèi)存中使用劳跃,不需要從dex中加載谎仲。
簡(jiǎn)單說(shuō)就是:
- .vdex 優(yōu)化dex文件的合法性驗(yàn)證。
- .odex 優(yōu)化dex文件加載刨仑。
- .art 優(yōu)化類(lèi)加載郑诺。
這里DexFile就是dex文件的一個(gè)封裝對(duì)象,這里很多細(xì)節(jié)不一一鋪開(kāi)贸人,直接到核心方法OpenDexFilesFromOat,Q之前這個(gè)方法會(huì)通過(guò)MakeUpToDate來(lái)做編譯间景,而Q上去掉了。
這里簡(jiǎn)單梳理下Q上OpenDexFilesFromOat到底做了些什么:
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat(
const char* dex_location,
jobject class_loader,
jobjectArray dex_elements,
const OatFile** out_oat_file,
std::vector<std::string>* error_msgs) {
ScopedTrace trace(__FUNCTION__);
CHECK(dex_location != nullptr);
CHECK(error_msgs != nullptr);
// Verify we aren't holding the mutator lock, which could starve GC if we
// have to generate or relocate an oat file.
Thread* const self = Thread::Current();
Locks::mutator_lock_->AssertNotHeld(self);
Runtime* const runtime = Runtime::Current();
std::unique_ptr<ClassLoaderContext> context;
// If the class_loader is null there's not much we can do. This happens if a dex files is loaded
// directly with DexFile APIs instead of using class loaders.
if (class_loader == nullptr) {
LOG(WARNING) << "Opening an oat file without a class loader. "
<< "Are you using the deprecated DexFile APIs?";
context = nullptr;
} else {
context = ClassLoaderContext::CreateContextForClassLoader(class_loader, dex_elements);
}
//初始化OatFileAssistant
OatFileAssistant oat_file_assistant(dex_location,
kRuntimeISA,
!runtime->IsAotCompiler(),
only_use_system_oat_files_);
// Get the oat file on disk.
//獲取oat file
std::unique_ptr<const OatFile> oat_file(oat_file_assistant.GetBestOatFile().release());
VLOG(oat) << "OatFileAssistant(" << dex_location << ").GetBestOatFile()="
<< reinterpret_cast<uintptr_t>(oat_file.get())
<< " (executable=" << (oat_file != nullptr ? oat_file->IsExecutable() : false) << ")";
const OatFile* source_oat_file = nullptr;
CheckCollisionResult check_collision_result = CheckCollisionResult::kPerformedHasCollisions;
std::string error_msg;
if ((class_loader != nullptr || dex_elements != nullptr) && oat_file != nullptr) {
// Prevent oat files from being loaded if no class_loader or dex_elements are provided.
// This can happen when the deprecated DexFile.<init>(String) is called directly, and it
// could load oat files without checking the classpath, which would be incorrect.
// Take the file only if it has no collisions, or we must take it because of preopting.
//odex沖突檢測(cè)
//是否提供class_loader或dex_elements,同時(shí)檢測(cè)odex文件路徑等艺智。
check_collision_result = CheckCollision(oat_file.get(), context.get(), /*out*/ &error_msg);
//odex沖突檢測(cè)通過(guò)
bool accept_oat_file = AcceptOatFile(check_collision_result);
if (!accept_oat_file) {
//如果沒(méi)通過(guò),那么原始dex文件存在也給通過(guò)
// Failed the collision check. Print warning.
if (runtime->IsDexFileFallbackEnabled()) {
if (!oat_file_assistant.HasOriginalDexFiles()) {
// We need to fallback but don't have original dex files. We have to
// fallback to opening the existing oat file. This is potentially
// unsafe so we warn about it.
accept_oat_file = true;
LOG(WARNING) << "Dex location " << dex_location << " does not seem to include dex file. "
<< "Allow oat file use. This is potentially dangerous.";
} else {
// We have to fallback and found original dex files - extract them from an APK.
// Also warn about this operation because it's potentially wasteful.
LOG(WARNING) << "Found duplicate classes, falling back to extracting from APK : "
<< dex_location;
LOG(WARNING) << "NOTE: This wastes RAM and hurts startup performance.";
}
} else {
// TODO: We should remove this. The fact that we're here implies -Xno-dex-file-fallback
// was set, which means that we should never fallback. If we don't have original dex
// files, we should just fail resolution as the flag intended.
if (!oat_file_assistant.HasOriginalDexFiles()) {
accept_oat_file = true;
}
LOG(WARNING) << "Found duplicate classes, dex-file-fallback disabled, will be failing to "
" load classes for " << dex_location;
}
LOG(WARNING) << error_msg;
}
if (accept_oat_file) {
VLOG(class_linker) << "Registering " << oat_file->GetLocation();
//source_oat_file 賦值為oat_file 倘要, 并且oat_files_中添加 oat_file
source_oat_file = RegisterOatFile(std::move(oat_file));
*out_oat_file = source_oat_file;
}
}
std::vector<std::unique_ptr<const DexFile>> dex_files;
// Load the dex files from the oat file.
if (source_oat_file != nullptr) {
bool added_image_space = false;
if (source_oat_file->IsExecutable()) {
ScopedTrace app_image_timing("AppImage:Loading");
// We need to throw away the image space if we are debuggable but the oat-file source of the
// image is not otherwise we might get classes with inlined methods or other such things.
std::unique_ptr<gc::space::ImageSpace> image_space;
if (ShouldLoadAppImage(check_collision_result,
source_oat_file,
context.get(),
&error_msg)) {
//加載.art文件
image_space = oat_file_assistant.OpenImageSpace(source_oat_file);
}
if (image_space != nullptr) {
ScopedObjectAccess soa(self);
StackHandleScope<1> hs(self);
Handle<mirror::ClassLoader> h_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader>(class_loader)));
// Can not load app image without class loader.
if (h_loader != nullptr) {
std::string temp_error_msg;
// Add image space has a race condition since other threads could be reading from the
// spaces array.
{
ScopedThreadSuspension sts(self, kSuspended);
gc::ScopedGCCriticalSection gcs(self,
gc::kGcCauseAddRemoveAppImageSpace,
gc::kCollectorTypeAddRemoveAppImageSpace);
ScopedSuspendAll ssa("Add image space");
runtime->GetHeap()->AddSpace(image_space.get());
}
{
ScopedTrace trace2(StringPrintf("Adding image space for location %s", dex_location));
//去.art里找dexfile 對(duì)應(yīng)的熱點(diǎn)函數(shù)類(lèi),map到內(nèi)存,并加入到class table
added_image_space = runtime->GetClassLinker()->AddImageSpace(image_space.get(),
h_loader,
dex_elements,
dex_location,
/*out*/&dex_files,
/*out*/&temp_error_msg);
}
if (added_image_space) {
// Successfully added image space to heap, release the map so that it does not get
// freed.
image_space.release(); // NOLINT b/117926937
// Register for tracking.
for (const auto& dex_file : dex_files) {
dex::tracking::RegisterDexFile(dex_file.get());
}
} else {
LOG(INFO) << "Failed to add image file " << temp_error_msg;
dex_files.clear();
{
ScopedThreadSuspension sts(self, kSuspended);
gc::ScopedGCCriticalSection gcs(self,
gc::kGcCauseAddRemoveAppImageSpace,
gc::kCollectorTypeAddRemoveAppImageSpace);
ScopedSuspendAll ssa("Remove image space");
runtime->GetHeap()->RemoveSpace(image_space.get());
}
// Non-fatal, don't update error_msg.
}
}
}
}
if (!added_image_space) {
DCHECK(dex_files.empty());
//嘗試從odex中加載dex
dex_files = oat_file_assistant.LoadDexFiles(*source_oat_file, dex_location);
// Register for tracking.
for (const auto& dex_file : dex_files) {
dex::tracking::RegisterDexFile(dex_file.get());
}
}
if (dex_files.empty()) {
error_msgs->push_back("Failed to open dex files from " + source_oat_file->GetLocation());
} else {
// Opened dex files from an oat file, madvise them to their loaded state.
for (const std::unique_ptr<const DexFile>& dex_file : dex_files) {
OatDexFile::MadviseDexFile(*dex_file, MadviseState::kMadviseStateAtLoad);
}
}
}
// Fall back to running out of the original dex file if we couldn't load any
// dex_files from the oat file.
if (dex_files.empty()) {
//前面都沒(méi)加載dexfile,那么直接嘗試加載原始dex文件十拣。
if (oat_file_assistant.HasOriginalDexFiles()) {
if (Runtime::Current()->IsDexFileFallbackEnabled()) {
static constexpr bool kVerifyChecksum = true;
const ArtDexFileLoader dex_file_loader;
if (!dex_file_loader.Open(dex_location,
dex_location,
Runtime::Current()->IsVerificationEnabled(),
kVerifyChecksum,
/*out*/ &error_msg,
&dex_files)) {
LOG(WARNING) << error_msg;
error_msgs->push_back("Failed to open dex files from " + std::string(dex_location)
+ " because: " + error_msg);
}
} else {
error_msgs->push_back("Fallback mode disabled, skipping dex files.");
}
} else {
error_msgs->push_back("No original dex files found for dex location "
+ std::string(dex_location));
}
}
if (Runtime::Current()->GetJit() != nullptr) {
ScopedObjectAccess soa(self);
Runtime::Current()->GetJit()->RegisterDexFiles(
dex_files, soa.Decode<mirror::ClassLoader>(class_loader));
}
return dex_files;
}
這里代碼挺長(zhǎng)封拧,但是總結(jié)起來(lái)就加載dex文件到內(nèi)存,并且初始化好DexFile返回夭问。而加載dex文件的邏輯是:
.art > odex > 從原始dex文件
.art在編譯期做了熱點(diǎn)函數(shù)對(duì)應(yīng)類(lèi)的預(yù)加載泽西,保存了內(nèi)存image,如果它有則通過(guò)classlinker.AddImageSpace直接將image 加載到內(nèi)存缰趋,省略類(lèi)加載過(guò)程捧杉,直接加入class table。找不到退而求其次秘血,執(zhí)行oat_file_assistant.LoadDexFiles味抖,從odex加載dex,因?yàn)閛dex中保存了一個(gè)優(yōu)化后的dex,都不行最后嘗試從原始dex文件中加載灰粮,如果此時(shí)vdex還有效仔涩,則能繞過(guò)dex驗(yàn)證過(guò)程,否則需要重新驗(yàn)證。
三粘舟、Apk加載過(guò)程與Tinker動(dòng)態(tài)加載dex補(bǔ)丁包整體順序流程
dex補(bǔ)丁包加載是在TinkerApplication的onBaseContextAttached中通過(guò)TinkerLoader.tryLoad觸發(fā)的熔脂,而TinkerApplication是繼承Application的。
那么在應(yīng)用啟動(dòng)流程中柑肴,application初始化流程是怎樣的:
這里借用我之前文章一張圖霞揉,雖然是android 4.4的,但是基本流程沒(méi)有什么變化晰骑。
handleBindApplication : 通過(guò)getPackageInfo初始化LoadApk零聚,初始化Instrumentation,通過(guò)makeApplication加載主apk, 然后初始化Application類(lèi),最后通過(guò)Instrumentation執(zhí)行Application的onCreate回調(diào)隶症。
也就是說(shuō)政模,當(dāng)tinker熱修復(fù)合成patch包,應(yīng)用程序進(jìn)程被kill重啟之后蚂会,走冷啟動(dòng)流程(冷啟動(dòng)詳細(xì)流程可以參考之前文章:深入剖析應(yīng)用啟動(dòng)流程)淋样,先通過(guò)LoadedApk加載之前出現(xiàn)bug的基準(zhǔn)apk,然后加載application并初始化胁住,初始化過(guò)程走application的onBaseContextAttached生命周期趁猴,Tinker通過(guò)TinkerLoader.tryLoad來(lái)走補(bǔ)丁包加載,加載原理便是hook DexPathList 將修復(fù)的合成dex添加到Elements[]頭部彪见。
寫(xiě)在最后的注意點(diǎn):
Android Q之前儡司,DexFile.loadDex會(huì)先判斷是否需要走編譯邏輯,如果要走余指,則需要同步等待編譯完才能執(zhí)行后續(xù)流程捕犬,因此如果合成后的patch包tinker_classN.apk太大的話,dex補(bǔ)丁加載過(guò)程會(huì)出現(xiàn)明顯卡頓酵镜,這個(gè)問(wèn)題系統(tǒng)層不太好去規(guī)避碉碉。
舉個(gè)比較典型的場(chǎng)景,比如系統(tǒng)ota升級(jí)之后淮韭,首次啟動(dòng)app加載熱修復(fù)包垢粮,必然會(huì)走編譯,因?yàn)閛ta升級(jí)之后odex失效需要重新編譯靠粪。那么系統(tǒng)層要想優(yōu)化主要有兩個(gè)思路:1)預(yù)編譯蜡吧;2)不同步等待編譯,先走解釋模式占键;
1)預(yù)編譯:在ota升級(jí)過(guò)程中或者升級(jí)完重啟手機(jī)過(guò)程中找個(gè)時(shí)機(jī)去將插件編譯一下昔善,這有個(gè)問(wèn)題就是:tinker_classN.apk存放的目錄是在data/data/packageName 應(yīng)用私有目錄下,這個(gè)目錄在鎖屏解鎖前捞慌,內(nèi)的文件都是按FBE加密的,系統(tǒng)沒(méi)法獲取有效解密文件柬批。但是如果鎖屏解鎖之后再做預(yù)加載顯然有點(diǎn)晚了啸澡。
2)不同步等待編譯,先走解釋模式: 這種便是google 在Android Q上的優(yōu)化氮帐,但是需要配合dexpath上報(bào)嗅虏,來(lái)讓系統(tǒng)在idle時(shí)進(jìn)行插件編譯,這也是原生唯一能編譯插件的地方上沐,但是這種場(chǎng)景觸發(fā)太不及時(shí)了皮服,因此可以增加一些場(chǎng)景來(lái)及時(shí)觸發(fā)插件編譯,保證下次應(yīng)用使用更快。