使用Parcelable时出现Class not found when unmarshalling问题
最近在使用Parcelable时遇到了Class not found when unmarshalling的问题,场景为在指定了单独process的Service中使用AlarmManager添加闹钟:
Intent intent = new Intent(context, AlertBroadcastReceiver.class); AlarmModel nextAlarm = getNextAlarm(alarms); intent.putExtra(ALARM_EXTRA, nextAlarm); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC_WAKEUP, nextAlarm.getNextAlertTime().getTime(), pendingIntent);
其中AlarmModel实现了Parcelable。然后在BroadcastReceiver中接收上面的pendingIntent,并取出AlarmModel:
AlarmModel alarmModel = intent.getParcelableExtra(ALARM_EXTRA);
这段程序在我自己的手机(Android 6.0.1)上可以正常运行,但在Android 7.0的模拟器上会指示Class not found when unmarshalling,出现ClassNotFoundException异常,如下所示:
09-18 18:13:00.021 1490-1526/system_process E/Parcel: Class not found when unmarshalling: com.nex3z.shalarm.presentation.model.AlarmModel java.lang.ClassNotFoundException: com.nex3z.shalarm.presentation.model.AlarmModel at java.lang.Class.classForName(Native Method) at java.lang.Class.forName(Class.java:400) at android.os.Parcel.readParcelableCreator(Parcel.java:2507) at android.os.Parcel.readParcelable(Parcel.java:2461) at android.os.Parcel.readValue(Parcel.java:2364) at android.os.Parcel.readArrayMapInternal(Parcel.java:2717) at android.os.BaseBundle.unparcel(BaseBundle.java:269) at android.os.Bundle.putAll(Bundle.java:226) at android.content.Intent.fillIn(Intent.java:8171) at com.android.server.am.PendingIntentRecord.sendInner(PendingIntentRecord.java:255) at com.android.server.am.PendingIntentRecord.sendWithResult(PendingIntentRecord.java:216) at com.android.server.am.ActivityManagerService.sendIntentSender(ActivityManagerService.java:7151) at android.app.PendingIntent.send(PendingIntent.java:836) at com.android.server.AlarmManagerService$DeliveryTracker.deliverLocked(AlarmManagerService.java:2984) at com.android.server.AlarmManagerService.deliverAlarmsLocked(AlarmManagerService.java:2424) at com.android.server.AlarmManagerService$AlarmThread.run(AlarmManagerService.java:2543) Caused by: java.lang.ClassNotFoundException: com.nex3z.shalarm.presentation.model.AlarmModel at java.lang.Class.classForName(Native Method) at java.lang.BootClassLoader.findClass(ClassLoader.java:1346) at java.lang.BootClassLoader.loadClass(ClassLoader.java:1406) at java.lang.ClassLoader.loadClass(ClassLoader.java:312) at java.lang.Class.classForName(Native Method) at java.lang.Class.forName(Class.java:400) at android.os.Parcel.readParcelableCreator(Parcel.java:2507) at android.os.Parcel.readParcelable(Parcel.java:2461) at android.os.Parcel.readValue(Parcel.java:2364) at android.os.Parcel.readArrayMapInternal(Parcel.java:2717) at android.os.BaseBundle.unparcel(BaseBundle.java:269) at android.os.Bundle.putAll(Bundle.java:226) at android.content.Intent.fillIn(Intent.java:8171) at com.android.server.am.PendingIntentRecord.sendInner(PendingIntentRecord.java:255) at com.android.server.am.PendingIntentRecord.sendWithResult(PendingIntentRecord.java:216) at com.android.server.am.ActivityManagerService.sendIntentSender(ActivityManagerService.java:7151) at android.app.PendingIntent.send(PendingIntent.java:836) at com.android.server.AlarmManagerService$DeliveryTracker.deliverLocked(AlarmManagerService.java:2984) at com.android.server.AlarmManagerService.deliverAlarmsLocked(AlarmManagerService.java:2424) at com.android.server.AlarmManagerService$AlarmThread.run(AlarmManagerService.java:2543) Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available 09-18 18:13:00.022 1490-1526/system_process W/Bundle: Failed to parse Bundle, but defusing quietly android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.nex3z.shalarm.presentation.model.AlarmModel at android.os.Parcel.readParcelableCreator(Parcel.java:2535) at android.os.Parcel.readParcelable(Parcel.java:2461) at android.os.Parcel.readValue(Parcel.java:2364) at android.os.Parcel.readArrayMapInternal(Parcel.java:2717) at android.os.BaseBundle.unparcel(BaseBundle.java:269) at android.os.Bundle.putAll(Bundle.java:226) at android.content.Intent.fillIn(Intent.java:8171) at com.android.server.am.PendingIntentRecord.sendInner(PendingIntentRecord.java:255) at com.android.server.am.PendingIntentRecord.sendWithResult(PendingIntentRecord.java:216) at com.android.server.am.ActivityManagerService.sendIntentSender(ActivityManagerService.java:7151) at android.app.PendingIntent.send(PendingIntent.java:836) at com.android.server.AlarmManagerService$DeliveryTracker.deliverLocked(AlarmManagerService.java:2984) at com.android.server.AlarmManagerService.deliverAlarmsLocked(AlarmManagerService.java:2424) at com.android.server.AlarmManagerService$AlarmThread.run(AlarmManagerService.java:2543)
由于其他地方也使用了类似方法来在Activity之间传递数据,在Android 7.0上并没有以上异常,猜测原因应该和远程Service有关,远程Service无法获取AlarmModel的信息。可以找到类似历史Issue。
一个应对方式是先把Parcelable放到Bundle中,再把Bundle放到Intent中,如下所示:
AlarmModel nextAlarm = getNextAlarm(alarms); Intent intent = new Intent(context, AlertBroadcastReceiver.class); Bundle alarmBundle = new Bundle(); alarmBundle.putParcelable(ALARM, nextAlarm); intent.putExtra(ALARM_BUNDLE, alarmBundle); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC_WAKEUP, nextAlarm.getNextAlertTime().getTime(), pendingIntent);
对应地,接收时先获取Bundle,再获取Parcelable:
Bundle alarmBundle = intent.getBundleExtra(ALARM_BUNDLE); AlarmModel alarmModel = alarmBundle.getParcelable(ALARM);
在原生AlarmClock中的Alarms.java中还可以看到另一种处理方法,把Parcelable转换为字节数组进行传递:
Intent intent = new Intent(ALARM_ALERT_ACTION); // XXX: This is a slight hack to avoid an exception in the remote // AlarmManagerService process. The AlarmManager adds extra data to // this Intent which causes it to inflate. Since the remote process // does not know about the Alarm class, it throws a // ClassNotFoundException. // // To avoid this, we marshall the data ourselves and then parcel a plain // byte[] array. The AlarmReceiver class knows to build the Alarm // object from the byte[] array. Parcel out = Parcel.obtain(); alarm.writeToParcel(out, 0); out.setDataPosition(0); intent.putExtra(ALARM_RAW_DATA, out.marshall()); PendingIntent sender = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
非常有用,7.0才会出现的问题