Dalvik虛擬機中DexClassLookup結構解析
http://blog.csdn.net/roland_sun/article/details/46877563 原文如下:
在Android系統中,所有的類定義以及具體的代碼都是包含在DEX文件中的。但是,一個功能豐富的程序往往都比較復雜,由很多類組成。
而每一個類,都由一個所謂類描述符(Class Descriptor)的字符串來唯一標識,兩個類不可能有同一個類描述符。類描述符不僅包含類名,還包含了類所在的包名。例如,如果你的類所在包名是“com.trendmicro.mars”,且類名是“Test”的話,那麽這個類的類描述符就是“Lcom/trendmicro/mars/Test;”。
但是,如果要從一個DEX文件內的眾多類中找出那個你想使用的類,僅僅通過逐一比較DEX文件中所有類的類描述符字符串的話,速度往往會比較慢,用戶體驗會比較差。
Dalvik虛擬機為了解決這個問題,在加載和驗證一個DEX文件的時候,會附帶生成一個所謂的DexClassLookup結構體,來加快類的查找速度。
[cpp] view plain copy- struct DexClassLookup {
- int size;
- int numEntries;
- struct {
- u4 classDescriptorHash;
- int classDescriptorOffset;
- int classDefOffset;
- } table[1];
- };
結構體最開始是一個int型的size,表示了這個DexClassLookup結構體到底要占用多少字節的空間。這個大小也包含了size變量本身的4字節。
接下來的int型numEntries,表示DexClassLookup到底包含了多少個條目。
最後定義了一個內部結構體,存放具體的數據。不過table[1]並不是表示DexClassLookup中只包含一項這個結構體數據,這裏只表示下面的是一個數組,具體有多少項,是由前面的numEntries來指定的。
下面,我們來看看,到底這個結構體是如何生成的(代碼位於\dalvik\libdex\DexFile.cpp內):
[cpp] view plain copy- DexClassLookup* dexCreateClassLookup(DexFile* pDexFile)
- {
- DexClassLookup* pLookup;
- int allocSize;
- int i, numEntries;
- int numProbes, totalProbes, maxProbes;
- numProbes = totalProbes = maxProbes = 0;
- assert(pDexFile != NULL);
- numEntries = dexRoundUpPower2(pDexFile->pHeader->classDefsSize * 2);
- allocSize = offsetof(DexClassLookup, table)
- + numEntries * sizeof(pLookup->table[0]);
- pLookup = (DexClassLookup*) calloc(1, allocSize);
- if (pLookup == NULL)
- return NULL;
- pLookup->size = allocSize;
- pLookup->numEntries = numEntries;
- for (i = 0; i < (int)pDexFile->pHeader->classDefsSize; i++) {
- const DexClassDef* pClassDef;
- const char* pString;
- pClassDef = dexGetClassDef(pDexFile, i);
- pString = dexStringByTypeIdx(pDexFile, pClassDef->classIdx);
- classLookupAdd(pDexFile, pLookup,
- (u1*)pString - pDexFile->baseAddr,
- (u1*)pClassDef - pDexFile->baseAddr, &numProbes);
- if (numProbes > maxProbes)
- maxProbes = numProbes;
- totalProbes += numProbes;
- }
- ...
- return pLookup;
- }
代碼首先確定到底要存放多少條數據。註意,並不是有多少個類就生成多少個條目的。可以看到,具體生成的條目數是類的個數乘以2,然後再算下一個2的冪次方。比如,如果我有5個類的話,那麽首先乘以2,得到10,下一個2的冪次方數字是16,,就會生成16個條目。為什麽要這麽做?我覺得是為了盡量減少Hash碰撞的情況發生。
知道了要創建多少條目的數據後,就可以知道到底要開辟多大的空間來存放這個結構體數據(按照現在的定義,分配空間的計算公式是8+numEntries*12),並且在內存中為這個結構體分配一段連續的空間。接著,對DexClassLookup結構體的前兩個變量size和numEntries賦值。
最後,就是要來填寫具體的數據了。程序中會遍歷DEX文件中包含的每一個類,逐一獲得它們的DexClassDef結構和類描述符,且傳遞給classLookupAdd函數,讓它來填寫對應該類的快速查找數據(代碼位於\dalvik\libdex\DexFile.cpp內):
[cpp] view plain copy- static void classLookupAdd(DexFile* pDexFile, DexClassLookup* pLookup,
- int stringOff, int classDefOff, int* pNumProbes)
- {
- const char* classDescriptor =
- (const char*) (pDexFile->baseAddr + stringOff);
- const DexClassDef* pClassDef =
- (const DexClassDef*) (pDexFile->baseAddr + classDefOff);
- u4 hash = classDescriptorHash(classDescriptor);
- int mask = pLookup->numEntries-1;
- int idx = hash & mask;
- int probes = 0;
- while (pLookup->table[idx].classDescriptorOffset != 0) {
- idx = (idx + 1) & mask;
- probes++;
- }
- pLookup->table[idx].classDescriptorHash = hash;
- pLookup->table[idx].classDescriptorOffset = stringOff;
- pLookup->table[idx].classDefOffset = classDefOff;
- *pNumProbes = probes;
- }
函數首先調用classDescriptorHash,計算出類描述符對應的一個Hash值,這是一個數字。
然後,代碼會根據條目數的多少,計算出一個mask,並且和前面計算的Hash值與以下,算出該條數據在數組中存放位置的下標。前面說過了,數據的條目數一定是2的冪次方。比如,如果是8的話,下標值就取Hash值得後三位,16的話就取Hash值得後四位。這也就解釋了,為什麽快速查找數據的條目數必須是2的冪次方了。
接下來,看看數組中這個下標對應的條目是不是已經存放了別的類的信息。這種情況,就是碰撞,兩個不同的類被映射到了同一個數字上。一旦出現了碰撞的情況話,程序接著用了一種非常簡單的處理方法,直接將下標加1,和mask再與一下,得到接著要嘗試存放的那個位置,再重頭判斷一下,直到找到一個沒有被用過的位置。但是,這樣處理,有可能會占了別的類應該存放的位置,使得性能下降。所以,前面的代碼在計算條目數的時候,人為的乘以2,降低了碰撞的概率。不過這樣處理的話,存儲空間會比較浪費。最後,找到了一個空的位置後,會將對應類的具體數據,包括前面算的類描述符Hash值、類描述符字符串和該類的DexClassDef相對於DEX文件頭的偏移量等信息,存放在該位置上。
好了,看完了如何生成DexClassLookup結構體數據,我們再來看看Dalvik虛擬機是如何利用它來加快類的查找速度的(代碼位於\dalvik\libdex\DexFile.cpp內):
[cpp] view plain copy- const DexClassDef* dexFindClass(const DexFile* pDexFile,
- const char* descriptor)
- {
- const DexClassLookup* pLookup = pDexFile->pClassLookup;
- u4 hash;
- int idx, mask;
- hash = classDescriptorHash(descriptor);
- mask = pLookup->numEntries - 1;
- idx = hash & mask;
- while (true) {
- int offset;
- offset = pLookup->table[idx].classDescriptorOffset;
- if (offset == 0)
- return NULL;
- if (pLookup->table[idx].classDescriptorHash == hash) {
- const char* str;
- str = (const char*) (pDexFile->baseAddr + offset);
- if (strcmp(str, descriptor) == 0) {
- return (const DexClassDef*)
- (pDexFile->baseAddr + pLookup->table[idx].classDefOffset);
- }
- }
- idx = (idx + 1) & mask;
- }
- }
查找的代碼就非常簡單了,還是先對要查找類的類描述符,用同樣的算法計算一下Hash值,根據條目的數目,取Hash值相應的低幾位。以這個值為下標,嘗試讀取數組中對應位置的數據。如果沒有碰撞情況發生的話,一次就能找到你想找的類。如果有碰撞情況的話,還是試著循環查找下一個位置的信息。所以,可以看出來,查找的時候,是將字符串的逐個字符比較轉變成了一個四字節數字的比較,速度大大加快了。
對每一個DEX文件來說,其實只需要在最開始計算一次就可以了,沒必要每次加載的時候都計算一遍。大家知道,一個DEX文件在第一次被加載的時候,Dalvik虛擬機會對其進行驗證和優化,從而以後再次加載這個DEX文件的時候,可以直接讀取優化過得ODEX文件,加快加載速度。而在ODEX文件中,其實就包含了對應於這個DEX文件的DexClassLookup結構體數據,直接mmap到內存就好了,不需要再算了。
這裏再引申討論一下,為什麽DEX文件中不直接包含對應的DexClassLookup結構體數據呢,就像ELF文件一樣?理論上其實是可以的,因為這些都是靜態數據,不會在運行的時候改變。我想唯一的解釋估計是android不想把快速查找的功能和DEX綁死,而是由Dalvik虛擬機自己實現。這樣,不同版本的虛擬機完全可以使用不同的快速查找算法。
Dalvik虛擬機中DexClassLookup結構解析