admin管理员组

文章数量:1576376

当我们在Android系统4.4上面,项目使用到okhttp3或者是retrofit(里面有使用到okhttp3)时,有时候会出现NoSuchMethodError、Expected Android API level 21+ but was 19这个错误,这是因为okhttp3的高版本不支持Android 4.4导致的,版本升级到3.13起,就要求android最低版本必须为android5.0(21)。


那我们该怎么解决这种情况呢?这里有两种方法:

①是使用 okhttp3 的 3.12.x 版本即可,retrofit2 引入兼容 4.4则使用 2.6.x 及以下版本;

当我们的项目中,使用到别人家的sdk时,如果他们的sdk里面的okhttp3版本太高的话(超过3.12.x),这时候我们让他们去改okhttp3的版本,是不太可能的,那这样子又该怎么解决呢?来看第二种方法。

②在讲解决方法前,我们先来了解下“Expected Android API level 21+ but was 19”这句是怎么被触发的。先来看到Platform.java(我这个是okhttp3   3.14.9版本

public class Platform {
  private static final Platform PLATFORM = findPlatform();
 
   .......省略一部分代码..........
   
  /** Attempt to match the host runtime to a capable Platform implementation. */
  private static Platform findPlatform() {
    if (isAndroid()) {
      return findAndroidPlatform();
    } else {
      return findJvmPlatform();
    }
  }

  public static boolean isAndroid() {
    return "Dalvik".equals(System.getProperty("java.vm.name"));
  }  

  .......省略一部分代码..........

}

首先重点来看第一行,有一个静态常量 PLATFORM = findPlatform(); 

接着看findPlatform()这个方法,方法里有一个if判断-------isAndroid();

继续看isAndroid()方法,里面很简单,拿到 系统属性"java.vm.name",看它是不是叫作"Dalvik",其实就是看我们的虚拟机有没有使用Dalvik。

这里为什么要做这个判断呢?这是因为在Android 4.4,ART和Dalvik是共存的,而在Android5.0开始,ART全面取代Dalvik,所以这里以这个来判断是否是Android4.4。

看完这个判断,我们返回findPlatform()方法,那经过isAndroid()这个判断后,因为我们现在是Android4.4,所以isAndroid()返回到的是true,那就是走到findAndroidPlatform()这个方法,具体看下面的源码:

private static Platform findAndroidPlatform() {
    Platform android10 = Android10Platform.buildIfSupported();

    if (android10 != null) {
      return android10;
    }

    Platform android = AndroidPlatform.buildIfSupported();

    if (android == null) {
      throw new NullPointerException("No platform found on Android");
    }

    return android;
  }

重点看第一行的Android10Platform.buildIfSupported(),看下面的源码:

  public static @Nullable Platform buildIfSupported() {
    if (!Platform.isAndroid()) {
      return null;
    }

    try {
      if (getSdkInt() >= 29) {
        Class<?> sslParametersClass =
            Class.forName("com.android.conscrypt.SSLParametersImpl");

        return new Android10Platform(sslParametersClass);
      }
    } catch (ReflectiveOperationException ignored) {
    }

    return null; // Not an Android 10+ runtime.
  }
}

还是走到isAndroid这个判断,前面我们知道,这个判断返回的是true,那这里取反后,就不会进入if语句,继续往下,又有一个if判断,这是对我们的 操作系统的版本号的判断,因为我们是Android4.4,这里我们的系统版本号是19,小于29,那就直接return null;那我们再回到Android10Platform.buildIfSupported(),我把代码从上面复制下来了,看下面源码就好

private static Platform findAndroidPlatform() {
    Platform android10 = Android10Platform.buildIfSupported();

    if (android10 != null) {
      return android10;
    }

    Platform android = AndroidPlatform.buildIfSupported();

    if (android == null) {
      throw new NullPointerException("No platform found on Android");
    }

    return android;
  }

因为Android10Platform.buildIfSupported()返回的是null,那继续往下走,走到AndroidPlatform.buildIfSupported(),来看下源码:

public static @Nullable Platform buildIfSupported() {
    if (!Platform.isAndroid()) {
      return null;
    }

    // Attempt to find Android 5+ APIs.
    Class<?> sslParametersClass;
    Class<?> sslSocketClass;

    try {
      sslParametersClass = Class.forName("com.android.conscrypt.SSLParametersImpl");
      sslSocketClass = Class.forName("com.android.conscrypt.OpenSSLSocketImpl");
    } catch (ClassNotFoundException ignored) {
      return null; // Not an Android runtime.
    }
    if (Build.VERSION.SDK_INT >= 21) {
      try {
        Method setUseSessionTickets = sslSocketClass.getDeclaredMethod(
            "setUseSessionTickets", boolean.class);
        Method setHostname = sslSocketClass.getMethod("setHostname", String.class);
        Method getAlpnSelectedProtocol = sslSocketClass.getMethod("getAlpnSelectedProtocol");
        Method setAlpnProtocols = sslSocketClass.getMethod("setAlpnProtocols", byte[].class);
        return new AndroidPlatform(sslParametersClass, sslSocketClass, setUseSessionTickets,
            setHostname, getAlpnSelectedProtocol, setAlpnProtocols);
      } catch (NoSuchMethodException ignored) {
      }
    }
    throw new IllegalStateException(
        "Expected Android API level 21+ but was " + Build.VERSION.SDK_INT);
  }

最终,我们Android4.4,会直接走到最后的throw new IllegalStateException,这也是我们的“Expected Android API level 21+ but was 19”就是在这里被抛出来的。

现在知道崩溃的原因后,想想怎么解决:

前面说过okhttp3.12.x版本是支持Android4.4的,那我们来看下okhttp3.12.x版本中,静态常量PLATFORM是怎么赋值的:

public class Platform {
    private static final Platform PLATFORM = findPlatform();
    ......省略代码.....
    
    private static Platform findPlatform() {
        Platform android = AndroidPlatform.buildIfSupported();
        ......省略代码.....
    
    }    
    ......省略代码.....   
}

okhttp3.12.x版本中,是AndroidPlatform.buildIfSupported赋值给PLATFORM,想到这里,我们就知道有个大概解决方法了:我们只要把okhttp3高版本里面的静态常量PLATFORM替换成okhttp3.12.x版本里的AndroidPlatform.buildIfSupported就可以了,下面就是具体的替换过程:

  1. 首先,为了防止在初始化Platform时,抛出“Expected Android API level 21+ but was 19”异常,我们要对isAndroid()方法动点手脚,因为这个方法里面是对 系统属性"java.vm.name"进行判断的,那我们就先暂时把这个系统属性修改为“jvm”,这样它就不会抛异常了;
  2. 接着,通过反射,把PLATFORM赋值成okhttp3.12.x版本中的AndroidPlatform.buildIfSupported;
  3. 最后,再把系统属性"java.vm.name"修改回来。具体代码如下:

这里首先要准备好两个文件:AndroidPlatform.javaOptionalMethod.java这里这两个文件都是从okhttp3.12.x版本中直接copy过来的,在我们反射给PLATFORM赋值时,需要用到。

public class LowPlatformUtils {

    //okhttp3 兼容android系统4.4
    public static void okhttp3InLowPlatform(){
        try{
            //获取vm的名字
            String currentVmName = System.getProperty("java.vm.name", "Dalvik");

            Properties properties = System.getProperties();
            //暂时把vm名字修改为 “jvm”,防止抛异常
            properties.setProperty("java.vm.name","jvm");

            //反射,把PLATFORM 常量修改为  AndroidPlatform9.buildIfSupported()
            Platform platform = new Platform();

            Field platformField = Platform.class.getDeclaredField("PLATFORM");
            platformField.setAccessible(true);

            //重新赋值
            platformField.set(platform,AndroidPlatform9.buildIfSupported());

            //重新把vm名字赋值回去
            properties.setProperty("java.vm.name",currentVmName);
        }catch (Throwable e){
            KkLog.d("LowPlatformUtils",KkLog.getStackTrace(e));
        }
    }
}

最后注意,最好在application里就进行初始化,预防有人抢先初始化OkHttpClient对象发生崩溃。

本文标签: 解决方法错误NoSuchMethodErrorexpectedretrofit