1. 程式人生 > >Dalvik虛擬機中DexClassLookup結構解析

Dalvik虛擬機中DexClassLookup結構解析

span type offsetof rep 函數 進行 現在 alloc lookup

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
  1. struct DexClassLookup {
  2. int size;
  3. int numEntries;
  4. struct {
  5. u4 classDescriptorHash;
  6. int classDescriptorOffset;
  7. int classDefOffset;
  8. } table[1];
  9. };

結構體最開始是一個int型的size,表示了這個DexClassLookup結構體到底要占用多少字節的空間。這個大小也包含了size變量本身的4字節。

接下來的int型numEntries,表示DexClassLookup到底包含了多少個條目。

最後定義了一個內部結構體,存放具體的數據。不過table[1]並不是表示DexClassLookup中只包含一項這個結構體數據,這裏只表示下面的是一個數組,具體有多少項,是由前面的numEntries來指定的。

下面,我們來看看,到底這個結構體是如何生成的(代碼位於\dalvik\libdex\DexFile.cpp內):

[cpp] view plain copy
  1. DexClassLookup* dexCreateClassLookup(DexFile* pDexFile)
  2. {
  3. DexClassLookup* pLookup;
  4. int allocSize;
  5. int i, numEntries;
  6. int numProbes, totalProbes, maxProbes;
  7. numProbes = totalProbes = maxProbes = 0;
  8. assert(pDexFile != NULL);
  9. numEntries = dexRoundUpPower2(pDexFile->pHeader->classDefsSize * 2);
  10. allocSize = offsetof(DexClassLookup, table)
  11. + numEntries * sizeof(pLookup->table[0]);
  12. pLookup = (DexClassLookup*) calloc(1, allocSize);
  13. if (pLookup == NULL)
  14. return NULL;
  15. pLookup->size = allocSize;
  16. pLookup->numEntries = numEntries;
  17. for (i = 0; i < (int)pDexFile->pHeader->classDefsSize; i++) {
  18. const DexClassDef* pClassDef;
  19. const char* pString;
  20. pClassDef = dexGetClassDef(pDexFile, i);
  21. pString = dexStringByTypeIdx(pDexFile, pClassDef->classIdx);
  22. classLookupAdd(pDexFile, pLookup,
  23. (u1*)pString - pDexFile->baseAddr,
  24. (u1*)pClassDef - pDexFile->baseAddr, &numProbes);
  25. if (numProbes > maxProbes)
  26. maxProbes = numProbes;
  27. totalProbes += numProbes;
  28. }
  29. ...
  30. return pLookup;
  31. }

代碼首先確定到底要存放多少條數據。註意,並不是有多少個類就生成多少個條目的。可以看到,具體生成的條目數是類的個數乘以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
  1. static void classLookupAdd(DexFile* pDexFile, DexClassLookup* pLookup,
  2. int stringOff, int classDefOff, int* pNumProbes)
  3. {
  4. const char* classDescriptor =
  5. (const char*) (pDexFile->baseAddr + stringOff);
  6. const DexClassDef* pClassDef =
  7. (const DexClassDef*) (pDexFile->baseAddr + classDefOff);
  8. u4 hash = classDescriptorHash(classDescriptor);
  9. int mask = pLookup->numEntries-1;
  10. int idx = hash & mask;
  11. int probes = 0;
  12. while (pLookup->table[idx].classDescriptorOffset != 0) {
  13. idx = (idx + 1) & mask;
  14. probes++;
  15. }
  16. pLookup->table[idx].classDescriptorHash = hash;
  17. pLookup->table[idx].classDescriptorOffset = stringOff;
  18. pLookup->table[idx].classDefOffset = classDefOff;
  19. *pNumProbes = probes;
  20. }

函數首先調用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
  1. const DexClassDef* dexFindClass(const DexFile* pDexFile,
  2. const char* descriptor)
  3. {
  4. const DexClassLookup* pLookup = pDexFile->pClassLookup;
  5. u4 hash;
  6. int idx, mask;
  7. hash = classDescriptorHash(descriptor);
  8. mask = pLookup->numEntries - 1;
  9. idx = hash & mask;
  10. while (true) {
  11. int offset;
  12. offset = pLookup->table[idx].classDescriptorOffset;
  13. if (offset == 0)
  14. return NULL;
  15. if (pLookup->table[idx].classDescriptorHash == hash) {
  16. const char* str;
  17. str = (const char*) (pDexFile->baseAddr + offset);
  18. if (strcmp(str, descriptor) == 0) {
  19. return (const DexClassDef*)
  20. (pDexFile->baseAddr + pLookup->table[idx].classDefOffset);
  21. }
  22. }
  23. idx = (idx + 1) & mask;
  24. }
  25. }

查找的代碼就非常簡單了,還是先對要查找類的類描述符,用同樣的算法計算一下Hash值,根據條目的數目,取Hash值相應的低幾位。以這個值為下標,嘗試讀取數組中對應位置的數據。如果沒有碰撞情況發生的話,一次就能找到你想找的類。如果有碰撞情況的話,還是試著循環查找下一個位置的信息。所以,可以看出來,查找的時候,是將字符串的逐個字符比較轉變成了一個四字節數字的比較,速度大大加快了。

對每一個DEX文件來說,其實只需要在最開始計算一次就可以了,沒必要每次加載的時候都計算一遍。大家知道,一個DEX文件在第一次被加載的時候,Dalvik虛擬機會對其進行驗證和優化,從而以後再次加載這個DEX文件的時候,可以直接讀取優化過得ODEX文件,加快加載速度。而在ODEX文件中,其實就包含了對應於這個DEX文件的DexClassLookup結構體數據,直接mmap到內存就好了,不需要再算了。

這裏再引申討論一下,為什麽DEX文件中不直接包含對應的DexClassLookup結構體數據呢,就像ELF文件一樣?理論上其實是可以的,因為這些都是靜態數據,不會在運行的時候改變。我想唯一的解釋估計是android不想把快速查找的功能和DEX綁死,而是由Dalvik虛擬機自己實現。這樣,不同版本的虛擬機完全可以使用不同的快速查找算法。

Dalvik虛擬機中DexClassLookup結構解析