记一次Build.gradle引发的ClassNotFound

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

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

剧情

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

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

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

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

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

没一会

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

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

啪 。。 啪 。。 啪 。。。

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

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

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

啪 。。 啪 。。 啪 。。

小伙子挺喜欢啪啊 。。。

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

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

还没等我继续废话呢

啪 。。

小伙子 你真的很喜欢啪啊

哎? 不是图片啊? 再一看 <font size=5>测试包!!!</font>我也是服气的。。。

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

运行。。

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

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

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

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

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

<b>那该怎样查看崩溃的日志信息呢?</b>

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

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

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

很明白直接的一个错误 <font color=red> Resources$NotFoundException </font> ,资源文件缺失。

这里先回顾一下:

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

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

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

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

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

啪 。。

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

难道是 R 文件没有找到?

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

    /**
     *
     * @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

关于 <font color=red> ClassNotFoundException </font>的错误,不管百度还是google常见的有几种可能的原因。

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

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

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

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

-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 中

<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中加载一张图片,这里我们使用反射获取资源文件

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不要怕,静下心来,分享日志,分析代码,一步步排出可能出现的原因。既然出现问题,肯定有导致问题的原因,发现根源,然后解决它!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容