Android高效安全的本地广播LocalBroadcast完全解析

背景

广播作为Android 四大组件有非常广泛的用途。广播可以用作进程间通信,也会用作进程内部某些组件内消息的传递。
这就会有个问题,如果想让发送的广播只有我自己能收到,不想被别人劫持到,来获取到广播中的敏感信息。
另外其他人如果发送相同Action的广播来伪造真正的广播,就会欺骗我的receiver.

如何安全高效的实现进程内部的广播发送呢?
有人说可以使用给广播加权限啊,你可以在Intent中指定PackageName 啊,后面的文章详解,先简单看下:

  1. 当应用程序发送某个广播时系统会将发送的Intent与系统中所有注册的BroadcastReceiver的IntentFilter进行匹配,若匹配成功则执行相应的onReceive函数。可以通过类似sendBroadcast(Intent,String)的接口在发送广播时指定接收者必须具备的permission。或通过Intent.setPackage设置广播仅对某个程序有效。

  2. 当应用程序注册了某个广播时,即便设置了IntentFilter还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似registerReceiver(BroadcastReceiver,IntentFilter,String,android.os.Handler)的接口指定发送者必须具备的permission,对于静态注册的广播可以通过android:exported="false"属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。

当然这都是书上告诉我们的方式,但是我感觉还不够简单。当然经过一番配置你可以实现了。
好了,现在安全解决了,那高效呢?

我们翻看context.sendBroadcast源码,看到发送广播的流程真的是相当的复杂啊。曾经天真年少的我竟然幻想一天弄懂广播的整个过程,但当我看到sendBroadcast方法的行数时我脸上是大写的崩溃。暂且不谈广播队列的分发规则和过程。这中间是存在的两次binder call就让这个过程变的不是那么高效。
首先你sendBroadcast会把广播信息告诉System_server (第一次Binder call),然后system_server经过一番查看找到你要的receivers,然后进入分发队列等待分发(过程很复杂),然后调用APP进程receiver的onReceiver()方法(第二次Binder call).大兄弟,我明明只想在我的进程内部发送一个广播在进程内部接收,为啥还要通过system_server呢。就算你长得帅,你有Free style,可是你很忙啊,找你的人那么多。自己的事情自己做,这是小学了老师经常教导我们的。看来Google的程序员一直没有忘记小学老师的教诲:

简介

看,迈着整齐步伐雄赳赳气昂昂向我们走来的是LocalBroadcast:
先来看官方说明:

 **
 - Helper to register for and send broadcasts of Intents to local objects
 - within your process.  This is has a number of advantages over sending
 - global broadcasts with {@link android.content.Context#sendBroadcast}:
 - <ul>
 - <li> You know that the data you are broadcasting won't leave your app, so
 - don't need to worry about leaking private data.
 - <li> It is not possible for other applications to send these broadcasts to
 - your app, so you don't need to worry about having security holes they can
 - exploit.
 - <li> It is more efficient than sending a global broadcast through the
 - system.
 - </ul>
 */

意思就是这个很牛逼,和全局广播相比有很多数不清的优势。(看来实现这个的哥们和实现全局广播的哥们关系不大好,竟然用这个词语: has a number of advantages) 。

  • 广播中携带的数据只会在你的APP中,不会暴露给其他APP,所以不用担心数据泄露的问题。
  • 其他APP无法伪造广播来欺骗你的Receiver

源码分析

我们下面来看下LocalBroadcastManager的源码:
https://android.googlesource.com/platform/frameworks/support/+/android-support-lib-19.1.0/v4/java/android/support/v4/content/LocalBroadcastManager.java

1.先来看下LocalBroadcastManager的构造,是使用标准的单例模式实现的。
APP开发者拿到mInstance之后就可以调用registerReceiver、unregisterReceiver、sendBroadcast。

    private final Handler mHandler;
    private static final Object mLock = new Object();
    private static LocalBroadcastManager mInstance;

    public static LocalBroadcastManager getInstance(Context context) {
    synchronized (mLock) {
            if (mInstance == null) {
                mInstance = new LocalBroadcastManager(context.getApplicationContext());
            }
            return mInstance;
        }
    }

    private LocalBroadcastManager(Context context) {
        mAppContext = context;
        mHandler = new Handler(context.getMainLooper()) {

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_EXEC_PENDING_BROADCASTS:
                        executePendingBroadcasts();
                        break;
                    default:
                        super.handleMessage(msg);
                }
            }
        };
    }

