使用 PendingIntent 的过程中遇到的问题

需求是这样的,一个 Android 应用要求弹出一个通知,同时通知上还有两个按钮,通知本身点击和按钮点击都对应不同的响应,但是在实现后发现不管点哪个按钮最后都响应同一个事件, 既返回的参数都是同一个。

我的实现方式是这样的,BroadcastReceiver 接收到通知后根据 key 作出不同的响应:

val pendingIntent1 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value1)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

val pendingIntent2 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value2)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

val pendingIntent3 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value3)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

一开始每个响应PendingIntent都是全局变量,只实例化一次,我怀疑是因为后续更新通知导致,所以改成每次更改通知时都新建 一个 PendingIntent,结果还是不行。

后来我把每个 PendingIntent 打印出来如下:

PendingIntent{2fd4457: android.os.BinderProxy@2f2cb44}
PendingIntent{5e7742d: android.os.BinderProxy@2f2cb44}
PendingIntent{c05d62: android.os.BinderProxy@2f2cb44}

而PendingIntent对象都toString() 方法是这样的:

 @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(128);
        sb.append("PendingIntent{");
        sb.append(Integer.toHexString(System.identityHashCode(this)));
        sb.append(": ");
        sb.append(mTarget != null ? mTarget.asBinder() : null);
        sb.append('}');
        return sb.toString();
    }

从上面两张图可以得出结论,虽然 PendingIntent 每次都是一个新的对象,但是他们都指向一个 IIntentSender,呐泥,why?

首先, 我们看看 PendingIntent 是如何创建的?
PendingIntent 提供几个静态方法来实例化自己:

PendingIntent.getActivity()
PendingIntent.getBroadcast()
PendingIntent.getService()
...

