記一次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中反射獲取資原始檔的程式碼
/**
*
* @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常見的有幾種可能的原因。
- 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不要怕,靜下心來,分享日誌,分析程式碼,一步步排出可能出現的原因。既然出現問題,肯定有導致問題的原因,發現根源,然後解決它!!!