看到构造函数中没有做复杂的操作,在主线程初始化了一个Handler.
可以猜测到这个Handler正是用于对广播的分发。

2.广播的注册、反注册、发送流程
如果让我们来自己来实现广播的注册、反注册、发送我们会怎么搞呢?
首先,注册的时候需要提供BroadcastReceiver和对应的IntentFilter,我们可以对这种数据结构进行封装,放到一个类中ReceiverRecord。
然后维护一个ReceiverRecord对象列表,用于记录当前注册了哪些BroadcastReceiver。可以简单使用ArrayList<ReceiverRecord>.
在unRegister的时候根据提供的BroadcastReceiver对象,遍历List找出对应的receiver进行移除。
这样每来一个unRegister我们都需要对Receiver列表做一次遍历,开销有点大,在查操作比较多的时候我们可以使用MAP。
HashMap<BroadCastReceiver, ReceiverRecord>
ReceiverRecord中已经包含BroadcastReceiver对象了,所以value直接使用IntentFilte就行了,简化数据结构。
那如果一个Receiver注册了多个IntentFilter呢?比如说一个receiver对象注册两次传入不同的IntentFilter.所以Value需要改造为ArrayList<IntentFilter>。 最终用于维护当前Reciver对象列表的数据结构是这样事儿的:
HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers.
当删除时可以通过receiver对象为key在map中快速查找并移除。

发送广播的时候呢?我们知道sendBroadcast时只传入了Intent对象,Intent携带了Action用于和已经注册的receiver匹配。在查找receiver时,需要对HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers 的Value进行遍历,每一个Value ArrayList<IntentFilter> 又需要遍历一次。这个查找的开销实在太大了。
看来我们为了实现Action和receiver的快速匹配需要再维护一个数据结构了。同样是频繁查找的需求使用HashMap.
将Action作为Key,value肯定是与之匹配的receiver了。因为一个Action可能会对应多个receiver,receiver注册的时候可以使用相同的Action.所以value需要使用ArrayList<BroadcastReceiver>. 当发送广播时可以快速根据Action找到对应的receiver。对了,不仅仅要使用Action匹配,filter中还有其他信息匹配成功之后才能确认是真正的receiver.所以需要使用ReceiverRecord作为value,因为不仅包含了receiver对象,同时包含了IntentFilter.所以最终的数据结构是HashMap<String,ArrayList<ReceiverRecord>>.

我们来看Google是怎么实现的呢?

先来看两个内部类:

    //内部类ReceiverRecord Receiver记录:用于记录reciver,对应的IntentFilter和是否在broadcast状态
    private static class ReceiverRecord {
        final IntentFilter filter;
        final BroadcastReceiver receiver;
        boolean broadcasting;

        ReceiverRecord(IntentFilter _filter, BroadcastReceiver _receiver) {
            filter = _filter;
            receiver = _receiver;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder(128);
            builder.append("Receiver{");
            builder.append(receiver);
            builder.append(" filter=");
            builder.append(filter);
            builder.append("}");
            return builder.toString();
        }
    }

    //BroadcastRecord 广播记录:用于记录广播的intent以及有哪些对应的ReceiverRecord
    private static class BroadcastRecord {
        final Intent intent;
        final ArrayList<ReceiverRecord> receivers;

        BroadcastRecord(Intent _intent, ArrayList<ReceiverRecord> _receivers) {
            intent = _intent;
            receivers = _receivers;
        }
    }

再来看几个成员变量:

  //维护一个mReceivers Map 记录一个所有的receiver,每一个对应哪些Intentfilters。主要用于记录当前有哪些receiver需要维护接受广播。
     //方便广播的反注册,反注册时可以快速找到filter从而找到Action,从而操作mAction.试想如果没有mReceivers,只能全部遍历mAction找出所有BroadcastRecord,从而找到filter和Action,性能很差。
    private final HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers
            = new HashMap<BroadcastReceiver, ArrayList<IntentFilter>>();
    //维护一个mActions Map 记录所有的Action,每一个对应哪些ReceiverRecord
    private final HashMap<String, ArrayList<ReceiverRecord>> mActions
            = new HashMap<String, ArrayList<ReceiverRecord>>();

    //维护一个List,记录当前正处在等待状态的广播BroadcastRecord,通过BroadcastRecord可以找到intent对应的receivers
    private final ArrayList<BroadcastRecord> mPendingBroadcasts
            = new ArrayList<BroadcastRecord>();

