简体   繁体   English

Class.getDeclaredMethods() 返回继承的默认方法

[英]Class.getDeclaredMethods() returns inherited default methods

The method getDeclaredMethods , when called on a class object, is supposed to return an array of Method objects representing the methods that are declared directly as part of that class.当在类对象上调用方法getDeclaredMethods时,应该返回一个 Method 对象数组,表示直接声明为该类一部分的方法。 It's not supposed to return any inherited methods.它不应该返回任何继承的方法。

This works fine when I install my app directly via Android Studio, regardless of the active build variant .当我直接通过 Android Studio 安装我的应用程序时,无论活动构建变体如何,这都可以正常工作。 Switching to a release build is not sufficient to trigger the problem.切换到发布版本不足以触发问题。

The problem arises when compiling an APK or App Bundle (.aab) and installing the app that way.编译 APK 或 App Bundle (.aab) 并以这种方式安装应用程序时会出现问题。 (Either directly by copying the APK onto a device, or rolling out the bundle on the Google Play Store and installing the app from there.) (直接将 APK 复制到设备上,或者在 Google Play 商店推出捆绑包并从那里安装应用程序。)

Here's my test scenario, in a fresh Android Studio project, using SDK 33, minSdk 21 (Android 5.0), minifyEnabled false , and the default proguardFiles statement deleted, to make sure this isn't caused by R8 / ProGuard.这是我的测试场景,在一个新的 Android Studio 项目中,使用 SDK 33、 minSdk 21 (Android 5.0)、 minifyEnabled false并删除了默认的proguardFiles语句,以确保这不是由 R8 / ProGuard 引起的。

The interface:界面:

// TestInterface.java

package com.example.testapp;

public interface TestInterface {
    default String methodWithDefault() {
        return "default";
    }

    String methodWithoutDefault();
}

The implementing class:实现类:

// TestClass.java

package com.example.testapp;

public class TestClass implements TestInterface {
    @Override
    public String methodWithoutDefault() {
        return "non-default";
    }
}

The test case:测试用例:

// MainActivity.java

package com.example.testapp;

import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

Contents of app/build.gradle : app/build.gradle的内容:

plugins {
    id 'com.android.application'
}

android {
    namespace 'com.example.testapp'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.testapp"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
        }
    }
    compileOptions {
        sourceCompatibility 11
        targetCompatibility 11
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.1'
    implementation 'com.google.android.material:material:1.7.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

Output when running directly from Android Studio:直接从 Android Studio 运行时的输出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Result: non-default

Output when building an APK and installing it on the device:构建 APK 并将其安装到设备时的输出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithDefault()
Result: default

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Result: non-default

Questions:问题:

  1. Why does this happen?为什么会这样?
  2. What's the best way to work around it?解决它的最佳方法是什么?

In typical rubber-duck debugging fashion, I found out some important details and how to work around this while improving the test-case before posting it on StackOverflow...在典型的橡皮鸭调试方式中,我发现了一些重要的细节以及如何在改进测试用例的同时解决这个问题,然后再将其发布到 StackOverflow 上......

First, let's have some properties of the methods printed as well.首先,让我们也打印一些方法的属性。 We can modify MainActivity as follows:我们可以修改MainActivity如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            sb.append("Synthetic: ").append(m.isSynthetic()).append('\n');
            sb.append("Bridge: ").append(m.isBridge()).append('\n');
            sb.append("Default: ").append(m.isDefault()).append('\n');
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

This makes Android Studio complain, because isDefault() is only available starting from SDK 24, but our minSdk is 21.这让 Android Studio 抱怨,因为isDefault()仅从 SDK 24 开始可用,但我们的 minSdk 是 21。

Let's increase minSdk to 24 and check the output:让我们将 minSdk 增加到 24并检查输出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Synthetic: false
Bridge: false
Default: false
Result: non-default

Oh, the inherited method is gone, If you play around with minSdk.哦,继承的方法没有了,如果你玩玩 minSdk。 you'll realize the issue appears with any value <= 23, So: we make the first important realization:你会发现问题出现在任何 <= 23 的值上,所以:我们做出第一个重要的认识:

1. The problem only arises if minSdk is less than 24. 1.只有当minSdk小于24时才会出现这个问题。

(Note that the actual SDK version of the Android device on which you're installing the APK doesn't seem to matter; I'm testing this all on an SDK 25 / Android 7.1.1 device.) (请注意,您安装 APK 的 Android 设备的实际 SDK 版本似乎并不重要;我正在 SDK 25 / Android 7.1.1 设备上测试这一切。)

Let's switch back to minSdk 21, and make the call to isDefault conditional on an SDK version check, like so:让我们切换回 minSdk 21,并以 SDK 版本检查为条件调用isDefault ,如下所示:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TestClass test = new TestClass();
        StringBuilder sb = new StringBuilder("Methods:\n");
        for (Method m : TestClass.class.getDeclaredMethods()) {
            sb.append('\n').append(m.toString()).append('\n');
            sb.append("Synthetic: ").append(m.isSynthetic()).append('\n');
            sb.append("Bridge: ").append(m.isBridge()).append('\n');
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                sb.append("Default: ").append(m.isDefault()).append('\n');
            }
            try {
                String s = (String) m.invoke(test);
                sb.append("Result: ").append(s).append('\n');
            } catch (InvocationTargetException e) {
                sb.append("Target exception: ").append(e.getTargetException()).append('\n');
            } catch (IllegalAccessException e) {
                sb.append("Illegal access.\n");
            }
        }

        System.out.println(sb);

        TextView textView = findViewById(R.id.textView);
        textView.setText(sb.toString());
    }
}

This yields the following output when installing the new APK on a device with SDK version 24 or newer:在使用 SDK 版本 24 或更高版本的设备上安装新 APK 时,这会产生以下输出:

Methods:

public java.lang.String com.example.testapp.TestClass.methodWithDefault()
Synthetic: true
Bridge: false
Default: false
Result: default

public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Synthetic: false
Bridge: false
Default: false
Result: non-default

Confusingly, the method is not marked as default.令人困惑的是,该方法标记为默认方法。 But it's marked as synthetic, so:但它被标记为合成的,所以:

2. We can filter out the inherited methods via Method.isSynthetic() . 2. 我们可以通过Method.isSynthetic()过滤掉继承的方法。

So, to answer our questions:所以,回答我们的问题:

  1. Why does this happen?为什么会这样?

I suppose the synthetic method is being generated when minSdk is less than 24 because the APK could be installed on an older Android device which doesn't have "direct" support for default interface methods in its JVM.我想合成方法是在 minSdk 小于 24 时生成的,因为 APK可以安装在旧的 Android 设备上,它的 JVM 中没有对默认接口方法的“直接”支持。

When installing the app directly via Android Studio, I guess the minSdk value is ignored, since Android Studio can check what the actual version of the device is.当直接通过 Android Studio 安装应用程序时,我猜 minSdk 值被忽略了,因为 Android Studio 可以检查设备的实际版本是什么。

If anyone has more exact information, please share.如果有人有更准确的信息,请分享。

  1. What's the best way to work around it?解决它的最佳方法是什么?

The inherited default methods can be filtered out by calling isSynthetic() on them, which will return true .可以通过对它们调用isSynthetic()来过滤掉继承的默认方法,这将返回true

If you want to keep some other synthetic methods and only filter out these ones, I don't know how to achieve that, but that should be an exceedingly rare situation.如果你想保留一些其他的合成方法,只过滤掉这些方法,我不知道如何实现,但这种情况应该是非常罕见的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM