升级AndroidX遇到的问题

记录在升级AndroidX时遇到的一个问题,仅供参考。

0x01 问题描述

在项目中升级AndroidX时遇到一个运行时异常,查看日志可了解到是因为找不到方法addOnTabSelectedListener,异常堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
E  Caused by: java.lang.reflect.InvocationTargetException
E at java.lang.reflect.Constructor.newInstance0(Native Method)
E at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
E at android.view.LayoutInflater.createView(LayoutInflater.java:658)
E at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:801)
E at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:741)
E at android.view.LayoutInflater.rInflate(LayoutInflater.java:874)
E at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:835)
E at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
E at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
E at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
E at com.android.internal.policy.PhoneWindow.setContentView(PhoneWindow.java:498)
E at com.android.internal.policy.HwPhoneWindow.setContentView(HwPhoneWindow.java:342)
E at android.app.Activity.setContentView(Activity.java:2941)
E at xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
E at android.app.Activity.performCreate(Activity.java:7458)
E at android.app.Activity.performCreate(Activity.java:7448)
E at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1286)
E at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3409)
E at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3614)
E at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:86)
E at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
E at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
E at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2199)
E at android.os.Handler.dispatchMessage(Handler.java:112)
E at android.os.Looper.loop(Looper.java:216)
E at android.app.ActivityThread.main(ActivityThread.java:7625)
E at java.lang.reflect.Method.invoke(Native Method)
E at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
E at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)
E Caused by: java.lang.NoSuchMethodError: No virtual method addOnTabSelectedListener(Lcom/google/android/material/tabs/TabLayout$OnTabSelectedListener;)V in class Lcom/google/android/material/tabs/TabLayout; or its super classes (declaration of 'com.google.android.material.tabs.TabLayout' appears in /data/app/xxxxxx-WbxmUkjqMEjNknQppPqeWw==/base.apk!classes2.dex)

0x02 问题分析

因为从support27到support28版本,TabLayout的API发生了变更,addOnTabSelectedListener方法入参发生了变化,可通过查看下图中的函数字节码可以看到,AndroidX中的该函数的入参为BaseOnTabSelectedListener,而support27中的该函数的入参为onTabSelectedListener。函数字节码如下:


发生改问题的原因是AndroidX是基于Support Library 28的重构。而在当前项目中依赖了27.0.2的TabLayout support包,并在依赖support27的基础上使用进行了AndroidX包名替换转换。所以在动手之前还是建议先把官方看清楚,在官方文档里说的很清楚了,要先把当前的 Support Library 依赖升级至版本28,然后再使用Android Studio的工具转换AndroidX。

0x03 函数签名

3.1 先写个demo

demo说明:

  • 接口A声明接口方法methodA
  • 接口B继承A
  • methodTest声明的函数参数为接口A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TestMethod {
void invokeTest() {
methodTest(new AbsClassB() {

@Override
public void methodA() {

}
});
}

void methodTest(AbsClassA a) {
System.out.print("-------");
}
}

interface AbsClassB extends AbsClassA {
}

interface AbsClassA {
void methodA();
}

反编译以上Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Compiled from "TestMethod.java"
class TestMethod {
TestMethod();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

void invokeTest();
Code:
0: aload_0
1: new #2 // class TestMethod$1
4: dup
5: aload_0
6: invokespecial #3 // Method TestMethod$1."<init>":(LTestMethod;)V
9: invokevirtual #4 // Method methodTest:(LAbsClassA;)V
12: return

void methodTest(AbsClassA);
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String -------
5: invokevirtual #7 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: return
}

由以上可知,所谓的方法签名至于函数定义时声明的参数类型有关,和调用时传递的参数类型无关。

3.2 什么是函数签名?

方法签名为了唯一标识一个方法。如果你查询一些资料和文档可能会得到下面的说法:
说法一:
同一类中不能存在两个名字及描述符完全相同的方法。
在Java中方法签名包括:方法名、形参参数列表、泛型方法类型参数列表。Java的方法签名并不包括返回值和访问修饰符。当类中存在签名相同的两个方法时编译会报错,当两个方法的其中一个签名是另一个方法的子签名时也会报错。自签名的定义是一个签名在类型擦除后与另一个签名相同,则称其为第二个签名的的子签名。

