List All Classes after Multidex

  1. 1. Intro
  2. 2. Problem
  3. 3. Clue 1
  4. 4. Clue 2
  5. 5. Solution

Intro

Google recently posted an article to solve the 65K Reference Limit problem.
It is such a relief that we will not need to worry about the following errors any more.

1
2
3
4
5
6
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
//--or--
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

Problem

After configuring multidex in the module, some projects may encounter the loss of part of the the classes when using reflection to list all the classes in the apk.
The usual code may like this:

1
2
3
4
5
6
7
8
9
//get the apk path
String path = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0).sourceDir;
//use DexFile the unzip the apk, find the *classes.dex* in the apk
DexFile dexfile = new DexFile(path);
//get all the classes in this dex
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
classNames.add(dexEntries.nextElement());
}

Clue 1

It’s easy to get the source code of DexFile.java,

DexFile.javalink
1
2
3
4
5
6
7
/**
* Opens a DEX file from a given filename. This will usually be a ZIP/JAR
* file with a "classes.dex" inside.
*
* ...
*/

public DexFile(String fileName) throws IOException {

We can find out that the DexFile constructor only loads the classes.dex in the apk, while the file structure in the apk should be like this:

1
2
3
test.apk
├── classes.dex
└── classes2.dex

It means that the classes in classes2.dex will not be opened by default constructor.

And the post by Google says that,

… it is possible that other included libraries have additional dependency requirements including the use of introspection or invocation of Java methods from native code.
Some libraries may not be able to be used until the multidex build tools are updated to allow you to specify classes that must be included in the primary dex file.

If we want to load all the classes in our project, we will need load the classes2.dex, classes3.dex manually.

Clue 2

Also from the post, we can find out that all the multi dex tasks start at MultiDex.install(context)(in MultiDex.java), which calls the MultiDexExtractor.load(context, applicationInfo, dexDir, false)(in MultiDexExtractor.java) to extract the apk and install extra dex files.

And we can find a method called MultiDexExtractor.loadExistingExtractions(...) which can load existing secondary dex files that extracted by MultiDexExtractor.load(...).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0)
//sourceApk = new File(applicationInfo.sourceDir)
// ie: test.apk
//dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); (in MultiDex.java)
// ie: /data/data/info.xudshen.test/code_cache/secondary-dexes
private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
throws IOException {

Log.i(TAG, "loading existing secondary dex files");
//the prefix of extracted file, ie: test.classes
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
final List<File> files = new ArrayList<File>(totalDexNumber);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
files.add(extractedFile);
//verify the zip file
if (!verifyZipFile(extractedFile)) {
Log.i(TAG, "Invalid zip file: " + extractedFile);
throw new IOException("Invalid ZIP file.");
}
} else {
throw new IOException("Missing extracted secondary dex file '" +
extractedFile.getPath() + "'");
}
}
return files;
}

The code indicates that all the extra dex files are extracted in /data/data/<packageName>/code_cache/secondary-dexes. What we need to do is getting the paths and passing it to DexFile constructor.

Unfortunately, we can not call this method directly since MultiDexExtractor.java is a private class.
We will implement this part of code in our own project.

Solution

Here is the complete code in Gist.
This solution is based on platform/frameworks/multidex ddd65a611834d9a9222603b2e85d558265f0aa85.

We will need update the code once the extract path changes in MultiDex.java.