Android 遍历Apk下的所有类文件

这篇博客介绍了如何在Android项目中遍历包含多 dex 文件的Apk中的所有类。传统方法只能遍历主dex文件,但通过反射BaseDexClassLoader或者ApplicationInfo可以获取到所有dex文件路径,然后遍历每个dex文件下的类。文章提供了具体的代码实现,包括通过反射获取dex文件和处理Instant Run情况的细节。
摘要由CSDN通过智能技术生成

Android 遍历Apk下的所有类文件

在项目里面大家可能会有这样的需求,获取特定包名下的所有类进行实例化;然后网上大部分帖子都是说用下面这个方法来实现

 /**
     *
     * @Description: 根据包名获得该包以及子包下的所有类
     * @param packageName 包名
     * @return List<Class>    包下所有类
     */
    private List<Class> getClasses(String packageCodePath,String packageName) throws ClassNotFoundException,IOException {
        DexFile df = new DexFile(packageCodePath);//通过DexFile查找当前的APK中可执行文件
        Enumeration<String> enumeration = df.entries();//获取df中的元素  这里包含了所有可执行的类名 该类名包含了包名+类名的方式
        List<Class> classes = new ArrayList<>();
        while(enumeration.hasMoreElements()){
            String  className = enumeration.nextElement();
            if (className.contains(packageName)) {
                XLog.d(TAG,"find init class path :%s",className);
                classes.add(Class.forName(className));
            }
        }
        return classes;
    }

这个方法实际上是有缺陷的,一旦项目进行了分包,存在多个dex文件,上面的方法就只能遍历主dex文件下的所有类。

那么在分包的情况下如何遍历apk下所有的类呢,实际上只需要拿到apk下所有的dex文件路径,再用上面的方法依次遍历所有dex文件下的类就行了,废话不多说,下面贴上具体实现代码

public class DexUtils {
    public static ArrayList<DexFile> getMultiDex()
    {
        BaseDexClassLoader dexLoader = (BaseDexClassLoader) Thread.currentThread().getContextClassLoader();
        Field f = getField("pathList", getClassByAddressName("dalvik.system.BaseDexClassLoader"));
        Object pathList = getObjectFromField(f, dexLoader);
        Field f2 = getField("dexElements", getClassByAddressName("dalvik.system.DexPathList"));
        Object[] list = getObjectFromField(f2, pathList);
        Field f3 = getField("dexFile", getClassByAddressName("dalvik.system.DexPathList$Element"));

        ArrayList<DexFile> res = new ArrayList<>();

        for(int i = 0; i < list.length; i++)
        {
            DexFile d = getObjectFromField(f3, list[i]);
            res.add(d);
        }

        return res;
    }

