1. 程式人生 > >Enum的坑 RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object

Enum的坑 RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object

昨天處理一個奇怪的crash,crash的呼叫棧是這樣色的:

Caused by: java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = azmo$a) at android.os.Parcel.readSerializable(Parcel.java:2925) at android.os.Parcel.readValue(Parcel.java:2711) at android.os.Parcel.readArrayMapInternal(Parcel.java:3029) at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:288) at android.os.BaseBundle.unparcel(BaseBundle.java:232) at android.os.BaseBundle.getInt(BaseBundle.java:1030) at android.content.Intent.getIntExtra(Intent.java:7380) at ***.onHandleWork(***.java:81)

查詢了程式碼,呼叫棧的起點是在一個JobIntentService,當它在處理Intent, getIntExtra時出現了ClassNotFoundException,而且這個類應該是被混淆了azmo$a,查詢build的混淆map.txt,果然沒有找到這個類名。

百思不得其解,順藤摸瓜,仔細的讀了下這個JobIntentService,發現唯一一種可能是使用者升級時,正好有一個Job在run或準備run,因為升級,app重啟了,新版本的app起來後,系統將重啟前的Intent再重發給了Service,此時因為新版本中類的混淆名字和舊版本中同一個類的混淆名字不一樣,當getIntentExtra去deSerialize這個類時,自然就ClassNotFoundException了。

這種問題很難重現,只能靠推理。有了猜想,就去代理裡推理驗證。這個crash產生首先要有兩個必要條件:

1. 在Intent建立的地方有自定義的class被putExtra

2. 這個被putExtra的自定義class沒有被exclude在混淆之外

程式碼裡有幾十個建立這個Intent的地方,還好大部分putExtra都是原聲型別long int啥的或者是Java內建class,String,只有一個Enum型別是被直接放到了intent裡面。必要條件一有了

直接查詢隨意兩個版本的混淆map.txt,果然兩個版本里這個Enum分別被混淆成了

****.UpdateVerifiedDeviceOperation$Action -> agll$a

****.UpdateVerifiedDeviceOperation$Action -> agml$a

這裡的混淆後的名字更證實了我的猜想,它和crash裡面找不到的類名azmo$a很接近。

驗證了猜想,開始fix,fix的基本思想是不要直接putExtra Enum型別

原來錯誤的程式碼

// putExtra的地方
intent.putExtra(UpdateVerifiedDeviceOperation.ACTION_PARAM, UpdateVerifiedDeviceOperation.Action.DELETE);

// 取引數的地方
mAction = (Action) mIntent.getSerializableExtra(ACTION_PARAM);

第一個fix,用的方法是用enum的ordinal去put,在get的地方再用Action的value陣列去找值

// putExtra的地方
intent.putExtra(UpdateVerifiedDeviceOperation.ACTION_PARAM, UpdateVerifiedDeviceOperation.Action.DELETE.ordinal());

// 取引數的地方
int actionId = mIntent.getIntExtra(ACTION_PARAM, -1);
if (actionId >= 0 && actionId < Action.values().length) {
    Action action = Action.values()[actionId];
}

聞到Bad smell了嗎? 這裡的fix依舊有一個嚴重的升級問題,如果兩個版本之間Action的定義變了,升級中新版本可能拿到錯誤的操作型別,或者根本拿不到應該有的型別,比如DELETE由第一個ENUM變成了第二個,這時有可能拿到了錯誤的新的第一個ENUM,比如Add

// 舊的
public enum Action {
    DELETE,
    DELETE_ALL
}

// 新的
public enum Action {
    SAVE,
    DELETE,
    DELETE_ALL
}

所以引入第二個fix,採用Enum的name來putExtra

// putExtra 的地方
intent.putExtra(UpdateVerifiedDeviceOperation.ACTION_PARAM, UpdateVerifiedDeviceOperation.Action.DELETE.name());

// 取引數的地方
String actionStr = mIntent.getStringExtra(ACTION_PARAM);

Action action = null;
try {
    action = (actionStr != null) ? Action.valueOf(actionStr) : null;
} catch (IllegalArgumentException e) {
    Timber.d(TAG, "Failed to convert action from string %s", actionStr);
}

這裡的第二個Fix就一定沒有升級的問題了,但這裡需要注意的是,當呼叫Enum的valueOf的時候一定要catch IllegalArgumentException,因為升級後,新版本中有可能原有的Enum常量DELETE可能已經被刪掉了。

好的,大功告成!

總結一下:

Best practice of passing enum in intent

1. When putExtra of intent, please use enum.name as the value for the extra parameter.

2. After getStringExtra from call back side, please catch the IllegalArgumentException if you want to convert the string back to enum. 

Another way in call back side is that just using the string of the enum type for business logic, no converting.

Tip

If you do want to putExtra with a customized class, make sure keep the Serializable in proguard configuration: