在Android应用程序中,是否可能在运行时动态加载库?

76

有没有办法在运行时使Android应用程序下载并使用Java库?

以下是一个示例:

假设应用程序需要根据输入值进行一些计算。 应用程序请求这些输入值,然后检查所需的 Class Method 是否可用。

如果不可用,它会连接到服务器,下载所需的库,并在运行时加载库,使用反射技术调用所需方法。 实现可能会随着下载库的用户等各种标准而改变。

7个回答

98

抱歉,我来晚了,问题已经有一个被接受的答案了,但是是的,你可以下载并执行外部库。这是我所做的方法:

我想知道这是否可行,所以我编写了以下类:

package org.shlublu.android.sandbox;

import android.util.Log;

public class MyClass {
    public MyClass() {
        Log.d(MyClass.class.getName(), "MyClass: constructor called.");
    }

    public void doSomething() {
        Log.d(MyClass.class.getName(), "MyClass: doSomething() called.");
    }
}

我将它打包成一个DEX文件,并将其保存在设备的SD卡上,路径为 /sdcard/shlublu.jar

然后我编写了下面这个“愚蠢的程序”,在从Eclipse项目中移除了 MyClass 并进行清理之后:

public class Main extends Activity {

    @SuppressWarnings("unchecked")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try {
            final String libPath = Environment.getExternalStorageDirectory() + "/shlublu.jar";
            final File tmpDir = getDir("dex", 0);

            final DexClassLoader classloader = new DexClassLoader(libPath, tmpDir.getAbsolutePath(), null, this.getClass().getClassLoader());
            final Class<Object> classToLoad = (Class<Object>) classloader.loadClass("org.shlublu.android.sandbox.MyClass");

            final Object myInstance  = classToLoad.newInstance();
            final Method doSomething = classToLoad.getMethod("doSomething");

            doSomething.invoke(myInstance);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

它基本上是这样加载类MyClass:

  • 创建一个DexClassLoader

  • 用它从"/sdcard/shlublu.jar"中提取类MyClass

  • 并将此类存储到应用程序的"dex"私有目录(手机的内部存储器)中。

然后,它创建了MyClass的实例,并在创建的实例上调用doSomething()

这样就可以工作了...... 我在LogCat中看到了MyClass中定义的跟踪:

enter image description here

我在2.1模拟器和我的物理HTC手机(运行Android 2.2,未root)上都尝试过了。

这意味着您可以为应用程序创建外部DEX文件,以便下载并执行它们。虽然这里使用了一些不太优雅的方式(丑陋的Object强制转换,Method.invoke()丑陋的调用...),但应该可以使用Interface来实现更简洁的方式。

哇。我很惊讶,我以为会出现SecurityException

以下是一些有助于进一步调查的事实:

  • 我的DEX shlublu.jar已签名,但我的应用程序没有
  • 我的应用程序从Eclipse / USB连接执行。因此,这是一个在DEBUG模式下编译的未签名APK

4
太好了!这真是我一直在寻找的!!正如您在这里看到的(http://groups.google.com/group/android-security-discuss/browse_thread/thread/e928d79627867246)一样,没有真正的安全问题,因为代码将使用您的应用程序权限执行。 - llullulluis
1
非常好,谢谢!是的,这正是相同的逻辑。超过64k个方法肯定很不寻常,因此这个功能的有效用途肯定是插件。 - Shlublu
1
我知道这是一个旧的帖子,但我希望这里有人能够帮助。我正在尝试做同样的事情(动态加载插件)。有人通过转换为接口使其工作了吗? - cstrutton
你是如何将classes.dex转换为jar文件的? - Dhrupal
显示剩余21条评论

16

Shlublu的答案非常好。以下是一些小细节可以帮助初学者:

  • 对于库文件"MyClass",创建一个单独的Android应用程序项目,其中src文件夹中只有MyClass文件(其他东西,如project.properties、manifest、res等,也应该在那里)
  • 在库项目清单中确保你有:
   <application android:icon="@drawable/icon" 
                android:label="@string/app_name">
        <activity android:name=".NotExecutable" 
                  android:label="@string/app_name">
        </activity>
    </application>

(".NotExecutable"不是保留字。只是我必须在这里放点什么)

  • 要制作.dex文件,只需将库项目作为Android应用程序运行(进行编译),然后从项目的bin文件夹中找到.apk文件。
  • 将.apk文件复制到您的手机上,并将其重命名为shlublu.jar文件(尽管APK实际上是jar的特化)

其他步骤与Shlublu所述相同。

  • 感谢Shlublu的合作。

欢迎Pätris,很高兴与您讨论。 (当我+1您的答案时,我最初写下了这条评论,但显然忘记按“添加评论”按钮...) - Shlublu
我很感谢这个方法,但是使用你的方法复制 @Shlublu 上面的示例会得到一个大约380kB的apk文件,然而如果我只是创建一个库项目,我可以获得一个大小为3kB的jar文件。然而,库方法需要额外的命令:dx --dex --keep-classes --output=newjar.jar libproject.jar。也许有一种方法可以在Eclipse构建库时自动执行这个dex步骤。 - Harvey
我很困惑为什么要使用apk文件?!我是Xamarin Android的新手。我想创建库项目dll(类库),然后在运行时在我的主应用程序中使用它。如何实现这一点? - Mohammad Afrashteh
@Pätris - 我尝试了你的方法(跳过了包含清单条目的第二步),并没有将名称更改为.jar。相反,将apk保留为默认名称app-debug.apk,并修改了Shlublu的代码(只更改了文件名)。我遇到了“ClassNotFound”异常。我正在Android 6.0上运行。 我有什么遗漏吗? - node_analyser

4
技术上应该可行,但谷歌政策如何?请参考以下网址:play.google.com/intl/zh-CN/about/developer-content-policy-print。在Google Play上分发的应用程序不得使用任何方法修改、替换或更新自身,除了Google Play的更新机制。同样,应用程序不得从Google Play以外的来源下载可执行代码(例如dex、JAR、.so文件)。此限制不适用于在虚拟机中运行并且对Android API有限制访问权限的代码(例如WebView或浏览器中的JavaScript)。

2
这是绝对正确的。然而,OP问是否在不使用Google Play的情况下技术上可行并且可能想为自己的目的做这样的事情。此外,看起来Google Play应用程序可能会嵌入JAR文件,这些文件将在部署时写入本地存储器,并且稍后由应用程序加载,而不会违反此规则(无论它是否是一个好主意)。但无论如何,您提醒开发人员从Google Play下载的应用程序不应该从其他地方下载代码是正确的。这应该是一条评论。 - Shlublu
2021年12月15日起,新的网址为: https://support.google.com/googleplay/android-developer/answer/9888379 - Thach Van

1

我不确定您是否可以通过动态加载Java代码来实现此目标。也许您可以尝试将脚本引擎嵌入到您的代码中,例如Rhino,它可以执行Java脚本,这些脚本可以被动态下载和更新。


谢谢。我会调查这种方法(今天之前不知道在Android上进行脚本编写)。如果有人需要更多信息,可以在这里找到:http://google-opensource.blogspot.com/2009/06/introducing-android-scripting.html - llullulluis
1
@Naresh 是的,你也可以下载并执行Java编译代码。请查看我在此页面上的回复。我实际上没有预料到这一点。 - Shlublu
1
谢谢@shlublu,看起来很有趣 : )。我能想到的唯一问题是它严重依赖反射或者你需要开始编写包装器,我看到了很多管理问题,如果我们必须以那种方式编写整个代码。这就是为什么我认为脚本是管理业务逻辑的简单而好的方法。 - Naresh
另外,您如何开始生成独立的dex jar文件?您会创建单独的Android应用程序项目吗?目前在Android中没有静态库的概念,对吧? - Naresh
我在这里做的是:我导出了一个仅包含MyClass的Java项目。这生成了一个符合Dalvik格式的有效Dex文件。 - Shlublu
1
看一下你设备的/system/framework目录,里面充满了以这种方式制作的JAR文件。(无需root权限) - Shlublu

1

我认为@Shlublu的答案是正确的,但我想强调一些关键点。

  1. We can load any classes from external jar and apk file.
  2. In Any way, we can load Activity from external jar but we can not start it because of the context concept.
  3. To load the UI from external jar we can use fragment. Create the instance of the fragment and embedded it in the Activity. But make sure fragment creates the UI dynamically as given below.

    public class MyFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup 
      container, @Nullable Bundle savedInstanceState)
     {
      super.onCreateView(inflater, container, savedInstanceState);
    
      LinearLayout layout = new LinearLayout(getActivity());
      layout.setLayoutParams(new 
     LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.MATCH_PARENT));
    Button button = new Button(getActivity());
    button.setText("Invoke host method");
    layout.addView(button, LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT);
    