广播的发送过程

    /**
     * Register a receive for any local broadcasts that match the given IntentFilter.
     *
     * @param receiver The BroadcastReceiver to handle the broadcast.
     * @param filter Selects the Intent broadcasts to be received.
     *
     * @see #unregisterReceiver
     */
    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        synchronized (mReceivers) {
            ReceiverRecord entry = new ReceiverRecord(filter, receiver);
            //查找receiver有没有在mReceiver记录中,如果不在需要添加进去。
            ArrayList<IntentFilter> filters = mReceivers.get(receiver);
            if (filters == null) {
                filters = new ArrayList<IntentFilter>(1);
                mReceivers.put(receiver, filters);
            }
            //将IntentFilter加入receiver对应的匹配规则中。filters为mReceivers map的value,类型是IntentFilter的ArrayList。
            //同一个receiver可能有多个IntentFilter。mReceivers就记录了所有的receiver,并且指明每一个receiver所能匹配到的IntentFilter.
            filters.add(filter);
            //开始遍历IntentFilter中的Action.检查Action是否在mActions,如果不在不要添加进去。
            //mActions是Action为Key , ArrayList<ReceiverRecord>为value的MAP。
            //记录了当前有那些Action,并且每个Action对应的Receiver(ReceiverRecord)是哪个。
            for (int i=0; i<filter.countActions(); i++) {
                String action = filter.getAction(i);
                ArrayList<ReceiverRecord> entries = mActions.get(action);
                if (entries == null) {
                    entries = new ArrayList<ReceiverRecord>(1);
                    mActions.put(action, entries);
                }
                //将ReceiverRcoder对象加入entries:entries是该Action对应的ReceiverRecord列表。
                entries.add(entry);
            }
        }
    }

广播的反注册过程

    /**
     * Unregister a previously registered BroadcastReceiver.  <em>All</em>
     * filters that have been registered for this BroadcastReceiver will be
     * removed.
     *
     * @param receiver The BroadcastReceiver to unregister.
     *
     * @see #registerReceiver
     */
    public void unregisterReceiver(BroadcastReceiver receiver) {
        synchronized (mReceivers) {
            //在Receiver列表中移除要注销的receiver,返回对应的filters.
            ArrayList<IntentFilter> filters = mReceivers.remove(receiver);
            if (filters == null) {
                return;
            }
            //在Action Map中移除对应的receiver
            for (int i=0; i<filters.size(); i++) {
                IntentFilter filter = filters.get(i);
                for (int j=0; j<filter.countActions(); j++) {
                    String action = filter.getAction(j);
                    //根据Action获取receiver列表,移除要删除的receiver.
                    ArrayList<ReceiverRecord> receivers = mActions.get(action);
                    if (receivers != null) {
                        for (int k=0; k<receivers.size(); k++) {
                            if (receivers.get(k).receiver == receiver) {
                                receivers.remove(k);
                                k--;
                            }
                        }
                        //如果发现Action对应的receiver都没有删除掉了,这时候就需要在Action列表中清空。
                        //因为没有receiver来处理这个Action了。
                        if (receivers.size() <= 0) {
                            mActions.remove(action);
                        }
                    }
                }
            }
        }
    }
    