    private static Field getField(String field, Class<?> className) {
        try {
            return className.getClass().getDeclaredField(field);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public static Class<?> getClassByAddressName(String classAddressName)
    {
        Class mClass = null;
        try
        {
            mClass = Class.forName(classAddressName);
        } catch(Exception e)
        {
        }
        return mClass;
    }


    public static <T extends Object> T getObjectFromField(Field field, Object arg)
    {
        try
        {
            field.setAccessible(true);
            return (T) field.get(arg);
        } catch(Exception e)
        {
            e.printStackTrace();
            return null;
        }
    }

除了上门通过反射BaseDexClassLoader拿到所有dex文件的方式外,还可以通过ApplicationInfo拿到所有dex文件

public class ClassUtils {
    private static final String TAG = "ClassUtils";

    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 final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;

    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);
    }

    /**
     * 通过指定包名,扫描包下面包含的所有的ClassName
     *
     * @param context     U know
     * @param packageName 包名
     * @return 所有class的集合
     */
    public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
        final Set<String> classNames = new HashSet<>();

        List<String> paths = getSourcePaths(context);
        final CountDownLatch parserCtl = new CountDownLatch(paths.size());

        for (final String path : paths) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexfile = null;

                    try {
                        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()) {
                            String className = dexEntries.nextElement();
                            if (className.startsWith(packageName)) {
                                classNames.add(className);
                            }
                        }
                    } catch (Throwable ignore) {
                        Log.e("ARouter", "Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null != dexfile) {
                            try {
                                dexfile.close();
                            } catch (Throwable ignore) {
                            }
                        }

                        parserCtl.countDown();
                    }
                }
            });
        }

        parserCtl.await();

        Log.d(TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
        return classNames;
    }

    /**
     * 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);

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

        //the prefix of extracted file, ie: test.classes
        String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

//        如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
//        通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
        if (!isVMMultidexCapable()) {
            //the total dex numbers
            int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
            File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);

            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() + "'");
                }
            }
        }
        sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
        return sourcePaths;
    }

    /**
     * Get instant run dex path, used to catch the branch usingApkSplits=false.
     */
    private static List<String> tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) {
        List<String> instantRunSourcePaths = new ArrayList<>();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs) {
            // add the split apk, normally for InstantRun, and newest version.
            instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs));
            Log.d(TAG, "Found InstantRun support");
        } else {
            try {
                // This man is reflection from Google instant run sdk, he will tell me where the dex files go.
                Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths");
                Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class);
                String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName);

                File instantRunFilePath = new File(instantRunDexPath);
                if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) {
                    File[] dexFile = instantRunFilePath.listFiles();
                    for (File file : dexFile) {
                        if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex")) {
                            instantRunSourcePaths.add(file.getAbsolutePath());
                        }
                    }
                    Log.d(TAG, "Found InstantRun support");
                }

            } catch (Exception e) {
                Log.e(TAG, "InstantRun support error, " + e.getMessage());
            }
        }

        return instantRunSourcePaths;
    }

    /**
     * Identifies if the current VM has a native support for multidex, meaning there is no need for
     * additional installation by this library.
     *
     * @return true if the VM handles multidex
     */
    private static boolean isVMMultidexCapable() {
        boolean isMultidexCapable = false;
        String vmName = null;

        try {
            if (isYunOS()) {    // YunOS需要特殊判断
                vmName = "'YunOS'";
                isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
            } else {    // 非YunOS原生Android
                vmName = "'Android'";
                String versionString = System.getProperty("java.vm.version");
                if (versionString != null) {
                    Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
                    if (matcher.matches()) {
                        try {
                            int major = Integer.parseInt(matcher.group(1));
                            int minor = Integer.parseInt(matcher.group(2));
                            isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
                                    && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
                        } catch (NumberFormatException ignore) {
                            // let isMultidexCapable be false
                        }
                    }
                }
            }
        } catch (Exception ignore) {

        }

        Log.i(TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
        return isMultidexCapable;
    }

    /**
     * 判断系统是否为YunOS系统
     */
    private static boolean isYunOS() {
        try {
            String version = System.getProperty("ro.yunos.version");
            String vmName = System.getProperty("java.vm.name");
            return (vmName != null && vmName.toLowerCase().contains("lemur"))
                    || (version != null && version.trim().length() > 0);
        } catch (Exception ignore) {
            return false;
        }
    }
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android系统中,要获取已安装的apk文件,可以使用以下步骤: 1. 首先,要获取apk文件,需要先获取应用的包名。可以使用PackageManager来获取应用程序的信息。例如,可以使用getInstalledPackages()方法获取已安装应用的列表,然后通过遍历列表来获取每个应用程序的包名。 2. 获取包名后,可以通过PackageManager的getApplicationInfo()方法来获取应用程序的详细信息,包括其数据目录。 3. 在应用程序详细信息中,可以通过applicationInfo.sourceDir来获取apk文件的路径。该路径可以用于读取apk文件的内容或者进行一些其他操作。 例如,可以使用以下代码片段来获取已安装应用的apk文件路径: PackageManager packageManager = getPackageManager(); List<PackageInfo> packageList = packageManager.getInstalledPackages(0); for(PackageInfo packageInfo : packageList) { ApplicationInfo appInfo = packageManager.getApplicationInfo(packageInfo.packageName, 0); String apkPath = appInfo.sourceDir; // 可以使用apkPath来进行apk文件的相关操作 } 需要注意的是,为了获取已安装应用的详细信息,需要在AndroidManifest.xml文件中添加相应的权限。例如,<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />可以用于获取应用程序的大小,而<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />可以用于读取外部存储的应用文件。 总之,通过PackageManager和ApplicationInfo,可以获取已安装应用的详细信息,包括apk文件的路径。根据这些信息,可以对apk文件进行一些操作,如读取内容或进行备份等。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值