Denny1989
Denny1989

Reputation: 649

Android Multidex list all classes

i would like to use the ApplicationWrapepr approach to use multidex in my application like described here https://plus.google.com/104023661970539138053/posts/YTMf8ADTcFg

I used the --minimal-main-dex option along with a keep file like this:

android/support/multidex/ZipUtil.class
android/support/multidex/ZipUtil$CentralDirectory.class
android/support/multidex/MultiDex$V14.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDexExtractor.class
com/<mypackage>/common/MyApplication.class
com/<mypackage>/common/MyApplicationWrapper.class
com/<mypackage>/common/ui/DashboardActivity.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDex.class
android/support/multidex/MultiDex$V19.class
android/support/multidex/MultiDex$V4.class

This results in the listed classes in my main dex file which is ok. I than use a library that uses the following code to list all classes in the dexfile but just gets the entries of the main "clesses.dex" and not also of all other loaded dex files because new DexFile only checks for "classes.dex":

private static List<String> getPaths(final String[] sourcePaths) {
    List<String> result = new ArrayList<String>();

    for (String s : sourcePaths) {
      try {
        DexFile dexfile = new DexFile(s);
        Enumeration<String> entries = dexfile.entries();

        while (entries.hasMoreElements()) {
          result.add(entries.nextElement());
        }
      } catch (IOException ioe) {
        Log.w(TAG, "cannot open file=" + s + ";Exception=" + ioe.getMessage());
      }
    }

    return result;
}

the for now single path gets determined with:

application.getApplicationContext().getApplicationInfo().sourceDir;

which results to somthing like /data/../myapplicationname.apk

Is there another possibility to get all classes in the dex files listed? Or all classes currently in the ClassLoaders? The library is essential to the project and uses this approach to find component implementations via reflection later on.

EDIT1: if found out that the classes2.dex file is placed under : /data/data/com./code_cache/secondary-dexes/com.-1.apk.classes2.dex

however when using new DexFile() with this path IOEsxception is thrown with the message "unable to open dexfile".

Upvotes: 2

Views: 4514

Answers (3)

KFJK
KFJK

Reputation: 135

My solution works on almost all cases such as normal apk, multi-dex apk or instant-run apk.

The idea comes from multi-dex source code. I use reflection to get all DexElement of the Thread.currentThread().getContextClassLoader(), actually it can be any other classLoader. The reflection way is much tricky, because the pathList field is not belong to PathClassLoader but is belong to its super class. And no matter the apk is multi-dex apk or instant-run apk, the classLoader->pathList field contains all DexFile you need to find. And list all classes in a DexFile is not too difficult.

Here's the code:

    public static ArrayList<String> findClassesStartWith(String prefix) {
        try {
            ArrayList<String> result = new ArrayList<>();
            ArrayList<DexFile> dexFiles = findAllDexFiles(Thread.currentThread().getContextClassLoader());
            for (DexFile dexFile : dexFiles) {
                Enumeration<String> classNames = dexFile.entries();
                while (classNames.hasMoreElements()) {
                    String className = classNames.nextElement();
                    if (className.startsWith(prefix)) {
                        result.add(className);
                    }
                }
            }
            return result;
        } catch (Exception ignored) {
        }
        return null;
    }

    public static ArrayList<DexFile> findAllDexFiles(ClassLoader classLoader) {
        ArrayList<DexFile> dexFiles = new ArrayList<>();
        try {
            Field pathListField = findField(classLoader, "pathList");
            Object pathList = pathListField.get(classLoader);
            Field dexElementsField = findField(pathList, "dexElements");
            Object[] dexElements = (Object[]) dexElementsField.get(pathList);
            Field dexFileField = findField(dexElements[0], "dexFile");

            for (Object dexElement : dexElements) {
                Object dexFile = dexFileField.get(dexElement);
                dexFiles.add((DexFile) dexFile);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dexFiles;
    }

    private static Field findField(Object instance, String name) throws NoSuchFieldException {
        Class clazz = instance.getClass();

        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(name);
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }

                return field;
            } catch (NoSuchFieldException var4) {
                clazz = clazz.getSuperclass();
            }
        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

Upvotes: 1

Rambo
Rambo

Reputation: 66

If you run application with instant-run, DEX file path need to add this:

// handle dex files built by instant run
        File instantRunFilePath = new File(applicationInfo.dataDir,
                                           "files" + File.separator + "instant-run" + File.separator + "dex");

        if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) {
            File[] sliceFiles = instantRunFilePath.listFiles();
            for (File sliceFile : sliceFiles) {
                if (null != sliceFile && sliceFile.exists() && sliceFile.isFile() && sliceFile.getName().endsWith(".dex")) {
                    sourcePaths.add(sliceFile.getAbsolutePath());
                }
            }
        }

Upvotes: 1

austin.s
austin.s

Reputation: 178

The DexFile accepts the path of a zip/apk file, and extracts it to find the .dex file. So if you use the .dex as the path, it will throw an error.

Also Google has posted an a article Building Apps with Over 65K Methods to solve the --multi-dex problem.

I write a class to load all the classes. You can read more at: http://xudshen.info/2014/11/12/list-all-classes-after-multidex/

import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import dalvik.system.DexFile;

/**
 * Created by [email protected] on 14/11/13.
 */
public class MultiDexHelper {
    private static final String EXTRACTED_NAME_EXT = ".classes";
    private static final String EXTRACTED_SUFFIX = ".zip";

    private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
            "secondary-dexes";

    private static final String PREFS_FILE = "multidex.version";
    private static final String KEY_DEX_NUMBER = "dex.number";

    private static SharedPreferences getMultiDexPreferences(Context context) {
        return context.getSharedPreferences(PREFS_FILE,
                Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
                        ? Context.MODE_PRIVATE
                        : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
    }

    /**
     * get all the dex path
     *
     * @param context the application context
     * @return all the dex path
     * @throws PackageManager.NameNotFoundException
     * @throws IOException
     */
    public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
        ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        File sourceApk = new File(applicationInfo.sourceDir);
        File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);

        List<String> sourcePaths = new ArrayList<String>();
        sourcePaths.add(applicationInfo.sourceDir); //add the default apk path

        //the prefix of extracted file, ie: test.classes
        String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        //the total dex numbers
        int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);

        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()) {
                sourcePaths.add(extractedFile.getAbsolutePath());
                //we ignore the verify zip part
            } else {
                throw new IOException("Missing extracted secondary dex file '" +
                        extractedFile.getPath() + "'");
            }
        }

        return sourcePaths;
    }

    /**
     * get all the classes name in "classes.dex", "classes2.dex", ....
     *
     * @param context the application context
     * @return all the classes name
     * @throws PackageManager.NameNotFoundException
     * @throws IOException
     */
    public static List<String> getAllClasses(Context context) throws PackageManager.NameNotFoundException, IOException {
        List<String> classNames = new ArrayList<String>();
        for (String path : getSourcePaths(context)) {
            try {
                DexFile dexfile = null;
                if (path.endsWith(EXTRACTED_SUFFIX)) {
                    //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                    dexfile = DexFile.loadDex(path, path + ".tmp", 0);
                } else {
                    dexfile = new DexFile(path);
                }
                Enumeration<String> dexEntries = dexfile.entries();
                while (dexEntries.hasMoreElements()) {
                    classNames.add(dexEntries.nextElement());
                }
            } catch (IOException e) {
                throw new IOException("Error at loading dex file '" +
                        path + "'");
            }
        }
        return classNames;
    }
}

Upvotes: 6

Related Questions