探究安卓应用类找不到的原因(NoClassDefFoundError和ClassNotFoundException)

最近在公司协助同事解决了几个类找不到的问题,都比较典型,特此记录一下。

原因1:新旧版本SDK API兼容性

io.reactivex.exceptions.UndeliverableException: java.lang.NoClassDefFoundError: Failed resolution of: Lretrofit2/HttpException; at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:366) at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:69) at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at java.lang.Thread.run(Thread.java:923) Caused by: java.lang.NoClassDefFoundError: Failed resolution of: Lretrofit2/HttpException; at retrofit2.adapter.rxjava2.BodyObservable$BodyObserver.onNext(BodyObservable.java:54) at retrofit2.adapter.rxjava2.BodyObservable$BodyObserver.onNext(BodyObservable.java:

一般碰到这种类找不到的问题,第一时间先看看apk本身,用jadx-gui反编译出来发现没有HttpException这个类,然后用android studio查看,有这个类,但show bytecode是灰色的,证明编译出来的apk是不包含这个类的。

然后去排查源码,我们确实有依赖retrofit,但为什么编译出来没有呢。看了一下dependency依赖树,确实只有一个版本的retrofit。。。那是不是编译的时候去掉了呢?然后全局搜索了一下retrofit,发现了下面的代码:

1
2
3
4
5
android.applicationVariants.all { variant ->
variant.getRuntimeConfiguration().exclude group: 'com.squareup.retrofit2', module: 'retrofit'
variant.getRuntimeConfiguration().exclude group: module: 'retrofit'

}

为什么要去掉呢?一般都是为了解决多个版本依赖的问题,但依赖树没有看到多个版本依赖啊,是不是某个sdk源码依赖了retrofit?

果然,注释掉上面代码后,报类冲突了,发现是我司某个sdk的问题,他自己源码集成了retrofit,包名一样,但因为是比较老的版本,没有HttpException这个类,而应用又是基于新版的retrofit开发的,但编译为了解决冲突去掉了,结果就出问题了。

那要怎么解决呢?改成exclude旧版的retrofit?行不通,因为这个sdk是源码依赖了retrofit,不是通过implement这种方法。

最后,我找到了这个sdk的源码,编译了一个没有retrofit的版本。因为依赖关系是应用依赖了A模块,A模块依赖了这个sdk。引进A模块时,exclude掉这个sdk,然后在应用侧直接依赖没有retrofit的版本。

启发

解决编译类冲突时,一定要留一个心眼,注意新旧版本api变更的问题。如果代码覆盖率测试没有跑全,就把问题带到线上了。比如,上面那个问题,只有在报错的时候才有可能触发,正常测试大概率是测不出来的,比较隐蔽。

原因2:系统的某个jar引用了不同版本的SDK

java.lang.NoSuchMethodError: No static method parseString(Ljava/lang/String;)Lcom/google/gson/JsonElement; in class Lcom/google/gson/JsonParser; or its super classes (declaration of 'com.google.gson.JsonParser' appears in /system/framework/xxxxxx.jar)

这也是一个编译通过,运行时报错的例子。重点是最后一句:/system/framework/xxxxxx.jar,这是一个自定义的系统库,放到了BootClasspath里面。反编译后发现里面依赖了一个旧版本的gson,没有parseString这个方法,由于双亲委派机制,应用优先使用了这个旧版本的gson,结果就出问题了。

启发

系统库开发的时候要慎重使用第三方库;如果系统库只是服务于一些特定的应用,不要加入到BootClasspath,应该让应用使用:use-library:动态连接库的方法。

原因3:升级persist应用

AndroidRuntime: java.lang.RuntimeException: Unable to get provider com.xxxx.sdk.xxxProvider: java.lang.ClassNotFoundException: Didn't find class "com.xxxx.sdk.xxxProvider" on path: DexPathList[[zip file "/system/vendor/app/xx.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

一般来说,安卓系统是禁止通过安装的方法来升级persist应用的,但因为历史原因,我司修改了这个机制,导致可以升级这类应用,结果就引发了这个问题:DexPathList没有更新成新安装的路径:data下面,还是停留在system分区。

一般升级普通应用时,系统会清除该应用的进程,同时清理之前在ActivityManagerService记录的该应用的四大组件,在下次应用重新启动时,系统会重新创建进程并加载各种组件运行。

但是,当升级的应用有persistent属性时,系统不会清除该应用。原因如下:

killPackageProcessesLocked (reference) in projects: frameworks - OpenGrok search results

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
final boolean killPackageProcessesLocked(String packageName, int appId,
int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
boolean doit, boolean evenPersistent, boolean setRemoved, int reasonCode,
int subReason, String reason) {
ArrayList<ProcessRecord> procs = new ArrayList<>();

// Remove all processes this package may have touched: all with the
// same UID (except for the system or root user), and all whose name
// matches the package name.
final int NP = mProcessNames.getMap().size();
for (int ip = 0; ip < NP; ip++) {
SparseArray<ProcessRecord> apps = mProcessNames.getMap().valueAt(ip);
final int NA = apps.size();
for (int ia = 0; ia < NA; ia++) {
ProcessRecord app = apps.valueAt(ia);
if (app.isPersistent() && !evenPersistent) {
// we don't kill persistent processes
continue;
}
if (app.removed) {
if (doit) {
procs.add(app);
}
continue;
}
------------------------------------------
}

系统也没有更新应用的类加载路径。

方案一:

安装完新版本后重启系统。

方案二:

在应用内接收新版应用安装完成的广播,当APK发生更新或安装时, 匹配自己的包名,然后调用context的startInstrumentation 方法,该方法会强制系统清除应用进程并清理AMS对各种组件的状态记忆并重新启动该应用。

或者,通过thread的全局异常捕获(类找不到,方法找不到),调用:

boolean b = context.startInstrumentation(new ComponentName(getApplicationContext(), InstrumentationRoboot.class), null, null);

1
2
3
4
5
6
public class InstrumentationRoboot extends Instrumentation {
@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
}
}

AndroidManifest.xml

1
2
3
4
5
6
7
8
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="自己的包名">

<instrumentation
android:name="com.test.InstrumentationRoboot"
android:targetPackage="自己的包名"/>

</manifest>

其他场景

插件化加载路径不当,宿主和插件的类加载问题;混淆导致反射失效;在安卓5.0以下没有启用MutilDex等等。

参考

Android应用具有persistent属性时升级清理AMS缓存数据_android:persistent=“true” 自升级-CSDN博客

一个一年没解决的ClassNotFoundException|类加载机制探索 - 掘金

怎么解决java.lang.NoClassDefFoundError错误-CSDN博客


探究安卓应用类找不到的原因(NoClassDefFoundError和ClassNotFoundException)
https://iwesley.top/article/3122544f/
作者
Wesley
发布于
2024年5月26日
许可协议