美文网首页Android开发经验谈
记一次Build.gradle引发的ClassNotFound

记一次Build.gradle引发的ClassNotFound

作者: 晨鸣code | 来源:发表于2017-08-28 10:26 被阅读42次

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

    相关文章

      网友评论

        本文标题:记一次Build.gradle引发的ClassNotFound

        本文链接:https://www.haomeiwen.com/subject/jhmwdxtx.html