程序間傳遞Parcelable物件出現ClassNotFoundException異常的解決方法:
在開發中可能有時候會遇到用Bundle傳遞一個Parcelable物件時出現ClassNotFoundException異常,而且這個異常有時候會出現有時候又不會出現,比如你在同一個程序的Activity間傳遞資料時就不會出現,但是你通過Messenger攜帶bundle進行程序通訊時就會出現,具體是什麼原因?
我先總體說下出現這個問題的原因和解決方案,然後再從原始碼分析為什麼這樣做.
首先出現這個異常的原因是因為ClassLoader不對造成的,我們應用中存在兩種類載入器它們分別是BootClassLoader和PathClassLoader.
BootClassLoader用來載入系統類,PathClassLoader用來載入我們在應用中自己寫的類.所以當類載入器為BootClassLoader時我們要載入自己寫的類就會出現ClassNotFound異常.
在程序間通訊中Messenger攜帶bundle傳遞Parcelable物件,在到達另一個程序後通過bundel取出parcelable物件時出現ClassNotFound異常是因為它通過BootClassLoader來載入我們的Parcelable物件
解決方案:
在bundle讀取資料前加這行程式碼:
bundle.setClassLoader(getClass().getClassLoader());
這裡有兩點需要注意:
1.getClass().getClassLoader()不一定得到的是PathClassLoader,有些類的ClassLoader預設是BootClassLoader,這個後面會講.所以在用這行程式碼之前還是要先確定是否拿到的是PathClassLoader.
2.如果bundle同時傳了parcelable物件和其他基本型別的資料,比如int或者String.那麼必須在getxxx之前就呼叫bundle.setClassLoader(getClass().getClassLoader());,否則同樣會出錯,原因見後面分析.
現在我們來具體分析:
我們通過原始碼看看Bundle是怎麼獲取資料的:
當bundle呼叫getXXX方法時會先呼叫一個unparcel()方法,該方法的原始碼如下:
主要關注 圖中用紅框圈起來的程式碼:
首先來看int N = mParcelledData.readInt();
這行程式碼會讀出所有存在bundle裡的資料總量.比如你存資料的程式碼是這樣的:
bundle.putInt(“age”,22);
bundle.putString(“name”,”zhangsan”);
bundle.putParcelable(“parcelableData”,book);(book是一個parcelable物件)
那麼N的值就是3,因為你在bundle存了三組資料.
然後執行到mParcelledData.readArrayMapInternal(mMap, N, mClassLoader);
mParcelledData的資料型別是Parcel,呼叫readArrayMapInternal會把mMap,N,mClassLoader作為引數傳進去.現在看一下這個方法是幹嗎的:
還是關注紅框部分的程式碼,你會發現key和value其實就是我們存放在bundle裡面的對應資料的key和value值.這裡就會從native讀取我們的資料然後封裝到outVal裡面,也就是封裝到我們傳進來的mMap.這時候N=3,就會迴圈取出我們存在bundle的組資料.key的值就是我們的age,name,parcelableData.value就是每個key對應的值.
在讀取value值時,會呼叫readValue(loader)方法,它把我們的類載入器傳進去了.
我們現在再來看看該方法是 做什麼的:
你會發現首先會判斷讀取的資料是什麼型別,如果讀取基本資料型別基本不必用到ClassLoader,如果是取集合或者是物件的資料就需要ClassLoader.
我們找到readParcelable(loader)方法,再看看裡面它做了什麼.
還是看紅框的部分,系統會先呼叫readParcelableCreator方法,我們繼續看:
看紅框部分的程式碼,它會先判斷傳進來的ClassLoader是否為null,如果是的話就會取出本類(Parcel)的ClassLoader.問題就出在這裡了.Parcel類的ClassLoader是BootClassLoader,如果通過它來載入我們自己寫的類就出現了ClassNotFoundException異常.
有幾點需要注意:
1.如果自己建立bundle物件,則bundle物件的ClassLoader預設為BootClassLoader.這個可以自己去看原始碼中bundle的建構函式.但是Activity跳轉時Intent也是通過Bundle傳遞資料啊,那為什麼在Activity跳轉時傳遞Parcelable物件就不會報錯呢?那是因為Intent會對傳進去的bundle做一些處理,你呼叫bundle.getxxx方法取物件時,這時候bundle的ClassLoader會被賦予一個PathClassLoader,所以就不會出錯.具體也是去看看原始碼就知道了.
2.通過Messenger在程序間傳遞資料時,如果通過bundle來攜帶資料,則從一個程序到另一個程序,bundle的ClassLoader會變成null,這個我也不知道為什麼,先記住吧.
總結:
1.通過Messenger在程序間傳遞資料,如果通過bundle來攜帶資料則從另一個程序取出bundle時它的ClassLoader是null.bundle在取Parcelable資料時如果發現bundle的ClassLoader是null就會從Parcel類取出ClassLoader,而Parcel類的ClassLoader是BootClassLoader,要用它來載入我們自己的類就會出現異常.所以我們就要通過bundle.setClassLoader(getClass().getClassLoader());來自己設定bundle的類載入器.如果bundle的類載入器不是null的話就會通過bundle的類載入器來載入類.
而在程序間傳遞基本資料型別時不會出錯,因為bundle讀取基本資料型別不需要通過ClassLoader.
2.還有如果bundle裡面同時傳了基本資料型別和parcelable資料型別為什麼要在getxxx之前就呼叫bundle.setClassLoader(getClass().getClassLoader());
這是因為bundle每個getxxx方法都會先呼叫unparcel方法,也就是我們第一次get資料前就得指定ClassLoader不然就會報錯.