1. 程式人生 > >記一次Build.gradle引發的ClassNotFound

記一次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常見的有幾種可能的原因。

  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不要怕,靜下心來,分享日誌,分析程式碼,一步步排出可能出現的原因。既然出現問題,肯定有導致問題的原因,發現根源,然後解決它!!!