以 getBroadcast() 为例

    public static PendingIntent getBroadcast(Context context, int requestCode,
            Intent intent, @Flags int flags) {
        return getBroadcastAsUser(context, requestCode, intent, flags, context.getUser());
    }

    /**
     * @hide
     * Note that UserHandle.CURRENT will be interpreted at the time the
     * broadcast is sent, not when the pending intent is created.
     */
    public static PendingIntent getBroadcastAsUser(Context context, int requestCode,
            Intent intent, int flags, UserHandle userHandle) {
        String packageName = context.getPackageName();
        String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
                context.getContentResolver()) : null;
        try {
            intent.prepareToLeaveProcess(context);
            IIntentSender target =
                ActivityManager.getService().getIntentSender(
                    ActivityManager.INTENT_SENDER_BROADCAST, packageName,
                    null, null, requestCode, new Intent[] { intent },
                    resolvedType != null ? new String[] { resolvedType } : null,
                    flags, null, userHandle.getIdentifier());
            return target != null ? new PendingIntent(target) : null;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

而上面打印出来的不同的 PendingIntent 对应同一个 target 就是上面通过 ActivityManager.getService().getIntentSender(...) 创建的,这里 ActivityManager.getService 返回的是 ActivityManagerService,我们去看看这个方法:


    @Override
    public IIntentSender getIntentSender(int type,
            String packageName, IBinder token, String resultWho,
            int requestCode, Intent[] intents, String[] resolvedTypes,
            int flags, Bundle bOptions, int userId) {
        //...删掉部分不重要的代码
        synchronized(this) {
            int callingUid = Binder.getCallingUid();
            int origUserId = userId;
            userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId,
                    type == ActivityManager.INTENT_SENDER_BROADCAST,
                    ALLOW_NON_FULL, "getIntentSender", null);
            if (origUserId == UserHandle.USER_CURRENT) {
                // We don't want to evaluate this until the pending intent is
                // actually executed.  However, we do want to always do the
                // security checking for it above.
                userId = UserHandle.USER_CURRENT;
            }
            try {
                if (callingUid != 0 && callingUid != SYSTEM_UID) {
                    final int uid = AppGlobals.getPackageManager().getPackageUid(packageName,
                            MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getUserId(callingUid));
                    if (!UserHandle.isSameApp(callingUid, uid)) {
                        String msg = "Permission Denial: getIntentSender() from pid="
                            + Binder.getCallingPid()
                            + ", uid=" + Binder.getCallingUid()
                            + ", (need uid=" + uid + ")"
                            + " is not allowed to send as package " + packageName;
                        Slog.w(TAG, msg);
                        throw new SecurityException(msg);
                    }
                }
                // ... 最终会调用和返回这个方法, 继续往下看看
                return getIntentSenderLocked(type, packageName, callingUid, userId,
                        token, resultWho, requestCode, intents, resolvedTypes, flags, bOptions);

            } catch (RemoteException e) {
                throw new SecurityException(e);
            }
        }
    }

    IIntentSender getIntentSenderLocked(int type, String packageName,
            int callingUid, int userId, IBinder token, String resultWho,
            int requestCode, Intent[] intents, String[] resolvedTypes, int flags,
            Bundle bOptions) {
         // 删掉部分不重要的代码
        final boolean noCreate = (flags&PendingIntent.FLAG_NO_CREATE) != 0;
        final boolean cancelCurrent = (flags&PendingIntent.FLAG_CANCEL_CURRENT) != 0;
        final boolean updateCurrent = (flags&PendingIntent.FLAG_UPDATE_CURRENT) != 0;
        flags &= ~(PendingIntent.FLAG_NO_CREATE|PendingIntent.FLAG_CANCEL_CURRENT
                |PendingIntent.FLAG_UPDATE_CURRENT);
        
// 这里根据传经来的一些参数构建来一个 key, 这个里很关键
        PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, activity,
                resultWho, requestCode, intents, resolvedTypes, flags,
                SafeActivityOptions.fromBundle(bOptions), userId);
        WeakReference<PendingIntentRecord> ref;
        ref = mIntentSenderRecords.get(key);
        PendingIntentRecord rec = ref != null ? ref.get() : null;
        if (rec != null) {
            if (!cancelCurrent) {
                if (updateCurrent) {
                    if (rec.key.requestIntent != null) {
                        rec.key.requestIntent.replaceExtras(intents != null ?
                                intents[intents.length - 1] : null);
                    }
                    if (intents != null) {
                        intents[intents.length-1] = rec.key.requestIntent;
                        rec.key.allIntents = intents;
                        rec.key.allResolvedTypes = resolvedTypes;
                    } else {
                        rec.key.allIntents = null;
                        rec.key.allResolvedTypes = null;
                    }
                }
                return rec;
            }
            makeIntentSenderCanceledLocked(rec);
            mIntentSenderRecords.remove(key);
        }
        if (noCreate) {
            return rec;
        }
        rec = new PendingIntentRecord(this, key, callingUid);
        mIntentSenderRecords.put(key, rec.ref);
        if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) {
            if (activity.pendingResults == null) {
                activity.pendingResults
                        = new HashSet<WeakReference<PendingIntentRecord>>();
            }
            activity.pendingResults.add(rec.ref);
        }
        return rec;
    }

上面的代码中可以看到最终返回的是PendingIntentRecord对象(既target = PendingIntentRecord),它是 IIntentSender.IIntentSender 的子类,调用者传入的参数会构建成一个对象 PendingIntentRecord.Key,而 mIntentSenderRecords 则是一个 HashMap 缓存所有的 PendingIntentRecord。

上面用新建的 key 去 mIntentSenderRecords 查询是否已经存在对应的 PendingIntentRecord,如存在则直接返回,否则新建一个 PendingIntentRecord 塞入 mIntentSenderRecords 并返回。

到这里稍微有一点豁然开朗,但是别急,还有一点点迷雾等待我们去拨开。

