Android:如何调用其他 API 级别中存在的方法?

Posted

技术标签:

【中文标题】Android:如何调用其他 API 级别中存在的方法?【英文标题】:Android: How do I call a method which is existing in other API Level? 【发布时间】:2011-11-16 15:08:57 【问题描述】:

我有使用 android 2.1 的应用程序,它利用 LocationManager 来获取高度。但是现在,我需要使用需要 API 级别 9 (2.3) 的 SensorManager 来获取高度。

如何通过放置条件并通过函数名调用它(在普通 Java 中可能)将SensorManager.getAltitude(float, float) 放入我的 2.1 android 应用程序中?

提前谢谢你

更新 1 如果您注意到我的应用程序需要使用 Android 2.1 编译。这就是为什么我正在寻找一种通过名称或任何其他可以编译的方式调用函数的方法。

【问题讨论】:

【参考方案1】:

您需要针对您需要的最高 api 进行构建,然后为您想要支持的其他级别有条件地编写备用代码路径

要在执行时检查当前 API 级别,Android 文档的最新建议是执行以下操作:

    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD)
    
        ...

不过,一旦引入了这种复杂性,就必须非常小心。目前没有自动检查所有代码路径的方法,以确保 minSdkVersion 之上的所有 api 级别调用都有替代调用来支持所有版本。如果存在可以执行此类操作的单元测试工具,也许有人可以加入。

【讨论】:

您检查API级别是正确的,但我问的是如何调用不属于编译平台的函数(在我的情况下是2.1,函数是2.3) 我第一句话就回答了。你不能。在您的情况下(Android 2.3),您必须针对 api 级别 9 构建,否则该方法不存在。 所以你说这是不可能的,除非根据相应的 api 级别构建它。 是的。如果从不存在该方法的库中导入 ClassA,则该方法不存在。您尝试调用的方法甚至没有在您尝试调用它的上下文中定义。为了使该方法存在,您必须导入包含该方法所在类的版本的库。 哦,是的,你是对的。即使它允许按名称调用函数,仍然会产生错误,因为它不存在。因此,如果我使用 API 级别 9 构建它,@schwiz 的答案是最好的方法吗?你们觉得呢……【参考方案2】:

您可以使用反射调用该方法,并在出现错误时优雅地失败(例如缺少类或方法)。见java.lang.reflect

其他选项是在第 9 级编译代码,但用 try/catch 包围以捕获在较低级别执行时可能出现的错误。不过,这可能很容易出错,我会三思而后行。


更新

这是测试代码

public void onCreate(Bundle savedInstanceState)

    try 
        // First we try reflection approach.
        // Expected result
        //    in 2.3 we print some value in log but no exception
        //    in 2.2 we print NoSuchMethodException
        // In both levels we get our screen displayed after catch
        Method m = SensorManager.class.getMethod("getAltitude",Float.TYPE, Float.TYPE);
        Float a = (Float)m.invoke(null, 0.0f, 0.0f);
        Log.w("test","Result 1: " + a);
     catch (Throwable e) 
        Log.e("test", "error 1",e);
    

    try 
        // Now we try compiling against 2.3
        // Expected result
        //    in 2.3 we print some value in log but no exception
        //    in 2.2 we print NoSuchMethodError (Note that it is an error not exception but it's still caught)
        // In both levels we get our screen displayed after catch
        float b = SensorManager.getAltitude(0.0f, 0.0f);
        Log.w("test","Result 2: " + b);

     catch (Throwable e) 
        Log.e("test", "error 2",e);
    

    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

结果:

2.3

09-14 07:04:50.374: DEBUG/dalvikvm(589): Debugger has detached; object registry had 1 entries
09-14 07:04:50.924: WARN/test(597): Result 1: NaN
09-14 07:04:51.014: WARN/test(597): Result 2: NaN
09-14 07:04:51.384: INFO/ActivityManager(75): Displayed com.example/.MyActivity: +1s65ms

2.2

09-14 07:05:48.220: INFO/dalvikvm(382): Could not find method android.hardware.SensorManager.getAltitude, referenced from method com.example.MyActivity.onCreate
09-14 07:05:48.220: WARN/dalvikvm(382): VFY: unable to resolve static method 2: Landroid/hardware/SensorManager;.getAltitude (FF)F
09-14 07:05:48.220: DEBUG/dalvikvm(382): VFY: replacing opcode 0x71 at 0x0049
09-14 07:05:48.220: DEBUG/dalvikvm(382): VFY: dead code 0x004c-0064 in Lcom/example/MyActivity;.onCreate (Landroid/os/Bundle;)V
09-14 07:05:48.300: ERROR/test(382): error 1
        java.lang.NoSuchMethodException: getAltitude
        at java.lang.ClassCache.findMethodByName(ClassCache.java:308)

跳过堆栈跟踪

09-14 07:05:48.300: ERROR/test(382): error 2
    java.lang.NoSuchMethodError: android.hardware.SensorManager.getAltitude
    at com.example.MyActivity.onCreate(MyActivity.java:35)

跳过更多堆栈跟踪

09-14 07:05:48.330: DEBUG/dalvikvm(33): GC_EXPLICIT freed 2 objects / 64 bytes in 180ms
09-14 07:05:48.520: INFO/ActivityManager(59): Displayed activity com.example/.MyActivity: 740 ms (total 740 ms)

