Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 由Android 65K方法數限制引發的思考

由Android 65K方法數限制引發的思考

編輯:Android資訊

前言

沒想到,65536真的很小。

Unable to execute dex: method ID not in [0, 0xffff]: 65536

PS:本文只是純探索一下這個65K的來源,僅此而已。

到底是65k還是64k?

都沒錯,同一個問題,不同的說法而已。

65536按1000算的話,是65k ~ 65 1000;

65536按1024算的話,是64k = 64 1024。

重點是65536=2^16,請大家記住這個數字。

時間點

從大家的經歷和這篇文章:

http://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

來看,這個錯誤是發生在構建時期。

65536是怎麼算出來的?

65536網上眾說紛纭,有對的,有不全對的,也有錯的。下面將跟蹤最新的AOSP源碼來順籐摸瓜,但是探索問題必然迂回冗余,僅作記錄,讀者可直接跳過看結果。

1. 首先,查找Dex的結構定義。

/*
 * Direct-mapped "header_item" struct.
 */
struct DexHeader {
    u1  magic[8];
    u4  checksum;
    u1  signature[kSHA1DigestLen];
    u4  fileSize;
    u4  headerSize;
    u4  endianTag;
    u4  linkSize;
    u4  linkOff;
    u4  mapOff;
    u4  stringIdsSize;
    u4  stringIdsOff;
    u4  typeIdsSize;
    u4  typeIdsOff;
    u4  protoIdsSize;
    u4  protoIdsOff;
    u4  fieldIdsSize;
    u4  fieldIdsOff;
    u4  methodIdsSize; // 這裡存放了方法字段索引的大小,methodIdsSize的類型為u4
    u4  methodIdsOff;
    u4  classDefsSize;
    u4  classDefsOff;
    u4  dataSize;
    u4  dataOff;
};

u4的類型定義如下:

/*
 * These match the definitions in the VM specification.
 */
typedef uint8_t             u1;
typedef uint16_t            u2;
typedef uint32_t            u4;
typedef uint64_t            u8;
typedef int8_t              s1;
typedef int16_t             s2;
typedef int32_t             s4;
typedef int64_t             s8;

進一步推出,methodIdsSize的類型是uint32_t,但它的限制為2^32 = 65536 * 65536,比65536大的多。

所以,65k不是dex文件結構本身限制造成的。

PS:Dex文件中存儲方法ID用的並不是short類型,無論最新的DexFile.h新定義的u4是uint32_t,還是老版本DexFile引用的vm/Common.h裡定義的u4是uint32或者unsigned int,都不是short類型,特此說明。

2. DexOpt優化造成?

這個說法源自:

當Android系統啟動一個應用的時候,有一步是對Dex進行優化,這個過程有一個專門的工具來處理,叫DexOpt。DexOpt的執行過程是在第一次加載Dex文件的時候執行的。這個過程會生成一個ODEX文件,即Optimised Dex。執行ODex的效率會比直接執行Dex文件的效率要高很多。但是在早期的Android系統中,DexOpt有一個問題,也就是這篇文章想要說明並解決的問題。DexOpt會把每一個類的方法id檢索起來,存在一個鏈表結構裡面。但是這個鏈表的長度是用一個short類型來保存的,導致了方法id的數目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。盡管在新版本的Android系統中,DexOpt修復了這個問題,但是我們仍然需要對老系統做兼容。

鑒於我能力有限,沒有找到這塊邏輯對應的代碼。

但我有個疑問,這個限制是在Android啟動一個應用的時候發生的,但從前面的“時間點”章節,65k問題是在構建的時候就發生了,還沒到啟動或者運行這一步。

我不敢否定這種說法,但說明65k至少還有其他地方限制。

3. DexMerger的檢測

只能在dalvik目錄下搜索關鍵字”methid ID not in”,在DexMergger裡找到了拋出異常的地方:

/**
 * Combine two dex files into one.
  */
public final class DexMerger {

    private void mergeMethodIds() {
        new IdMerger<MethodId>(idsDefsOut) {
            @Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
                return tableOfContents.methodIds;
            }

            @Override MethodId read(Dex.Section in, IndexMap indexMap, int index) {
                return indexMap.adjust(in.readMethodId());
            }

            @Override void updateIndex(int offset, IndexMap indexMap, int oldIndex, int newIndex) {
                if (newIndex < 0 || newIndex > 0xffff) {
                    throw new DexIndexOverflowException(
                            "method ID not in [0, 0xffff]: " + newIndex);
                }
                indexMap.methodIds[oldIndex] = (short) newIndex;
            }

            @Override void write(MethodId methodId) {
                methodId.writeTo(idsDefsOut);
            }
        }.mergeSorted();
    }
}

這裡定義了indexMap的methodIds的單項值要強轉short,所以在存放之前check一下范圍是不是0 ~ 0xffff。我們看看IndexMap的定義:

/**
 * Maps the index offsets from one dex file to those in another. For example, if
 * you have string #5 in the old dex file, its position in the new dex file is
 * {@code strings[5]}.
 */
public final class IndexMap {
    private final Dex target;
    public final int[] stringIds;
    public final short[] typeIds;
    public final short[] protoIds;
    public final short[] fieldIds;
    public final short[] methodIds;

    // ... ...
}

看上去是對了,可是這個DexMerger是合並兩個dex的,默認情況下我們只有一個dex的,那麼這個65k是哪裡限制的呢?再查!

