在类对象上调用方法getDeclaredMethods
时,应该返回一个method对象数组,该数组表示直接声明为该类的一部分的方法。它不应该返回任何继承的方法。
当我直接通过Android Studio安装我的应用程序时,不管active build variant. 切换到一个发布版本并不足以触发这个问题。
当编译APK或App Bundle (.aab)并以这种方式安装应用程序时,会出现问题。(可以直接将APK复制到设备上,或者在Google Play Store上推出捆绑包并从那里安装应用程序。)
这是我的测试场景,在一个新的Android Studio项目中,使用SDK 33,minSdk 21
(Android 5.0),minifyEnabled false
和默认的proguardFiles
语句删除,以确保这不是由R8/ProGuard引起的。
接口:
// TestInterface.java
package com.example.testapp;
public interface TestInterface {
default String methodWithDefault() {
return "default";
}
String methodWithoutDefault();
}
实现类:
// TestClass.java
package com.example.testapp;
public class TestClass implements TestInterface {
@Override
public String methodWithoutDefault() {
return "non-default";
}
}
测试用例:
// 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());
}
}
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'
}
直接从Android Studio运行时的输出:
Methods:
public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Result: non-default
构建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
问题:
- 为什么会发生这种情况?
- 解决这个问题的最佳方法是什么?
在典型的橡皮鸭调试方式中,我发现了一些重要的细节以及如何在改进测试用例的同时解决这个问题,然后将其发布在StackOverflow上…
首先,让我们打印方法的一些属性。我们可以这样修改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());
}
}
这让Android Studio抱怨,因为isDefault()
只能从SDK 24开始使用,但我们的minSdk是21。
让我们将minSdk增加到24并检查输出:
Methods:
public java.lang.String com.example.testapp.TestClass.methodWithoutDefault()
Synthetic: false
Bridge: false
Default: false
Result: non-default
哦,继承的方法消失了!如果使用minSdk,您会发现任何值<= 23都会出现这个问题。所以,我们做了第一个重要的实现:
1。只有当minSdk小于24时才会出现问题。
(请注意,安装APK的Android设备的实际SDK版本似乎无关紧要;我正在SDK 25/Android 7.1.1设备上测试这一切。
让我们切换回minSdk 21,并将对isDefault
的调用作为SDK版本检查的条件,如下所示:
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());
}
}
当在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
令人困惑的是,该方法是而不是标记为默认值的。但是它被标记为合成的,所以:
2。可以通过Method.isSynthetic()
过滤掉继承的方法。
那么,回答我们的问题:
- 为什么会发生这种情况?
我认为合成方法是在minSdk小于24时生成的,因为APK可以安装在没有"直接"在其JVM中支持默认接口方法。
当直接通过Android Studio安装应用程序时,我猜minSdk值被忽略了,因为Android Studio可以检查设备的实际版本。
如果有人有更确切的信息,请分享。
- 解决这个问题的最佳方法是什么?
继承的默认方法可以通过调用isSynthetic()
来过滤掉,这将返回true
。
如果你想保留一些其他的合成方法,只过滤掉这些方法,我不知道如何做到这一点,但这应该是一种极其罕见的情况。