Ivelius
Ivelius

Reputation: 5013

Find All Subclasses of a given class (Android)

I've used Reflections Library to Find All Subclasses of a given class in Java before.

This is a code snippet that I use in a simple java project :

    Reflections reflections = new Reflections(PACKAGE_NAME);

    Set<Class<? extends SuperClass>> subTypes =
            reflections.getSubTypesOf(SuperClass.class);

    for (Class<? extends SuperClass> subType : subTypes) {

        log("Subclass = " + subType.getSimpleName());
    }

When I run the same code in android project , "subType" List returns empty.

Can Anybody help me to get this work on Android ?

EDIT Jars I add for the whole thing to work are :

Upvotes: 5

Views: 3214

Answers (5)

Vasilii Shumilov
Vasilii Shumilov

Reputation: 301

Here is a Kotlin solution that also works with interfaces. What you need is a real Context instance, so be aware that you are not able to use this approach in unit-tests. Note that DexFile is deprecated due to API fragility with no useful replacement.

private inline fun <reified T> getAllSubclasses(
    startsWithPackage: String,
    context: Context,
): Set<Class<out T>> {
    val classNames = DexFile(context.packageCodePath).entries()
    val classLoader = context.classLoader

    val superclass = T::class.java

    val result = mutableSetOf<Class<out T>>()

    classNames.iterator().forEach { className ->
        if (className.startsWith(startsWithPackage) && className.contains("$").not()) {
            val loadedClass = classLoader.loadClass(className)
            if (loadedClass.superclass == superclass || loadedClass.interfaces.contains(superclass)) {
                @Suppress("UNCHECKED_CAST")
                result.add(loadedClass as Class<out T>)
            }
        }
    }

    return result
}

The method above could be used like that.

val subclasses = getAllSubclasses<SuperClass>("com.example", context)

Upvotes: 0

Sam Sch
Sam Sch

Reputation: 672

Here's my one-method-solution based on fantouch's answer.
Result will also contain inderect descendants.

public class Utils
{
    // ...

    public static ArrayList<Class> GetSubClasses( Context context, String packageName, Class targetSuperClass )
    {
        ArrayList<Class> subClasses = new ArrayList<>();

        try
        {
            DexFile dex = new DexFile( context.getPackageCodePath() );
            for ( Enumeration<String> iterator = dex.entries(); iterator.hasMoreElements(); )
            {
                String className = iterator.nextElement();

                if ( !className.startsWith( packageName ) || className.contains("$") )
                {
                    continue;
                }

                Class classObj = Class.forName( className );

                Class superClass = classObj;
                while ( true )
                {
                    superClass = superClass.getSuperclass();

                    if ( superClass == null || superClass == Object.class )
                    {
                        break;
                    }

                    if ( superClass == targetSuperClass )
                    {
                        subClasses.add( classObj );
                        break;
                    }
                }
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }

        return subClasses;
    }
}

Upvotes: 0

fantouch
fantouch

Reputation: 1169

maybe you can try this:

public abstract class ClassScanner {

    private static final String TAG = "ClassScanner"; 
    private Context mContext;

    public ClassScanner(Context context) {
        mContext = context;
    }

    public Context getContext() {
        return mContext;
    }

    void scan() throws IOException, ClassNotFoundException, NoSuchMethodException {
        long timeBegin = System.currentTimeMillis();

        PathClassLoader classLoader = (PathClassLoader) getContext().getClassLoader();
        //PathClassLoader classLoader = (PathClassLoader) Thread.currentThread().getContextClassLoader();//This also works good
        DexFile dexFile = new DexFile(getContext().getPackageCodePath());
        Enumeration<String> classNames = dexFile.entries();
        while (classNames.hasMoreElements()) {
            String className = classNames.nextElement();
            if (isTargetClassName(className)) {
                //Class<?> aClass = Class.forName(className);//java.lang.ExceptionInInitializerError
                //Class<?> aClass = Class.forName(className, false, classLoader);//tested on 魅蓝Note(M463C)_Android4.4.4 and Mi2s_Android5.1.1
                Class<?> aClass = classLoader.loadClass(className);//tested on 魅蓝Note(M463C)_Android4.4.4 and Mi2s_Android5.1.1
                if (isTargetClass(aClass)) {
                    onScanResult(aClass);
                }
            }
        }

        long timeEnd = System.currentTimeMillis();
        long timeElapsed = timeEnd - timeBegin;
        Log.d(TAG, "scan() cost " + timeElapsed + "ms");
    }

    protected abstract boolean isTargetClassName(String className);

    protected abstract boolean isTargetClass(Class clazz);

    protected abstract void onScanResult(Class clazz);
}

and this is a example how to use:

    new ClassScanner(context) {

    @Override
    protected boolean isTargetClassName(String className) {
        return className.startsWith(getContext().getPackageName())//I want classes under my package
                && !className.contains("$");//I don't need none-static inner classes
    }

    @Override
    protected boolean isTargetClass(Class clazz) {
        return AbsFactory.class.isAssignableFrom(clazz)//I want subclasses of AbsFactory
                && !Modifier.isAbstract(clazz.getModifiers());//I don't want abstract classes
    }

    @Override
    protected void onScanResult(Class clazz) {
        Constructor constructor = null;
        try {
            constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            constructor.newInstance();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}.scan();

Upvotes: 2

Marcin Łoś
Marcin Łoś

Reputation: 3246

There is an official API for dealing with dex files. See here for details. Basically, given path of the .dex file, you can enumerate all the files inside it. To get the URL of the classes.dex, you can use

  Thread.currentThread().getContextClassLoader().getResource("classes.dex")

(you may need to cut the trailing "!/classes.dex", can't check it right now). Then, you can iterate over the classes.dex contents and attempt to load all the classes you find this way. (Class.forName), and, finally, check whatever class properties you need. Keep in mind, however, that classes.dex contains all the classes, and "all" can be "quite a lot". There are also other performance issues to remember when dealing with .dex files. You should, however, be fine, as long as you don't attempt something needlessly inefficient. Especially since Reflections accomplishes its magic in a similar manner, as far as I remamber.

Upvotes: 0

mach
mach

Reputation: 8395

This might not work due to the compilation process used in Android. The source code is converted into .class files with the Java compiler. The next step turns the .class files into Android .dex files (Dalvik bytecode) and they are unlikely to keep all meta-data.

http://developer.android.com/tools/building/index.html

Upvotes: 1

Related Questions