说法二:
在同一class文件中,两个方法可以拥有同样的特征签名,前提是返回值不能相同。

这看起来是自相矛盾的啊,到底一个函数签名包不包含方法的返回值类型呢?

如下面所示的代码,使用Java编译器编译一定是编译不通过的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestMethodSameName {  
public static void foo(int i) {
System.out.println("TestMethodSameName.foo:(I)V");
}

public static int foo(int i) {
System.out.println("TestMethodSameName.foo:(I)I");
return i;
}

public static void main(String[] args) {
foo(123); // foo:(I)V
foo(456); // foo:(I)I
}
}

但是如果你使用使用javac和javap命令查看方法签名时你会发现

1
2
3
4
5
6
7
8
9
10
11
12
13
javac TestMethodSameName.java
javap -s -p TestMethodSameName.class
Compiled from "TestMethodSameName.java"
public class TestMethodSameName {
public TestMethodSameName();
descriptor: ()V

public static int foo(int);
descriptor: (I)I

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
}

可以看到所谓方法描述是包含函数参数类型和返回值类型的(descriptor: ([Ljava/lang/String;)V).所以是不是以上说法一和说法二都对呢?
其实说法一针对的是Java编译器,说法二针对的是JVM的。在虚拟机的标准里是允许一个类中方法名和形参一样但是返回值不一样,因为这两个方法对应的方法描述不一样,针对字节码层面这种情况是存在的,JVM是也正常运行的。
那么为什么JVM支持的东西,JAVA偏偏不支持呢?
因为在方法调用的时候并不能保证指定了方法的返回信息,编译器并不知道你实际调用了哪个方法。还是以上面的代码为例,当忽略返回值的调用时就搞不清到底在调用哪个方法了。

1
foo(1)

3.3 重载和重写

重载:同一个类中方法相同方法参数不同的方法。不能通过返回类型是否相同来判断重载。
重写:方法签名必定相同,返回值必定相同, 访问修饰符 子 > 父, 异常类 子 < 父

3.4 父类的private方法子类可以重写吗?

JVM调用方法有五条指令,分别是

  1. invokestatic,用来调用static方法(类方法)
  2. invokespecial,用来调用需要特殊处理的实例方法,私有方法,父类方法(super.),初始化方法。在对象的创建过程中,new之后很多都会执行方法,就是依赖字节码中是否包含invokespecial指令。静态绑定
  3. invokevirtual,用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)最常见的。动态绑定 多态例子
  4. invokeinterface,调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适当的方法进行调用。
  5. invokedynamic。方法动态解析出调用点限定符所引用的方法

程序绑定: 指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定。

静态绑定: 在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。例如:C。针对java简单的可以理解为程序编译期的绑定。invokespecial和invokestatic是采用静态绑定。

动态绑定: 在运行时根据具体对象的类型进行绑定。invokevirtual和invokeinterface是采用动态绑定。

可以看到,在调用private、super、方法时使用的invokespecial指令,而在实例对象的其他方法时使用的是invokevirtual指令。正是由于这两种绑定的不同,在子类覆盖超类的方法、并向上转型引用后,才产生了多态以及其他特殊的调用结果。运行时,invokespecial选择方法基于引用声明的类型,而不是对象实际的类型。但invokevirtual则选择当前引用的对象的类型。

回到问题,父类的private方法子类可以重写吗?

答案很明显,不能,从invoke指令的角度看,父类的private方法的程序绑定在编译期就已经确定了,跟Java的多态特性是无关的,所以不能被重写。

0x04 总结

如果只是针对AndroidX的升级其实很简单,只要关注到一点,AndroidX的升级和转换要保证所有依赖的support包都升级到28.0.0以上。
如果从JVM规范和指令角度看,一个很小的点还是有很多值得深挖的地方的。

参考文档

https://mp.weixin.qq.com/s/fmnoKH-R9PCmg-3ATRMIbQ
http://wxweven.win/2017/09/15/JVM-invokespecial%E5%92%8Cinvokevirtual/
https://hllvm-group.iteye.com/group/topic/27033
https://www.iteye.com/blog/rednaxelafx-479301
https://www.cnblogs.com/onlywujun/p/3523991.html