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.
在unRegister的时候根据提供的BroadcastReceiver对象,遍历List找出对应的receiver进行移除。
这样每来一个unRegister我们都需要对Receiver列表做一次遍历,开销有点大,在查操作比较多的时候我们可以使用MAP。
HashMap<BroadCastReceiver, ReceiverRecord>
ReceiverRecord中已经包含BroadcastReceiver对象了,所以value直接使用IntentFilte就行了,简化数据结构。
那如果一个Receiver注册了多个IntentFilter呢?比如说一个receiver对象注册两次传入不同的IntentFilter.所以Value需要改造为ArrayList。 最终用于维护当前Reciver对象列表的数据结构是这样事儿的:
HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers.
当删除时可以通过receiver对象为key在map中快速查找并移除。

发送广播的时候呢?我们知道sendBroadcast时只传入了Intent对象,Intent携带了Action用于和已经注册的receiver匹配。在查找receiver时,需要对HashMap<BroadcastReceiver, ArrayList<IntentFilter>> mReceivers 的Value进行遍历,每一个Value ArrayList 又需要遍历一次。这个查找的开销实在太大了。
看来我们为了实现Action和receiver的快速匹配需要再维护一个数据结构了。同样是频繁查找的需求使用HashMap.
将Action作为Key,value肯定是与之匹配的receiver了。因为一个Action可能会对应多个receiver,receiver注册的时候可以使用相同的Action.所以value需要使用ArrayList. 当发送广播时可以快速根据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产生互动。