4. 回歸DexFile

基本上前面基本是一個摸著石頭過河、反復驗證網絡說法的一個過程,雖然回想起來傻傻的,但是這種記錄還是有必要的。

前面看到DexFile的存放方法數大小的類型是uint32,但是根據後面的判斷,我們確定是打包的過程中產生了65k問題,所以我們得回過頭老老實實研究一下dx的打包流程。

… 此處省略分析流程5000字 …

OK,我把dx打包涉及到流程記錄下來:

// 源碼目錄:dalvik/dx
// Main.java
-> main() -> run() -> runMonoDex()(或者runMultiDex()) -> writeDex()
// DexFile
-> toDex() -> toDex0()
// MethodIdsSection extends MemberIdsSection extends UniformItemSection extends  Section
-> prepare() -> prepare0() -> orderItems() -> getTooManyMembersMessage()
// Main.java
-> getTooManyIdsErrorMessage()

最終狐狸的尾巴是在MemberIdsSection漏出來了:

package com.android.dx.dex.file;

import com.android.dex.DexException;
import com.android.dex.DexFormat;
import com.android.dex.DexIndexOverflowException;
import com.android.dx.command.dexer.Main;

import java.util.Formatter;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Member (field or method) refs list section of a {@code .dex} file.
 */
public abstract class MemberIdsSection extends UniformItemSection {

    /**
     * Constructs an instance. The file offset is initially unknown.
     *
     * @param name {@code null-ok;} the name of this instance, for annotation
     * purposes
     * @param file {@code non-null;} file that this instance is part of
     */
    public MemberIdsSection(String name, DexFile file) {
        super(name, file, 4);
    }

    /** {@inheritDoc} */
    @Override
        protected void orderItems() {
            int idx = 0;

            if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
                throw new DexIndexOverflowException(getTooManyMembersMessage());
            }

            for (Object i : items()) {
                ((MemberIdItem) i).setIndex(idx);
                idx++;
            }
        }

    private String getTooManyMembersMessage() {
        Map<String, AtomicInteger> membersByPackage = new TreeMap<String, AtomicInteger>();
        for (Object member : items()) {
            String packageName = ((MemberIdItem) member).getDefiningClass().getPackageName();
            AtomicInteger count = membersByPackage.get(packageName);
            if (count == null) {
                count = new AtomicInteger();
                membersByPackage.put(packageName, count);
            }
            count.incrementAndGet();
        }

        Formatter formatter = new Formatter();
        try {
            String memberType = this instanceof MethodIdsSection ? "method" : "field";
            formatter.format("Too many %s references: %d; max is %d.%n" +
                    Main.getTooManyIdsErrorMessage() + "%n" +
                    "References by package:",
                    memberType, items().size(), DexFormat.MAX_MEMBER_IDX + 1);
            for (Map.Entry<String, AtomicInteger> entry : membersByPackage.entrySet()) {
                formatter.format("%n%6d %s", entry.getValue().get(), entry.getKey());
            }
            return formatter.toString();
        } finally {
            formatter.close();
        }
    }

}

裡面有一段:

// 如果方法數大於0xffff就提示65k錯誤
if (items().size() > DexFormat.MAX_MEMBER_IDX + 1) {
    throw new DexIndexOverflowException(getTooManyMembersMessage());
}

// 這個DexFormat.MAX_MEMBER_IDX就是0xFFFF
/**
 * Maximum addressable field or method index.
 * The largest addressable member is 0xffff, in the "instruction formats" spec as field@CCCC or
 * meth@CCCC.
 */
public static final int MAX_MEMBER_IDX = 0xFFFF;

至此,真相大白!

5. 根本原因

為什麼定義DexFormat.MAX_MEMBER_IDX為0xFFFF?

雖然我們找到了65k報錯的地方,但是為什麼程序中方法數超過0xFFFF就要報錯呢?

通過搜索”instruction formats”, 我最終查到了Dalvik VM Bytecode,找到最新的官方說明:

https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

裡面說明了上面的@CCCC的范圍必須在0~65535之間,這是dalvik bytecode的限制。

所以,65536是bytecode的16位限制算出來的:2^16。

PS:以上分析得到群裡很多朋友的討論和幫忙。

6. 回顧

我好像明白了什麼:

  1. 65k問題是dx打包單個Dex時報的錯,所以只要用dx打包單個dex就可能有這個問題。
  2. 不僅方法數,字段數也有65k問題。
  3. 目前來說,65k問題和系統無關。
  4. 目前來說,65k問題和art無關。
  5. 即使分包MultiDex,當主Dex的方法數超過65k依然會報錯。
  6. MultiDex方案不是從根本上解決了65k問題,但是大大緩解甚至說基本解決了65k問題。

新的Jack能否解決65k問題?

據說Jack的方式把class打包成.jack文件。所以我認為,Jack具備解決65k問題的條件:

  1. 打包:新的jack文件肯定是拋棄了dalvik的兼容性,這也注定咱們這兩年可能還用不了。
  2. 虛擬機:完全采用新的ART虛擬機,把class轉化成本地機器碼,就能避開dalvik bytecode的16位限制。
  3. 上面兩條屬於廢話,說白了,完全不用dalvik虛擬機了,同時也就完全不用dx了,如此,當然就不存在65k問題了。

以上純屬我個人推測,一切以科學分析為准。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved