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

n3ipq98p  于 2022-12-21  发布在  Android
关注(0)|答案(1)|浏览(96)

方法getDeclaredMethods,当在一个类对象上调用时,应该返回一个Method对象数组,这些对象表示直接声明为该类的一部分的方法,它不应该返回任何继承的方法。
当我直接通过Android Studio安装应用时,无论活动的构建版本变体如何,此功能都能正常工作。切换到发布构建版本不足以触发问题。
编译APK或App Bundle(.aab)并以这种方式安装应用时会出现问题。(直接将APK复制到设备上,或在Google Play商店推出捆绑包并从那里安装应用)。
以下是我的测试场景,在一个全新的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

问题:
1.为什么会发生这种情况?
1.解决这个问题的最佳方法是什么?

jchrr9hc

jchrr9hc1#

在典型的橡皮鸭调试方式中,我发现了一些重要的细节,以及如何在改进测试用例的同时解决这个问题,然后将其发布到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时才出现问题。**

(Note安装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()过滤掉继承的方法。**

所以,为了回答我们的问题:
1.为什么会发生这种情况?
我认为合成方法是在minSdk小于24时生成的,因为APK * 可以 * 安装在旧的Android设备上,该设备在其JVM中没有对默认接口方法的"直接"支持。
当直接通过Android Studio安装应用程序时,我猜minSdk值会被忽略,因为Android Studio可以检查设备的实际版本是什么。
如果任何人有更确切的信息,请分享。
1.解决这个问题的最佳方法是什么?
继承的默认方法可以通过调用isSynthetic()过滤掉,isSynthetic()将返回true
如果你想保留一些其他的合成方法,只过滤掉这些方法,我不知道如何实现,但这应该是一个非常罕见的情况。

相关问题