评论

  • Tenant Representation回复

    "Fine way of describing, and fastidious article to obtain facts on the topic of my presentation subject, which i am going to present in university."

  • bags回复

    I'm really impressed with your writing skills as well as with the layout on your weblog. Is this a paid theme or did you modify it yourself? Anyway keep up the nice quality writing, it is rare to see a nice blog like this one these days..

  • How To Be Happy Family回复

    I¡¦ll immediately grasp your rss as I can not find your e-mail subscription hyperlink or e-newsletter service. Do you have any? Please let me recognize in order that I could subscribe. Thanks.

  • Laptop Computers回复

    Wonderful work! This is the type of information that are supposed to be shared around the net. Shame on the search engines for no longer positioning this put up higher! Come on over and discuss with my website . Thanks =)

  • เทพทันใจ回复

    Quality posts is the key to interest the visitors
    to pay a quick visit the site, that's what this site is providing.

  • Yaramaz TV回复

    Nice post. I learn something new and challenging on websites I stumbleupon everyday. It will always be useful to read content from other authors and practice a little something from other sites.

  • เบอร์มงคล回复

    Greetings from Ohio! I'm bored to death at work so I decided to check out your blog on my iphone during lunch break.

    I enjoy the info you provide here and can't wait to take a look when I get home.
    I'm amazed at how fast your blog loaded on my phone ..
    I'm not even using WIFI, just 3G .. Anyways, awesome site!

  • adidas nmd回复

    very nice put up, i actually love this web site, keep on it

  • meat equipment回复

    "I loved your blog article.Really looking forward to read more. Much obliged."

  • diaper bags回复

    wonderful issues altogether, you simply gained a emblem new reader. What may you recommend in regards to your put up that you just made a few days ago? Any positive?

  • yeezy boost回复

    My spouse and i were quite joyous John could complete his researching by way of the ideas he received out of the web page. It is now and again perplexing just to always be freely giving methods which people may have been making money from. Therefore we figure out we need the writer to give thanks to for that. All the explanations you've made, the straightforward website menu, the relationships you can help create - it's all powerful, and it's really making our son in addition to us recognize that this subject matter is enjoyable, and that is rather pressing. Many thanks for the whole thing!

  • what's a dispensary回复

    This is very interesting, You are a very skilled blogger. I ave joined your rss feed and look forward to seeking more of your wonderful post. Also, I have shared your web site in my social networks!

  • Luiz Gastao Bittencourt da Silva回复

    I think that everything published made a bunch of sense. However, what about this? suppose you were to create a awesome title? I ain't saying your content is not solid, however what if you added a post title that grabbed folk's attention? I mean %BLOG_TITLE% is kinda boring. You could glance at Yahoo's home page and see how they create post headlines to get people to open the links. You might add a video or a pic or two to grab readers excited about everything've written. Just my opinion, it might make your website a little livelier.|

  • casino games online for real money回复

    Wow, superb blog layout! How long have you been blogging for? you made blogging look easy. The overall look of your web site is magnificent, as well as the content!

  • london escort elite回复

    Wow, that as what I was exploring for, what a stuff! present here at this webpage, thanks admin of this web page.

  • Newark Airport Car Service回复

    "Wow, awesome blog layout! How lengthy have you ever been blogging for? you make running a blog glance easy. The entire glance of your site is great, as well as the content material!"

  • this website回复

    There is definately a great deal to know about this subject. I love all of the points you ave made.

  • dispensary seo回复

    the home as value, homeowners are obligated to spend banks the real difference.

  • Newark Airport Car Service回复

    "I simply could not leave your site before suggesting that I really enjoyed the usual info a person supply to your guests? Is gonna be back continuously to inspect new posts"

  • see the website回复

    Pretty! This has been an extremely wonderful post. Thanks for providing this information.

  • Luiz Fernando Monteiro Bittencourt回复

    Having read this I believed it was rather enlightening. I appreciate you finding the time and energy to put this informative article together. I once again find myself spending way too much time both reading and leaving comments. But so what, it was still worth it!|

  • herb回复

    Very nice post. I just stumbled upon your blog and wanted to say that I ave truly enjoyed browsing your blog posts. In any case I all be subscribing to your feed and I hope you write again very soon!

  • נייק回复

    Thank you for your article.Thanks Again. Cool.

  • weed回复

    Wir freuen uns auf Ihren Anruf oder Ihren Besuch.

  • saps回复

    Im obliged for the blog.Much thanks again. Really Great.

  • to learn more回复

    You obviously know your stuff. Wish I could think of something clever to write here. Thanks for sharing.

  • Originals NMD Grey White回复

    My wife and i were very more than happy that Edward managed to carry out his web research from the precious recommendations he obtained from your own web site. It's not at all simplistic to just continually be giving away steps which some others could have been selling. And we also recognize we have the writer to be grateful to because of that. All the explanations you've made, the easy site navigation, the friendships your site help to foster - it's everything spectacular, and it's really leading our son in addition to our family consider that this article is interesting, and that is unbelievably serious. Thanks for the whole thing!

  • klondike solitaire回复

    Valuable information. Lucky me I found your web site by accident, and I am shocked why this accident didn at happened earlier! I bookmarked it.

  • Luiz Gastao Bittencourt da Silva回复

    I am not sure where you're getting your info, but great topic. I needs to spend some time learning more or understanding more. Thanks for magnificent information I was looking for this information for my mission.|

  • air jordan回复

    you have got a terrific blog here! would you wish to make some invite posts on my blog?

  • try this out回复

    I simply want to mention I am new to blogging and site-building and seriously loved your blog. Likely I’m planning to bookmark your blog post . You absolutely come with wonderful article content. Thanks for sharing with us your web-site.

  • adidas yeezy回复

    Thank you for all your effort on this website. My mum delights in doing research and it's really easy to see why. My spouse and i hear all regarding the lively ways you present priceless tips and tricks by means of your web site and as well strongly encourage response from other people on that topic plus our favorite child is undoubtedly starting to learn a great deal. Take advantage of the rest of the year. You're the one conducting a powerful job.

  • Continued回复

    I just want to mention I am just new to weblog and absolutely loved your web blog. Very likely I’m going to bookmark your website . You certainly have perfect article content. Appreciate it for revealing your webpage.

  • more回复

    I simply want to say I'm very new to blogs and really savored this page. Probably I’m going to bookmark your blog post . You actually have very good well written articles. Appreciate it for revealing your web page.

  • additional info回复

    I just want to say I am newbie to blogs and definitely liked your web site. Likely I’m likely to bookmark your website . You absolutely have good stories. Thanks a lot for sharing your web site.

  • Umanizzare presidios回复

    Hi there, I desire to subscribe for this weblog to take newest updates, thus where can i do it please assist.|

  • cheap jordans canada回复

    "Only wanna tell that this is extremely helpful, Thanks for taking your time to write this."