记一次Build.gradle引发的ClassNotFound

前段时间发过这样一篇文章 Android Studio 打包Jar,因为任务需要将项目中一个模块打包成jar包提供给第三方公司使用,实话说打包完,并且提供给N个公司使用,那感觉。。。

不过装逼过度总是要还的,这不 前两天打脸的来了。。。

剧情

剧情有点繁琐,不想看的童鞋可以跳的后面的错误原因或错误重现那。。。

那是一个挺(热)悠(成)闲(狗)的早上,刚到公司打开电脑,正准备浏览几篇技术文章,再开始一天的工作呢。突然 本公司与B公司战略合作群 里出现对方技术人员的疑问

B公司-Android:我这边调用SDK崩溃 @xxx

看见这个问题,我第一瞬间想到肯定是没按照步骤进行配置,因为之前给别家接SDK时也遇到过调用失败的问题

我:是不是配置出错了,是按照文档中的要求配置的吗?权限有给吗?

没一会

B公司-Android: 都按照文档中配置了,权限也都申请了,但还是使用不了。

刚准备质问一下是否真的配置全了

啪 。。 啪 。。 啪 。。。

对方接连贴了N张配置的截图,我仔细看看,的确都是按照文档中配置的。。。

装逼第一步 。。。 失败 。。。

我:能把日志给我看看吗。。

啪 。。 啪 。。 啪 。。

小伙子挺喜欢啪啊 。。。

我盯着那日志看了半天,没找到任何问题,连一个红色的报错都没有,这TM什么鬼。。

我:全部日志就这些吗?没有看见报错啊。。你确定出错了?

还没等我继续废话呢

啪 。。

小伙子 你真的很喜欢啪啊

哎? 不是图片啊? 再一看 测试包!!!我也是服气的。。。

算了,看在群里这么多老板的份上,我忍了。。。掏出测试机。。。安装测试包。。

运行。。

果然,程序运行到我的SDK模块时,软件崩溃了。。。

打开Studio 日志,翻了一个遍,的确和他刚才给的日志一样,并没有找到错误点,这TM就很奇怪了。。。

再运行一次,依旧是这样,不过这次我发现一点奇怪的地方,APP崩溃后并没有直接退出程序,而是重启了一遍程序,难道是这里做的怪?

打开Studio日志,盯着日志打印,运行程序,程序崩溃后果然看见一片红色的打印!!然而当APP自动重启后,日志记录中所有的报错部分全都没了!看来的确是这个重启刷新了日志,导致错误信息看不见了。

其实这个问题以我以往的经验,应该是Activity的启动模式设置成了android:launchMode="singleTask",所有的Activity都在单独的任务栈中,如果Activity使用默认启动模式,都在一个任务栈中,当某个Activity崩溃时会导致整个程序的退出,而使用 singleTask 会导致Activity崩溃,程序重启到前一个Activity,同时会重启一个新的进程。

那该怎样查看崩溃的日志信息呢?

很简单,Android Studio查看日志的时候可以选择不同的进程

例如我这里选取的进程是com.lcm.test,而当出现上面的那种情况时,一般情况下我们都会在这里看见一个与当前进程同名的一个进程,不过进程后会多一个[DEAD],例如com.lcm.test[DEAD],我们选取这个进程,就可以看见刚才崩溃的那个进程的日志信息了。

既然能找到错误了,我们就来看看是什么错

很明白直接的一个错误 Resources$NotFoundException ,资源文件缺失。

这里先回顾一下:

SDK中包含一个Activity,而Activity的Layout文件以及一些资源文件是单独提供给第三方的,第三方将jar包以及资源文件放到项目的相关目录下,SDK中通过反射获取第三方APP资源文件对应的ID,然后再加载相应的资源文件。

所以看到 Resources$NotFoundException ,我立马就怀疑是不是对方没有加入我提供的资源文件。

我:我这边看见是资源文件未找到的错误,你那边使用SDK时有拷贝提供的资源文件到项目中吗?

发完这句话我就后悔了,文档中说的很清楚,一般人不会忘记这一步吧,果然

B公司-Android: 都拷贝过来了,你看

啪 。。

果然不会犯这么低级的错误,继续研究日志,在 Warn 级别的日志中发现这样一个警告

难道是 R 文件没有找到?

