首页 > 解决方案 > 以编程方式/动态将 classes.dex 包含到 jar 文件中

问题描述

我正在寻找一种在运行时以编程方式将 jar 文件中的一些类和其他文件加载到我的 Android 应用程序中的方法。

这样做的目的是,借助 jar 文件中包含的类和其他文件,可以增加应用程序的功能。我曾尝试使用 java.net.URLClassLoader,但由于 Android Dalvik VM 只能加载包含“classes.dex”文件的 jar 文件,因此这不起作用。然后,这些特殊的 jar 文件将由DexClassLoader加载,如线程中所述。

但是,我正在寻找一种解决方案,其中可以以编程方式而不是手动方式从 jar 文件中创建 classes.dex 文件并将其添加到 jar 文件中。到目前为止,出于测试目的,我已尝试在我的操作系统(Windows10)的 cli 中手动执行此过程,但这也不起作用。这是我的 cli 的输出:

C:\Users\%USERNAME%\AppData\Local\Android\Sdk\build-tools\29.0.2>dx --dex --output=C:\Users\%USERNAME%\Downloads\classes.dex C:\Users\%USERNAME%\Downloads\Weather.jar
-Djava.ext.dirs=C:\Users\%USERNAME%\AppData\Local\Android\Sdk\build-tools\29.0.2\lib is not supported.  Use -classpath instead.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

注意:“%USERNAME%”已替换为有效用户名。出于安全原因,它只是被审查了。

这是我当前的类加载解决方案的代码,它在常规 JVM (JDK/JRE 9.0.4) 上运行良好,但在 Android 应用程序中却不行:

package chrtopf.ddns.net.smarthomeapp.plugins;

import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericSignatureFormatError;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.MalformedParameterizedTypeException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

import chrtopf.ddns.net.smarthomeapi.android.Plugin;

/**
 * This class is responsible for executing the highly unstable process of getting a new instance
 * of a plugins main class. There is a total number of 11 exceptions to be thrown during this
 * procedure. Every exception get printed with its stack trace and a custom designed fatal error
 * message. The one and only method this class makes use of is loadPlugin(). The plugins are
 * loaded through the URIClassLoader. ATTENTION: If the package name of a class to be loaded 
 * is the same as one of the package names from the application in which this PluginLoader class
 * is used a conflict between the two same named packages is created. In this conflict the same
 * named package of your application is always going to be chosen first INSTEAD of the package
 * from a different .jar archive file.
 * 
 * @author ChrTopf
 *
 */
public class PluginLoader {

    private static final String TAG = "PluginLoader";
    private PluginInterface app;
    
    /**
     * Initializes a new plugin loader object.
     * @param app The plugin interface which is going to be used in the constructor of the plugins
     * main class. This interface delivers access to important methods to the plugin. (PluginInterface)
     */ 
    public PluginLoader(PluginInterface app) {
        this.app = app;
    }
    
    /**
     * This method loads the main class of a plugin from a specific package of a specific .jar archive file.
     * @param plugin_name The name of the plugin (only for GUI and debug, but important). (String)
     * @param jar_file The existing .java archive file of the plugin to be loaded. (File)
     * @param main_path The package path to the plugins main class. (String)
     * @return returns a new Instance of the plugins main class. (Plugin)
     */
    public Plugin loadPlugin(String plugin_name, File jar_file, String main_path) {
        try {
            //get the url e.g. the path to the .jar file of the plugin
            URL file_path = jar_file.toURI().toURL();
            //try to load the .jar archive
            URLClassLoader classloader = new URLClassLoader(new URL[] {file_path});
            //load the main class from the archive as specified in the .properties file
            Class<?> plugin = classloader.loadClass(main_path);
            //get the constructor of the main plugin class
            Constructor<?> plugin_const = plugin.getDeclaredConstructor(PluginInterface.class);
            //prepare the constructor
            plugin_const.setAccessible(true);
            //get a new instance of the main plugin class using the prepared constructor
            Plugin new_plugin = (Plugin) plugin_const.newInstance(this.app);
            //close the classloader
            classloader.close();
            //return the new instance of the plugin
            return new_plugin;
        } catch (MalformedURLException e) {
            e.printStackTrace();
            Log.e(TAG, "The configuration file of the plugin with the name " + plugin_name + " has an incorrect path to the .jar archive!");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Log.e(TAG, "The configuration file of the plugin with the name " + plugin_name + " has an incorrect path to the main class! Or the plugin is made for a different smarthome server version.");
        } catch (TypeNotPresentException e) {
            e.printStackTrace();
            Log.e(TAG, "The main class of the plugin with the name " + plugin_name + " has no superclass, but it needs to be plugin.Plugin!");
        } catch (MalformedParameterizedTypeException | GenericSignatureFormatError e) {
            e.printStackTrace();
            Log.e(TAG, "This should definitely not happen. Please contact the author of the smarthome server application or verfiy that you used java version 13.0.1 for your plugin with the name " + plugin_name);
        } catch (InstantiationException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could somehow not be constructed from the plugin " + plugin_name);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could not be constructed from the plugin " + plugin_name + " because it is unknown to the smarthome server.");
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could not be constructed from the plugin " + plugin_name + " due to illegal Arguments.");
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could not be constructed from the plugin " + plugin_name + " due to the constructor of the main class throwing an exception.");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could not be constructed from the plugin " + plugin_name + " because a specific method could not be found in the main plugin class.");
        } catch (SecurityException e) {
            e.printStackTrace();
            Log.e(TAG, "A new instance of the main class could not be constructed from the plugin " + plugin_name + " due to missing file system rights.");
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "The classloader could not be closed successfully.");
        }
        return null;
    }
}

