前段时间发过这样一篇文章 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常见的有几种可能的原因。
- jar包未引入,相应的类无法找到
- Manifest.xml 中注册Activity时,类名写错
- 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不要怕,静下心来,分享日志,分析代码,一步步排出可能出现的原因。既然出现问题,肯定有导致问题的原因,发现根源,然后解决它!!!