【讨论】:

你不能只是试图捕捉这样的错误,如果你试图访问不存在的 api,类加载器会使应用程序崩溃。 你是说Android类加载器会崩溃而不是产生像NoClassDefFoundError这样的标准错误吗?你试过了吗? Dalvik VM 将抛出 VerifyError。 在 Android 中是否可以进行反射?你会分享它的代码 sn-p 吗? @Alex Gitelman 如果不将 API 调用嵌套在我的回答中的第二个类中,您将永远无法加载该类。【参考方案3】:

您可以利用在访问类之前不加载类的方式,以便轻松解决不需要反射的问题。您使用带有静态方法的内部类来使用您的新 API。这是一个简单的例子。

public static String getEmail(Context context)
    try
        if(Build.VERSION.SDK_INT > 4) return COMPATIBILITY_HACK.getEmail(context);
        else return "";
    catch(SecurityException e)
        Log.w(TAG, "Forgot to ask for account permisisons");
        return "";
    



//Inner class required so incompatibly phones won't through an error when this class is accessed. 
    //this is the island of misfit APIs
    private static class COMPATIBILITY_HACK

        /**
         * This takes api lvl 5+
         * find first gmail address in account and return it
         * @return
         */
        public static String getEmail(Context context)
            Account[] accounts = AccountManager.get(context).getAccountsByType("com.google");
            if(accounts != null && accounts.length > 0) return accounts[0].name;
            else return "";
        
     

【讨论】:

看起来很酷,但不知道如何使用 SensorManager 来实现它。你会分享它的代码 sn-p 吗? @eros 我已经提供了一个sn-p,你有什么不明白的?在 COMPATIBILITY_HACK 类中创建一个静态方法,使用您想要的 API。 SensorManager.getAltitude(float, float) 没有出现在智能感知中,因为我使用的是 2.1。还有可能吗? 基于@Rich cmets,您的答案仍然需要正确的API 级别。对吗? 你必须使用 2.2 sdk 或更高版本【参考方案4】:

当问题是“我在当前 API 级别是否有这个方法?”然后使用分支:

class SomeClass 
    public void someMethod()
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
        
           //use classes and/or methods that were added in GINGERBREAD
        
    

为此,您需要使用 Gingerbread 或更高版本的 Android 库。否则,代码将无法使用 Gingerbread 中添加的类进行编译。

这个解决方案比令人作呕的反射更干净。请注意,dalvik 将记录一个(非致命的)错误,指出在尝试加载 SomeClass 时找不到在 GINGERBREAD 中添加的类,但应用程序不会崩溃。如果我们尝试使用该特定类并进入 IF 分支,它只会崩溃 - 但我们不这样做(除非我们在 GINGERBREAD 或更高版本上)。

请注意,当您有一个永远存在的类但在 Gingerbread 中添加了一个新的方法时,该解决方案也有效。在运行时,如果您在 pre-Gingerbread 上运行,您只需不进入 IF 分支并且不调用该方法,因此应用程序不会崩溃。

【讨论】:

有没有办法让 Eclipse 中的 lint 知道发生了什么?我有上面的代码,但 lint 会警告我。我只能告诉它安静。但宁愿以一种干净的方式来做,所以我不会错过任何东西。 使用 @TargetApi 注释 - 它旨在为 Lint 提供有关您意图的提示。它会覆盖您正在注释的代码部分的清单最低 API 级别。例如,如果你用 @TargetApi(Build.VERSION_CODES.GINGERBREAD) 注释一个方法,那么你告诉 Lint 你知道你正在使用一些只有可用的东西,因为 Gingerbread 和 Lint 可以抑制它的警告。【参考方案5】:

这里你如何使用反射(从不可用的级别调用 StrictMode 类:

  try 
          Class<?> strictmode = Class.forName("android.os.StrictMode");
          Method enableDefaults = strictmode.getMethod("enableDefaults");
          enableDefaults.invoke(null, new Object[] );
   catch (Exception e) 
          Log.i(TAG, e.getMessage());
  

【讨论】:

【参考方案6】:

我还没有尝试过 - 但应该可以使用一些代码生成来创建一个代理库(每个 API 级别),它将包装整个本机 android.jar 并且其实现将尝试从android.jar。

此代理库将使用上述内部静态类方式或反射来使 dalvikvm 延迟链接到请求的方法。

它将让用户访问她想要的所有 API(假设她会检查正确的 API 级别)并防止令人不快的 dalvikvm 日志消息。您还可以嵌入每个方法的 API 级别并抛出一个可用的异常(BadApiLevelException 之类的)

(有人知道为什么 Google/Android 还没有做类似的事情吗?)

【讨论】:

以上是关于Android:如何调用其他 API 级别中存在的方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何在Android Studio中为消息“调用需要API级别21(当前最小值为16)”启用lint错误?

Android studio:调用需要 API 级别 16 错误

应用程序如何在没有 root 的情况下访问 Android 6.0(API 级别 23)中 USB OTG 存储上的文件?

在 Windows 上选择 Google API(API 级别 17 或任何其他 API 级别)时,Android 模拟器未加载

如何让信任锚在 Android API 级别 16-19 上正常工作

调用需要 API 级别 24(当前最低为 8):新的 android.location.GnssStatus.Callback