注意: loadPlugin() 方法从 jar 文件中返回一个类,该类是 Plugin 类的接口。

由于 Dalvik VM 无法从这种类型的 jar 文件中加载类,我在 LogCat 中收到以下错误:

2020-12-05 13:36:26.648 15121-15121/chrtopf.ddns.net.smarthomeapp2 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: chrtopf.ddns.net.smarthomeapp2, PID: 15121
    java.lang.RuntimeException: Unable to start activity ComponentInfo{chrtopf.ddns.net.smarthomeapp2/chrtopf.ddns.net.smarthomeapp.main.MainActivity}: java.lang.UnsupportedOperationException: can't load this type of class file
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3375)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3514)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2110)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7697)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
     Caused by: java.lang.UnsupportedOperationException: can't load this type of class file
        at java.lang.ClassLoader.defineClass(ClassLoader.java:591)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:469)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
        at java.security.AccessController.doPrivileged(AccessController.java:69)
        at java.security.AccessController.doPrivileged(AccessController.java:94)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at chrtopf.ddns.net.smarthomeapp.plugins.PluginLoader.loadPlugin(PluginLoader.java:59)
        at chrtopf.ddns.net.smarthomeapp.plugins.PluginWrap.load(PluginWrap.java:82)
        at chrtopf.ddns.net.smarthomeapp.plugins.PluginManager.loadPlugin(PluginManager.java:178)
        at chrtopf.ddns.net.smarthomeapp.plugins.PluginManager.startExec(PluginManager.java:131)
        at chrtopf.ddns.net.smarthomeapp.main.MainActivity.onCreate(MainActivity.java:105)
        at android.app.Activity.performCreate(Activity.java:7815)
        at android.app.Activity.performCreate(Activity.java:7804)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1325)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3350)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3514) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2110) 
        at android.os.Handler.dispatchMessage(Handler.java:107) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7697) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950) 

我正在使用 Android API Level 29 和 Android Studio 4.0.1

提前感谢您的支持!

标签: javaandroidandroid-studiojvmdalvik

解决方案


因此,经过大量研究和大量编码后,我终于可以为我的原始问题提供更好的解决方案/答案。

正如 CommonsWare 在我上一篇文章中评论的那样,我搜索了谷歌文档以找到一种以编程方式扩展我的应用程序功能的方法。我喜欢一个!它被称为“动态功能模块”或“按需交付”,是 android app bundle 的一部分。使用此功能,您可以在运行时为您的应用下载和安装附加功能。最好的部分是:它不会干扰 google play 发行条款!

但在你确定这个解决方案之前,这里有一个观点需要提到:

  1. 如果您想测试您的应用程序,尤其是按需动态功能模块的下载和安装过程,您需要将整个应用程序包(也是您的基本应用程序模块)上传到 google play。(提示:先使用“内部应用共享”进行测试)
  2. 要使第一点起作用,您需要拥有 google play 控制台的开发者帐户。
  3. 在您的应用程序项目中创建动态功能模块之前,您首先必须将其从 apk(即时应用程序)转换为 android 应用程序包。
  4. 可以在运行时使用 google play 核心库从您的应用程序请求和安装动态功能模块。(需要作为依赖项添加到您的基本应用程序模块中)
  5. 一个动态特性模块也可以包括额外的库(例如 jar 档案),就像一个基本的应用程序模块一样。所以在我的情况下,我可以坚持使用我已经准备好的 jar 文件来使用类加载器来实现和测试我的原始解决方案。
  6. 如果您的动态功能模块只需要包含一些库,您可以删除空的 java 包,如果您在 android studio 中创建新的动态功能模块,默认情况下会生成这些包。
  7. 最后但同样重要的是,您可以在动态功能模块中创建一个类的新实例(也适用于位于 dfm 中的库类),如下例所示:
//try to create a new instance of a plugin / some class from your library
try {
    Plugin plugin = (Plugin) Plugin.class.forName("your.java.library.classpath.to.class").newInstance();
    // do something with the newly created class
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (java.lang.InstantiationException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (Exception e){
    e.printStackTrace();
}

注意:此处仅使用名为“Plugin”的类作为示例。

我希望这也有助于你展示一旦你知道需要做什么,设计一个模块化的 android 应用程序是多么容易。我强烈建议您在深入了解该主题之前查看 google 文档并通读它们,因为它可能会变得有些混乱或复杂。从动态功能模块访问代码或资源时。


推荐阅读