首先声明,这个问题最重要部分的解决,主要是另一位同事YC的功劳,我只是希望将来能有所参考而将这个问题的解决过程记录下来。
自测没有问题的App,在测试那里出现了必现的 AbstractMethodError
Crash。这样的问题既然编译能过,很可能是混淆问题。然而检查mapping文件后,发现名称虽然被混淆,但并没有被删除,应该是没有问题的。
常规的定位方式
崩溃的 CallStack 如下:
FATAL EXCEPTION: main
Process: com.mobvoi.ticwear.dialer, PID: 2541
java.lang.AbstractMethodError: abstract method "android.view.View dialer.TS.z(java.util.List)"
at dialer.TS.a(HeaderScrollingViewBehavior.java:57)
at ticwear.design.widget.AppBarLayout$ScrollingViewBehavior.a(AppBarLayout.java:1416)
at ticwear.design.widget.CoordinatorLayout.onMeasure(CoordinatorLayout.java:778)
at android.view.View.measure(View.java:17668)
查看 HeaderScrollingViewBehavior.java:57
得知是一个 abstract method 的问题。
HeaderScrollingViewBehavior 的实现如下:
abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
...
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child, ...) {
...
final View header = findFirstDependency(dependencies);
...
}
abstract View findFirstDependency(List<View> views); // line 57
}
其实现子类实现如下:
public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {
@Override
View findFirstDependency(List<View> views) {
...
}
}
这几个类的继承关系如下:
CoordinatorLayout$Behavior
^
...
^
abstract HeaderScrollingViewBehavior
^
CoordinatorLayout$ScrollingViewBehavior
因为 AbstractMethodError
是由于子类未继承父类的abstract方法,导致调用此 abstract 方法时,找不到实现而崩溃。实际情况中,由于带有 abstract 方法的类必须是 abstract 的,而这样的类必须被继承和实现才可能被实例化。因此,绝不可能出现这样的崩溃。这个时候,我能想到的只有下几种可能:
- 使用了 Android SDK 的某个方法,而在不同的系统版本中没有实现。
- 使用的某个库使用的
provided
方式编译。这样,只是在编译时引用了库,而在实际打包时,并未加入应用中。 - 混淆出问题,错误的删除了子类实现。
出错的并不是 Android SDK 的方法,我也并没有使用类似 provided
这样的方式编译。那么,只可能是混淆的锅了。
于是检查了 ProGuard,有这样的规则:
-keep public class * extends ticwear.design.widget.CoordinatorLayout$Behavior {
public <init>(android.content.Context, android.util.AttributeSet);
public <init>();
}
查看 ProGuard 的 mapping 文件,发现 ScrollingViewBehavior
没混淆,而 HeaderScrollingViewBehavior
被混淆了。
这也是正常的,因为根据 ProGuard 规则,确实应该这样。而且 findFirstDependency
混淆后的签名一致,仍然符合正确的继承关系,不应该出现 AbstractMethodError
的问题。
而且,从 crash 的 CallStack 来看,调用栈确实是从子类过来的,因此子类的代码是一定存在,没有被混淆删除的。
这就非常奇怪了。实在不知道还有什么其他可能。Google 了一下类似的问题。发现这篇文章里提到只有在开启 ART 的机器中才会出错,怀疑是不是碰到什么 ART 的 bug 了,其解决办法就是 keep 住相关方法,不让其混淆。然而,秉承着程序出错不能怪编译器的传统,而且也检查过混淆是没有问题的,所以我们还是决定继续寻找 root cause。
ART 和包访问域
检查代码,检查混淆规则,检查问题产生所需的环境,一切似乎都是正常的。直到YC发现 Crash 的 App 有这样的 warning log (是的,程序员不能只关心 ERROR,也还是需要看看 WARNING 的):
"Before Android 4.1, method ... would have incorrectly overridden the package-private method in ..."
错误的覆盖包访问域的方法?这个问题是与包访问权限相关的?
查看源码,发现确实是函数的访问权限出错了:
if (klass->CanAccessMember(super_method->GetDeclaringClass(),
super_method->GetAccessFlags())) {
if (super_method->IsFinal()) {
ThrowLinkageError(klass.Get(), "Method %s overrides final method in class %s",
PrettyMethod(virtual_method).c_str(),
super_method->GetDeclaringClassDescriptor());
return false;
}
vtable->SetWithoutChecks<false>(j, virtual_method);
virtual_method->SetMethodIndex(j);
} else {
LOG(WARNING) << "Before Android 4.1, method " << PrettyMethod(virtual_method)
<< " would have incorrectly overridden the package-private method in "
<< PrettyDescriptor(super_method->GetDeclaringClassDescriptor());
}
于是再次查看 mapping 文件,有这么两个转换:
ticwear.design.widget.HeaderScrollingViewBehavior -> dialer.TS
ticwear.design.widget.AppBarLayout$ScrollingViewBehavior -> ticwear.design.widget.AppBarLayout$ScrollingViewBehavior
也就是说, ScrollingViewBehavior
由于被 keep,保持了原有的包名,而 HeaderScrollingViewBehavior
的包名被混淆了。
这个包名的混淆,是YC添加的一个 ProGuard 规则的作用,目的是更好的防止反编译,和减小包体积:
-repackageclasses 'dialer'
-allowaccessmodification
父类的包名被修改,而继承的又是包访问域的方法。导致这个 crash。
需要说明的是,这个问题只有在打开ART的机器上才会导致crash,应该是ART做了更严格的检查。老的VM是不会有crash的。
这么看来,问题的原因就是混淆器错误的混淆了本该正常的代码访问权限。这应该归咎于混淆器的bug。
问题的解决
但还有另一个情况。我们的构建系统有两套。一套是应用层的构建,使用 Gradle;另一套是系统的构建,使用 Android Make。实测发现只有 Make 构建出来的 APK 会 crash。事情变得更蹊跷了。
YC 对比了 make 使用的系统默认 ProGuard 规则,和 gradle 使用的 Android SDK 的 ProGuard 规则,发现只有些许细微差别,而且使用相同规则构建出来的两个 APK,也还是同样只在 Make 上会 crash。
于是尝试了反编译两者构建出来的 APK。终于发现了一个区别:
- Gradle 生成的 APK,其父类
HeaderScrollingViewBehavior
的包名虽然被修改,但访问权限也随之修改为 public,使得不同包名的子类仍然可以正确的继承。 - Make 生成的 APK,访问权限没有修改,也就是说,
-allowaccessmodification
貌似没有生效。
到这里,大致就可以确定是 Android Make 所使用的 ProGuard 由于太老,并未处理包访问域的问题,也许升级就能解决了。
不过这个改动比较大,目前来看,还是禁用 -repackageclasses
规则比较保险。
如果大家也遇到类似的问题,不妨尝试更新你的编译器,最新版本的编译器已经没有对这个问题做了处理了。
Comments