既然 HashMap 觉得 PendingIntentRecord 已经存在,那肯定是 PendingIntentRecord.Key 存在,那我们看看这个对象,它是 PendingIntentRecord 的内部类:

    final static class Key {
        final int type;
        final String packageName;
        final ActivityRecord activity;
        final String who;
        final int requestCode;
        final Intent requestIntent;
        final String requestResolvedType;
        final SafeActivityOptions options;
        Intent[] allIntents;
        String[] allResolvedTypes;
        final int flags;
        final int hashCode;
        final int userId;

        private static final int ODD_PRIME_NUMBER = 37;

        Key(int _t, String _p, ActivityRecord _a, String _w,
                int _r, Intent[] _i, String[] _it, int _f, SafeActivityOptions _o, int _userId) {
       //  删掉部分无用代码
            requestIntent = _i != null ? _i[_i.length-1] : null; // 最后一个Intent
            requestResolvedType = _it != null ? _it[_it.length-1] : null;
       // 着重看一下 hash 生成的部分
            int hash = 23;
            // _f 是在 PendingIntent.getBroadcast 时传入的 flags
            hash = (ODD_PRIME_NUMBER*hash) + _f;
            // _r 是在 PendingIntent.getBroadcast 时传入的 requestCode
            hash = (ODD_PRIME_NUMBER*hash) + _r;
            hash = (ODD_PRIME_NUMBER*hash) + _userId;
            if (_w != null) {
                hash = (ODD_PRIME_NUMBER*hash) + _w.hashCode();
            }
            if (_a != null) {
                hash = (ODD_PRIME_NUMBER*hash) + _a.hashCode();
            }
            // requestIntent 就是你传入的 Intent 数组里的最后一个 Intent,
            // 一般使用的时候只有一个 Intent
            // 这里为里生成 hashcode, 调用里 Intent.filterHashCode()
            if (requestIntent != null) {
                hash = (ODD_PRIME_NUMBER*hash) + requestIntent.filterHashCode();
            }
            if (requestResolvedType != null) {
                hash = (ODD_PRIME_NUMBER*hash) + requestResolvedType.hashCode();
            }
            hash = (ODD_PRIME_NUMBER*hash) + (_p != null ? _p.hashCode() : 0);
            hash = (ODD_PRIME_NUMBER*hash) + _t;
            hashCode = hash;
        }

        public int hashCode() {
            return hashCode;
        }
    }

接着看一下 Intent.filterHashCode 的生成算法

    public int filterHashCode() {
        int code = 0;
        if (mAction != null) {
            code += mAction.hashCode();
        }
        if (mData != null) {
            code += mData.hashCode();
        }
        if (mType != null) {
            code += mType.hashCode();
        }
        if (mPackage != null) {
            code += mPackage.hashCode();
        }
        if (mComponent != null) {
            code += mComponent.hashCode();
        }
        if (mCategories != null) {
            code += mCategories.hashCode();
        }
        return code;
    }

那我们捋一下 PendingIntentRecord.key 的 hashcode 和我们传入的哪些参数有关系:
记得我们上面是如何创建 PendingIntent 的吗?

val pendingIntent1 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value1)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

我们传入的参数是:
context, requestCode, Intent, flags
而 Intent 又包含哪些参数呢?

    private String mAction;
    private Uri mData;
    private String mType;
    private String mPackage;
    private ComponentName mComponent;
    private int mFlags;
    private ArraySet<String> mCategories;
    private Bundle mExtras;
    private Rect mSourceBounds;
    private Intent mSelector;
    private ClipData mClipData;
    private int mContentUserHint = UserHandle.USER_CURRENT;
    private String mLaunchToken;

大概是上面这些, 其中参与了生成 hashcode 就是上面提到的 filterHashCode 这个方法里的参数 所以总共参与生成 hashcode 的和调用中有关的参数是:

context, requestCode, flags, Intent.action, Intent.data, Intent.package, Intent.component, Intent.categories

再回过头来看我是如何创建 PendingIntent 的

val pendingIntent1 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value1)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

val pendingIntent2 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value2)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

val pendingIntent3 = PendingIntent.getBroadcast(context, 0, Intent(BROADCAST_ACTION).apply {
            setClass(context, BroadcastReceiver::class.java)
            putExtra(key, value3)
        }, PendingIntent.FLAG_UPDATE_CURRENT)

😭 唯一不同的是 Intent.extras,却没有参与 hashcode 计算,不知道是 google 故意为止还是一个小 bug 😄。

总结:

  • PendingIntent 中 的 target 是 PendingIntentRecord,它们都被缓存到 ActivityManagerService 中的一个HashMap中,是弱引用类型;
  • 影响 PendingIntent 复用的参数主要是: requestCode, flags, Intent.action, Intent.data, Intent.package, Intent.component, Intent.categories
    所以在我的做法基础上,只要修改一下 requestCode 为不同的值就解决了。

参考:说说PendingIntent的内部机制

完!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容