广播的发送过程:
    /**
     * Broadcast the given intent to all interested BroadcastReceivers.  This
     * call is asynchronous; it returns immediately, and you will continue
     * executing while the receivers are run.
     *
     * @param intent The Intent to broadcast; all receivers matching this
     *     Intent will receive the broadcast.
     *
     * @see #registerReceiver
     */
    public boolean sendBroadcast(Intent intent) {
        synchronized (mReceivers) {
            final String action = intent.getAction();
            final String type = intent.resolveTypeIfNeeded(
                    mAppContext.getContentResolver());
            final Uri data = intent.getData();
            final String scheme = intent.getScheme();
            final Set<String> categories = intent.getCategories();

            final boolean debug = DEBUG ||
                    ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0);
            if (debug) Log.v(
                    TAG, "Resolving type " + type + " scheme " + scheme
                    + " of intent " + intent);

            //根据Action从mActions MAP 中取对应的receivers.
            ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
            if (entries != null) {
                if (debug) Log.v(TAG, "Action list: " + entries);

                ArrayList<ReceiverRecord> receivers = null;
                //遍历receivers找出符合IntentFilter条件的
                for (int i=0; i<entries.size(); i++) {
                    ReceiverRecord receiver = entries.get(i);
                    if (debug) Log.v(TAG, "Matching against filter " + receiver.filter);
                    //如果receiver已经在分发中,不做处理。
                    if (receiver.broadcasting) {
                        if (debug) {
                            Log.v(TAG, "  Filter's target already added");
                        }
                        continue;
                    }

                    //检查receiver是否和发送广播时传入的Intent匹配,并加入符合条件的列表receivers
                    int match = receiver.filter.match(action, type, scheme, data,
                            categories, "LocalBroadcastManager");

                    if (match >= 0) {
                        if (debug) Log.v(TAG, "  Filter matched!  match=0x" +
                                Integer.toHexString(match));
                        if (receivers == null) {
                            receivers = new ArrayList<ReceiverRecord>();
                        }
                        receivers.add(receiver);
                        //将receiver 是否在分发中置位true
                        receiver.broadcasting = true;
                    } else {
                        if (debug) {
                            String reason;
                            switch (match) {
                                case IntentFilter.NO_MATCH_ACTION: reason = "action"; break;
                                case IntentFilter.NO_MATCH_CATEGORY: reason = "category"; break;
                                case IntentFilter.NO_MATCH_DATA: reason = "data"; break;
                                case IntentFilter.NO_MATCH_TYPE: reason = "type"; break;
                                default: reason = "unknown reason"; break;
                            }
                            Log.v(TAG, "  Filter did not match: " + reason);
                        }
                    }
                }
                //将broadcasting置位false 加入mPendingBroadcasts等待队列中.
                if (receivers != null) {
                    for (int i=0; i<receivers.size(); i++) {
                        receivers.get(i).broadcasting = false;
                    }
                    mPendingBroadcasts.add(new BroadcastRecord(intent, receivers));
                    //发送消息MSG_EXEC_PENDING_BROADCASTS
                    if (!mHandler.hasMessages(MSG_EXEC_PENDING_BROADCASTS)) {
//通过Handler发送消息来处理                        mHandler.sendEmptyMessage(MSG_EXEC_PENDING_BROADCASTS);
                    }
                    return true;
                }
            }
        }
        return false;
    }
    

可以看到最终把要发送的广播加入队列mPendingBroadcasts,然后使用Handler发送消息给主线程处理的,调用executePendingBroadcasts()进行分发。

    private void executePendingBroadcasts() {
        while (true) {
            BroadcastRecord[] brs = null;
            synchronized (mReceivers) {
                final int N = mPendingBroadcasts.size();
                if (N <= 0) {
                    return;
                }
                brs = new BroadcastRecord[N];
                mPendingBroadcasts.toArray(brs);
                mPendingBroadcasts.clear();
            }
            for (int i=0; i<brs.length; i++) {
                BroadcastRecord br = brs[i];
                for (int j=0; j<br.receivers.size(); j++) {
                    br.receivers.get(j).receiver.onReceive(mAppContext, br.intent);
                }
            }
        }
    }
}

LocalBroadcast也支持使用同步的方式进行分发:

 /**
     * Like {@link #sendBroadcast(Intent)}, but if there are any receivers for
     * the Intent this function will block and immediately dispatch them before
     * returning.
     */
    public void sendBroadcastSync(Intent intent) {
        if (sendBroadcast(intent)) {
            executePendingBroadcasts();
        }
    }

总结

1.LocalBroadcast是APP内部维护的一套广播机制,有很高的安全性和高效性。
所以如果有APP内部发送、接收广播的需要应该使用LocalBroadcast。
2.Receiver只允许动态注册,不允许在Manifest中注册。
3.LocalBroadcastManager所发送的广播action,只能与注册到LocalBroadcastManager中BroadcastReceiver产生互动。

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

推荐阅读更多精彩内容