PendingIntent getBroadcast丢失Parcelable数据

13

这里是问题。我的程序在Android 6.0上运行得很完美。升级设备到Android 7.0后,PendingIntent无法将可包含数据传递到广播接收器。以下是代码。

触发警报

public static void setAlarm(@NonNull Context context, @NonNull Todo todo) {
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(context.ALARM_SERVICE);
    Intent intent = new Intent(context, AlarmReceiver.class);
    intent.putExtra("KEY_TODO", todo);
    PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    alarmManager.set(AlarmManager.RTC_WAKEUP, todo.remindDate.getTime(), alarmIntent);
}

Todo是一个可序列化的类,而todo是我在通知中需要的实例。

在BroadcastReceiver中,我无法获取可序列化数据。

public void onReceive(Context context, Intent intent) {

    Todo todo = intent.getParcelableExtra("KEY_TODO");

}

以下是我调试时意图的结果

在此输入图片描述

我不知道为什么这个意图只包含一个我从未放置的整数。可 Parcelable 的待办事项在哪里。 这段代码在 Android 6.0 中没有问题,但无法在 7.0 上运行。


你尝试过在将Todo对象添加到“extras”之前将其包装在Bundle中吗?当将自定义的Parcelable对象传递给AlarmManager时,这通常是有效的(但在Android 7中可能已经失效)。我会对你的发现感兴趣。 - David Wasser
添加额外内容:Bundle bundle = new Bundle; bundle.putParcelable("todo", todo); intent.putExtra("KEY_TODO", bundle);。提取额外内容:Bundle bundle = intent.getBundleExtra("KEY_TODO"); if (bundle != null) { Todo todo = bundle.getParcelableExtra("todo"); } - David Wasser
2个回答

22
引用自我的博客:
自己定义的`Parcelable`类,即应用程序独有的类而非Android框架提供的类,在作为`Intent` extras使用时会不定期地出现问题。简单来说,如果一个核心OS进程需要修改`Intent` extras,则该进程会尝试在设置extras `Bundle`之前重建你的`Parcelable`对象。由于这个进程没有你的类,所以会出现运行时异常。
其中可能会出现这种情况的一个领域是`AlarmManager`。在Android N上,曾经可以使用自定义`Parcelable`对象的代码在此领域可能无法使用,详见此处
目前我知道的最有效的解决方法是手动将你的`Parcelable`转换为`byte[]`并将其放入`Intent` extra中,根据需要手动将其转换回`Parcelable`。这个Stack Overflow答案展示了该技术,而这个示例项目则提供了一个完整的工作示例。
其中关键点是`Parcelable`和`byte[]`之间的转换。
/***
 Copyright (c) 2016 CommonsWare, LLC
 Licensed under the Apache License, Version 2.0 (the "License"); you may not
 use this file except in compliance with the License. You may obtain a copy
 of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
 by applicable law or agreed to in writing, software distributed under the
 License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
 OF ANY KIND, either express or implied. See the License for the specific
 language governing permissions and limitations under the License.

 From _The Busy Coder's Guide to Android Development_
 https://commonsware.com/Android
 */

package com.commonsware.android.parcelable.marshall;

import android.os.Parcel;
import android.os.Parcelable;

// inspired by https://dev59.com/22Ml5IYBdhLWcg3w76sI#18000094

public class Parcelables {
  public static byte[] toByteArray(Parcelable parcelable) {
    Parcel parcel=Parcel.obtain();

    parcelable.writeToParcel(parcel, 0);

    byte[] result=parcel.marshall();

    parcel.recycle();

    return(result);
  }

  public static <T> T toParcelable(byte[] bytes,
                                   Parcelable.Creator<T> creator) {
    Parcel parcel=Parcel.obtain();

    parcel.unmarshall(bytes, 0, bytes.length);
    parcel.setDataPosition(0);

    T result=creator.createFromParcel(parcel);

    parcel.recycle();

    return(result);
  }
}

1
你真是救了我一天啊!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!我无法在Android N系统中使用AlarmManager传递Parcelable数据。 - BIN
感谢CommonsWare告诉我们这个限制。你的回答帮了我们很大的忙。通过了解这个限制,我们打算只传递DB Id。当闹钟被触发时,根据接收到的DB Id执行SQLite查询(使用Room)是否安全? - Cheok Yan Cheng
1
@CheokYanCheng:我不确定您所说的“安全”是什么意思。您将受到所有标准限制:不要在主应用程序线程上执行I/O操作,在Android 8.0+上后台服务只能运行一分钟等。 - CommonsWare
我明白你的意思,也了解从Android O开始启动服务的限制。请问,如果我让Executors.newSingleThreadExecutor在BroadcastReceiver的onReceive方法中执行数据库操作(向表中写入几行数据),这样做可以吗?我已经测试过,目前看来是可以的。但是,我不确定是否存在任何边缘情况。 - Cheok Yan Cheng
@CheokYanCheng:“这样做可以吗?”-- 如果没有任何服务或其他内容使您的进程保持运行状态,在onReceive()返回后,您的进程可能在任何时间点被终止。这可能会在您的线程完成工作之前发生。 goAsync()会有所帮助,或将工作委派给一个服务。 - CommonsWare
启动另一个服务来进行简单的数据库更新太过繁琐。但是,goAsync似乎是一种解决方法。感谢您告诉我。我希望它不会返回null。 - Cheok Yan Cheng

15

在问题下面的评论中,按照@David Wasser的建议,我成功解决了同样的问题:

    public static void setAlarm(@NonNull Context context, @NonNull Todo todo) {

      ...

      Intent intent = new Intent(context, AlarmReceiver.class);

      Bundle todoBundle = new Bundle();
      todoBundle.putParcelable("KEY_TODO", todo);

      intent.putExtra("KEY_TODO", todoBundle); // i just reuse the same key for convenience

      ...

    }

然后在广播接收器中像这样提取bundle:

    public void onReceive(Context context, Intent intent) {

        Bundle todoBundle = intent.getBundleExtra("KEY_TODO");

        Todo todo;

        if (todoBundle != null ) {
            todo = todoBundle.getParcelable("KEY_TODO");
        }

    }

谢谢Hubert。这个解决方案太棒了。我浪费了一天的时间来解决这个问题。 - chaitanya dalvi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接