    return layout;
     }
    }
    

1
如果您将.DEX文件保存在手机的外部存储器中,例如SD卡(不建议这样做!任何具有相同权限的应用程序都可以轻松覆盖您的类并执行代码注入攻击),请确保已经授予该应用程序读取外部存储器的权限。如果是这种情况,则会抛出一个“ClassNotFound”异常,这是非常具有误导性的,可以在清单中添加以下内容(请查阅谷歌获取最新版本)。
<manifest ...>

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="18" />
    ...
</manifest>

1
我可以帮助您翻译以下内容:

当然,这是可能的。未安装的apk可以由主android应用程序调用。一般来说,需要解决资源和activity的生命周期问题,然后才能动态加载jar或apk。 详情请参考我在Github上的开源研究: https://github.com/singwhatiwanna/dynamic-load-apk/blob/master/README-en.md

此外,需要使用DexClassLoader和反射,现在看一些关键代码:

/**
 * Load a apk. Before start a plugin Activity, we should do this first.<br/>
 * NOTE : will only be called by host apk.
 * @param dexPath
 */
public DLPluginPackage loadApk(String dexPath) {
    // when loadApk is called by host apk, we assume that plugin is invoked by host.
    mFrom = DLConstants.FROM_EXTERNAL;

    PackageInfo packageInfo = mContext.getPackageManager().
            getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
    if (packageInfo == null)
        return null;

    final String packageName = packageInfo.packageName;
    DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
    if (pluginPackage == null) {
        DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
        AssetManager assetManager = createAssetManager(dexPath);
        Resources resources = createResources(assetManager);
        pluginPackage = new DLPluginPackage(packageName, dexPath, dexClassLoader, assetManager,
                resources, packageInfo);
        mPackagesHolder.put(packageName, pluginPackage);
    }
    return pluginPackage;
}

您的要求只是在开源项目中部分功能。

我在使用你的项目时遇到了一些错误,它仍然存在与我的先前项目相同的问题。当启动活动时出现ClassNotFoundException错误。 - KDoX
我是Android开发的新手。我不知道为什么我们应该使用APK来动态加载方法?!我想在运行时动态加载由类库项目创建的DLL。 - Mohammad Afrashteh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接