这里贴上SDK中反射获取资源文件的代码

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
32
33
34
35
36
/**
*
* @param context 上下文
* @param className 资源文件的类型 layout、id、drawable
* @param name 资源文件的名字
* @return
*/
public static int getIdByName(Context context, String className, String name) {
String packageName = context.getPackageName();
Class r = null;
int id = 0;
try {
r = Class.forName(packageName + ".R");
Class[] classes = r.getClasses();
Class desireClass = null;
for (int i = 0; i < classes.length; ++i) {
if (classes[i].getName().split("\\$")[1].equals(className)) {
desireClass = classes[i];
break;
}
}
if (desireClass != null)
id = desireClass.getField(name).getInt(desireClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return id;
}

通过代码 我们知道,我们是通过 Class.forname(包名+R) 来获取APP的R文件,然后在R文件中找到我们所需要的资源文件对应的ID,具体可以看我之前的文章 Android Studio 打包Jar

关于 ClassNotFoundException 的错误,不管百度还是google常见的有几种可能的原因。

  1. jar包未引入,相应的类无法找到
  2. Manifest.xml 中注册Activity时,类名写错
  3. App混淆时,未保留相关类,导致混淆后无法加载相关类

这里能正确的调用SDK中的方法,说明jar包是正常引入的,所以排除第一种可能。

让对方再次检查了一遍Manifest 文件,确定配置注册的Activity完整类名填写没问题,排除第二种可能性。

剩第三种,询问后得知,对方的确开启了APP代码混淆,立马想到让他在代码混淆配置文件中添加保留R文件代码

1
2
3
-keep class **.R$* {  
*;
}

以防万一,还让他添加了保留我的SDK代码的逻辑,虽然我的jar包已经做过代码混淆了。

让他再次测试运行

B公司-Android:还是一样的结果

啪 。。

顺手还贴了个测试包过来。。。

安装 运行,的确错误信息依然存在,真是xxxx 。。。

突然,我想起以前遇到的一个坑 multiDex导致NoClassDefFoundError错误 ,大概就是Android 打包时遇到 65535 错误,采取 multiDex 进行分包,但是在分包后程序运行过程中会遇到 NoClassDefFoundError 的错误,也是类加载失败。我突然想会不会是这个原因呢?

我:你的项目中是不是开启了 multiDexEnabled true 配置

B公司-Android:嗯嗯 是的

啊哈!果然有进行分包处理!肯定是这里的错!

为了避免又被打脸的尴尬,我强装冷静道

我:我怀疑是这个分包导致的错,这样,你按照我说的进行配置。。

大致配置情况,在我的这篇博客中有写 multiDex导致NoClassDefFoundError错误 ,大致原理就是在进行分包的时候,手动将自己需要的类保留到主要的包中,使其在APP启动时就加载。为了避免太装逼,我没有直接把自己的博客地址给他 😄。

这回应该没错了吧,哈哈,喝口水休息下。。。看一下时间,都快到中午了。。。

但是,没过五分钟。。

B公司-Android:还是不行啊,还是一样的错。。

我擦嘞!!!真的假的!!!

赶紧让他又发了个测试包过来,安装运行,果然错误信息连变都没变。。。

不甘心的我

我 :你确定是安照我说的配置了吗?

啪 。。啪 。。 啪 。。 啪 。。

朋友!你体验过绝望吗? 我体验过!!

接下来的一天,基本上就是陪着他检查各种可能的情况,一遍的调试,一遍遍的被打脸。。

我都准备让他把源码发过来,自己运行检查,但是一般公司怎么可能轻易把代码外流啊。。

错误原因

万万没想到我最终还是解决了这个BUG!(咋突然跳到了王大锤的节奏呢。。哈哈😄)

正当我们一筹莫展的时候,我突然发现一个奇怪的地方,对方的build.gradle中配置的applicationId = aa.bb.cc 和AndroidManifest.xml 中

1
2
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="aa.bb.dd.cc">

package 配置的包名不一致。

我:你们这Build.gradle 和 manifest.xml 中配置的包名为什么不一致呢?

B公司-Android:这个项目以前是从Ecipse转过来的,中间有次改过包名,Android Studio 改包名只要修改 build.gradle中的 applicationId 就可以了。

哦?是吗?

我再回头看看错误原因 java.lang.ClassNotFoundException:aa.bb.cc.R
这里寻找的是 build.gradle 中配置的包名对应的R文件,我灵机一动

我:你能看看项目 build/intermediates/classes/debug/项目包名/R 目录下的R文件是否存在吗?

B公司-Android:存在的

我:那你看看这个目录中的项目包名是什么?

B公司-android:是 aa.bb.dd.cc

我擦,难道真的是这里的原因,项目编译时产生的R文件存在的位置是与Manifest 中配置包名也就是项目的工程目录相对应的目录中,而代码中获取的项目包名是 build.gradle 中配置的applicationId对应的包名,如果再使用这个包名去反射获取R文件当然是失败的了!!

我不是很自信的跟他说到

我: 你把这两个地方的包名改成一致的试试看。死马当活马医了。。

B公司-android:。。。。。。好吧

然后。。就没有然后了。。。。问题就这么解决了。。。

错误重现

创建工程

正常创建一个工程,在一个Activity中加载一张图片,这里我们使用反射获取资源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
private ImageView ivImg;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(MResource.getIdByName(getApplicationContext(), "layout", "activity_main"));

ivImg = (ImageView) findViewById(R.id.image);

int imgId = MResource.getIdByName(getApplicationContext(), "drawable", "iv_img");

ivImg.setImageResource(imgId);
}
}

build.gradle 以及 Manifest中配置包名都为 com.lcm.classNotFound

build 目录结构如下

正常显示结果如下

修改包名

修改 build.gradle 中的 applicationId 为 com.lcm.test

运行

出现 ClassNotFoundException 错误,且反射R文件包名对应为build.gradle中配置包名。

小结

虽然是友方出现的问题,但也实实在在的锻炼了我的解决错误的能力,我记录下整个过程,是为了给自己一个好的示范,真正解决过程中,还是走了一些弯路的,只不过这里没有记录。这里记录下的是我认为正确的过程,遇到BUG不要怕,静下心来,分享日志,分析代码,一步步排出可能出现的原因。既然出现问题,肯定有导致问题的原因,发现根源,然后解决它!!!

坚持原创技术分享,您的支持将鼓励我继续创作!