Android源码篇-深入理解粘性广播(1)

广播

作为Android的四大组件之一,广播的用途还是非常广泛的。广播是一种同时通知多个对象的事件通知机制,顾名思义也能大概知道是这个意思,类似日常生活中的大喇叭广播,多个人可以收听,人们大都只关心和自己有关的事情,而对和自己无关的事情进行屏蔽,Android中的广播和这个差不多。

基本概念

普通广播:(略)
有序广播:(略)
粘性广播:只要不移除,就一直在内存当中。本文主要说这种广播,同时介绍一个Google的一个"bug",不知google是有意的还是无意而引起的这个bug。粘性广播是Depracated,Google已经不建议使用,存在安全隐患。
静态注册:(略)
动态注册:(略)

发送广播

下图是是广播发送时序图(比较粗略,不过通过这个图能足够梳理发送广播的流程)。本文基于的是Android11。关于怎么画时序图,每个人都有每个人的喜好工具,笔者比较喜欢开源工具plantuml.

plantuml

官方文档: https://plantuml.com/zh/sequence-diagram。
Android启动流程中的图就是使用plantuml来绘制的。下图是createVirtualDisplay的流程图,plantuml的代码如下,基本上常用的功能下面的代码都有涉及。

@startuml
WFDSession -> DisplayManager: createVirtualDisplay
note left :Surface from native method
DisplayManager ->DisplayManagerGlobal:createVirtualDisplay
activate DisplayManagerGlobal
DisplayManagerGlobal -> DisplayManagerService: createVirtualDisplay
DisplayManagerService -> DisplayManagerService: createVirtualDisplayInternal
    activate DisplayManagerService
     DisplayManagerService ->  VirtualDisplayAdapter: createVirtualDisplayLocked
     VirtualDisplayAdapter --> DisplayManagerService: DisplayDevice device
     DisplayManagerService ->DisplayManagerService:   handleDisplayDeviceAddedLocked
        activate DisplayManagerService #DarkSalmon
          DisplayManagerService ->DisplayManagerService: addLogicalDisplayLocked(device);
            activate DisplayManagerService #00fa9a
                DisplayManagerService -> DisplayManagerService:assignDisplayIdLocked
                note left: displayId
                    activate DisplayManagerService #AB82FF
                    deactivate
                DisplayManagerService -> DisplayManagerService:assignLayerStackLocked(displayId)
                note left:layerStack
                    activate DisplayManagerService #AB82FF
                    deactivate
                DisplayManagerService -> LogicalDisplay: new LogicalDisplay
                LogicalDisplay --> DisplayManagerService:LogicalDisplay display
                DisplayManagerService ->DisplayManagerService: configureColorModeLocked
                    activate DisplayManagerService #AB82FF
                    deactivate
                DisplayManagerService ->DisplayManagerService: sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);
                    activate DisplayManagerService #AB82FF
                    deactivate
            deactivate
            DisplayManagerService -> DisplayManagerService:updateDisplayStateLocked
                    activate DisplayManagerService #AB82FF
                    deactivate
            DisplayManagerService -> DisplayManagerService:scheduleTraversalLocked(false)
                    activate DisplayManagerService #AB82FF
                    deactivate
        deactivate
        DisplayManagerService -> DisplayManagerService:findLogicalDisplayForDeviceLocked(device)
                    activate DisplayManagerService #AB82FF
                    deactivate
        DisplayManagerService --> DisplayManagerGlobal:display.getDisplayIdLocked()
    deactivate
DisplayManagerGlobal -> DisplayManagerGlobal:getRealDisplay(displayId)
                    activate DisplayManagerGlobal #AB82FF
                    deactivate
DisplayManagerGlobal --> DisplayManager: new VirtualDisplay 
deactivate
DisplayManager --> WFDSession: VirtualDisplay mVirtualDisplay
@enduml

效果:
在这里插入图片描述

sendStickyBroadcast
声明

通过下面的声明可知要使用sendStickyBroadcast,需要android.Manifest.permission.BROADCAST_STICKY权限. 关于Android的权限后续文章会专门介绍.

 @Deprecated
 @RequiresPermission(android.Manifest.permission.BROADCAST_STICKY)
 public abstract void sendStickyBroadcast(@RequiresPermission Intent intent);
流程图

sendStickyBroadcast的主要流程如下所示,后面的部分和发送正常的广播流程一致.
在这里插入图片描述流程涉及到aidl, android中实现进程通信的一种最重要的机制。后面会开专题来讲解aidl.

流程中关键代码

判断caller是否是System.

final boolean isCallerSystem;
switch (UserHandle.getAppId(callingUid)) {
    case ROOT_UID:
    case SYSTEM_UID:
    case PHONE_UID:
    case BLUETOOTH_UID:
    case NFC_UID:
    case SE_UID:
    case NETWORK_STACK_UID:
        isCallerSystem = true;
        break;
    default:
        isCallerSystem = (callerApp != null) && callerApp.isPersistent();
        break;
}

Intent->filterEquals的实现:

public boolean filterEquals(Intent other) {
   if (other == null) {
       return false;
   }
   if (!Objects.equals(this.mAction, other.mAction)) return false;
   if (!Objects.equals(this.mData, other.mData)) return false;
   if (!Objects.equals(this.mType, other.mType)) return false;
   if (!Objects.equals(this.mIdentifier, other.mIdentifier)) return false;
   if (!(this.hasPackageEquivalentComponent() && other.hasPackageEquivalentComponent())
           && !Objects.equals(this.mPackage, other.mPackage)) {
       return false;
   }
   if (!Objects.equals(this.mComponent, other.mComponent)) return false;
   if (!Objects.equals(this.mCategories, other.mCategories)) return false;

   return true;
}
userid uid appid 多用户

userid:就是有多少个实际的用户罗,例如老爸很穷,要跟儿子共用一台手机,那可以跟手机将两个用户,user 0和 user 1。两个用户的应用和数据是独立的。
uid:跟应用进程相关。除了 sharduid的应用,每个用户的每个应用的 uid不一样的。用户 0的应用的uid从一万开始算。

appid:跟 app相关,包名相同的 appid都一样。即使是不同用户。例如你和儿子都在这台手机装了微信,但这两个微信的appid是一样的。uid与 userId存在一种计算关系( uid = userId * 1000000 + appId)

多用户其实是系统为应用的 data目录和 storage目录分配了一份不同且独立的存储空间,不同用户下的存储空间互不影响且没有权限访问。同时,系统中的AMS、 PMS、 WMS等各大服务都会针对userId/UserHandle进行多用户适配,并在用户启动、切换、停止、删除等生命周期时做出相应策略的改变。通过以上两点,Android创造出来一个虚拟的多用户运行环境.

数据结构

SparseArray: Android 在 Android SdK 为我们提供的一个基础的数据结构,其功能类似于 HashMap。与 HashMap 不同的是它的 Key 只能是 int 值,不能是其他的类型。

    /**
     * State of all active sticky broadcasts per user.  Keys are the action of the
     * sticky Intent, values are an ArrayList of all broadcasted intents with
     * that action (which should usually be one).  The SparseArray is keyed
     * by the user ID the sticky is for, and can include UserHandle.USER_ALL
     * for stickies that are sent to all users.
     */
    final SparseArray<ArrayMap<String, ArrayList<Intent>>> mStickyBroadcasts =
            new SparseArray<ArrayMap<String, ArrayList<Intent>>>();

ArrayMap: ArrayMap是一种通用的key-value映射的数据结构,旨在提高内存效率,它与传统的HashMap有很大的不同。它将其映射保留在数组数据结构中:两个数组(其中一个存放每个item的hash值的整数数组,以及key/value对的Object数组)。这避免了它为放入映射的每个item创建额外的对象,并且它还积极地控制这些数组的增长。数组的增长只需要复制数组中的item,而不是重建hash映射。

26/**
27 * ArrayMap is a generic key->value mapping data structure that is
28 * designed to be more memory efficient than a traditional {@link java.util.HashMap}.
29 * It keeps its mappings in an array data structure -- an integer array of hash
30 * codes for each item, and an Object array of the key/value pairs.  This allows it to
31 * avoid having to create an extra object for every entry put in to the map, and it
32 * also tries to control the growth of the size of these arrays more aggressively
33 * (since growing them only requires copying the entries in the array, not rebuilding
34 * a hash map).
35 *
36 * <p>Note that this implementation is not intended to be appropriate for data structures
37 * that may contain large numbers of items.  It is generally slower than a traditional
38 * HashMap, since lookups require a binary search and adds and removes require inserting
39 * and deleting entries in the array.  For containers holding up to hundreds of items,
40 * the performance difference is not significant, less than 50%.</p>
41 *
42 * <p>Because this container is intended to better balance memory use, unlike most other
43 * standard Java containers it will shrink its array as items are removed from it.  Currently
44 * you have no control over this shrinking -- if you set a capacity and then remove an
45 * item, it may reduce the capacity to better match the current size.  In the future an
46 * explicit call to set the capacity should turn off this aggressive shrinking behavior.</p>
47 */
48public final class ArrayMap<K, V> implements Map<K, V> {

stickyBroadcast使用

源代码

AndroidManifest.xml 需要添加如下权限:

<uses-permission android:name="android.permission.BROADCAST_STICKY" />

MainActivity.java:
通过分析前面的intentFilter方法可知,当包不一致的时候,尽管action是一致的,但是intent是不同的。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        password_et = (EditText) this.findViewById(R.id.password);
        username_et = (EditText) this.findViewById(R.id.username);
        message_tv = ((TextView) findViewById(R.id.textView));
        registerReceiver(new MyReceiver(), new IntentFilter("send"));

        this.findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setAction("send");
                sendStickyBroadcast(intent);
				//注意这里,设置了package,这样两个intent是不一样的
                intent.setPackage("com.android.systemui");
                sendStickyBroadcast(intent);
            }
        });

    }

MyReceiver.java

public class MyReceiver extends BroadcastReceiver {

    private static final String TAG = MyReceiver.class.getSimpleName();
    @Override
    public void onReceive(Context context, Intent intent) {
                Log.d(TAG,"burning "+ intent.getPackage() + ":"+intent.getAction());
                //移除广播
                context.removeStickyBroadcast(intent);
    }
}
运行结果

第一次运行application,点击登陆:

12-28 21:33:01.153 14806 14806 D MyReceiver: burning null:send

关闭程序,再次运行application:
(1)在没有点击登陆的时候,可以看到下面的信息,居然接收到包名是com.android.systemui的intent。

这个一方面证明了sticky broadcast在内存中一直存在并且可以在不同的包之间使用,这个很明显是存在安全隐患的。同时证明了removeStickyBroadcast移除的是没有设置package的intent. 显然如果removeStickyBroadcast使用不当,会造成令人迷惑的结果。

12-28 21:34:55.649 15394 15394 D MyReceiver: burning com.android.systemui:send

(2)点击登陆,可以看到下面的输出:

12-28 21:35:00.801 15394 15394 D MyReceiver: burning null:send
解惑

removeStickyBroadcast最终会调用AMS中的unbroadcastIntent:

    public final void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) {
        // Refuse possible leaked file descriptors
        if (intent != null && intent.hasFileDescriptors() == true) {
            throw new IllegalArgumentException("File descriptors passed in Intent");
        }

        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
                userId, true, ALLOW_NON_FULL, "removeStickyBroadcast", null);

        synchronized(this) {
            if (checkCallingPermission(android.Manifest.permission.BROADCAST_STICKY)
                    != PackageManager.PERMISSION_GRANTED) {
                String msg = "Permission Denial: unbroadcastIntent() from pid="
                        + Binder.getCallingPid()
                        + ", uid=" + Binder.getCallingUid()
                        + " requires " + android.Manifest.permission.BROADCAST_STICKY;
                Slog.w(TAG, msg);
                throw new SecurityException(msg);
            }
            ArrayMap<String, ArrayList<Intent>> stickies = mStickyBroadcasts.get(userId);
            if (stickies != null) {
                ArrayList<Intent> list = stickies.get(intent.getAction());
                if (list != null) {
                    int N = list.size();
                    int i;
                    for (i=0; i<N; i++) {
                        if (intent.filterEquals(list.get(i))) {
                            list.remove(i);
                            break;
                        }
                    }
                    if (list.size() <= 0) {
                        stickies.remove(intent.getAction());
                    }
                }
                if (stickies.size() <= 0) {
                    mStickyBroadcasts.remove(userId);
                }
            }
        }
    }

registerReceiver实现:

    public Intent registerReceiver(IApplicationThread caller, String callerPackage,
            IIntentReceiver receiver, IntentFilter filter, String permission, int userId,
            int flags) {
       . . . 
        ArrayList<Intent> allSticky = null;
        if (stickyIntents != null) {
            final ContentResolver resolver = mContext.getContentResolver();
            // Look for any matching sticky broadcasts...
            for (int i = 0, N = stickyIntents.size(); i < N; i++) {
                Intent intent = stickyIntents.get(i);
                // Don't provided intents that aren't available to instant apps.
                if (instantApp &&
                        (intent.getFlags() & Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) == 0) {
                    continue;
                }
                // If intent has scheme "content", it will need to acccess
                // provider that needs to lock mProviderMap in ActivityThread
                // and also it may need to wait application response, so we
                // cannot lock ActivityManagerService here.
                if (filter.match(resolver, intent, true, TAG) >= 0) {
                    if (allSticky == null) {
                        allSticky = new ArrayList<Intent>();
                    }
                    allSticky.add(intent);
                }
            }
        }

        // The first sticky in the list is returned directly back to the client.
        Intent sticky = allSticky != null ? allSticky.get(0) : null;
        if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Register receiver " + filter + ": " + sticky);
        if (receiver == null) {
            return sticky;
        }
        . . . 
        return sticky;
        }
    }

返回值是:

Intent sticky = allSticky != null ? allSticky.get(0) : null;

显然,第二次启动application之后,在onCreate方法中调用了registerReceiver方法,方相应action对应的intent是设置package的(因为只有一个),所以会出现下面的日志

12-28 21:34:55.649 15394 15394 D MyReceiver: burning com.android.systemui:send

但是,当点击登陆的时候,为什么不是设置package的intent,而是没有设置package的intent。

12-28 21:35:00.801 15394 15394 D MyReceiver: burning null:send

AMS中部分输出日志:

11-04 13:08:28.976  1672 15703 W ActivityManager: burning broadcastIntentLocked sticy true
11-04 13:08:28.977  1672 15703 W ActivityManager: burning broadcastIntentLocked sticy enter
11-04 13:08:28.977  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  UserHandle.USER_ALLtrue
11-04 13:08:28.977  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  userId0 true
11-04 13:08:28.977  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  stickiesCount1
11-04 13:08:28.977  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  stickiesCount filterIntent { act=send flg=0x10 pkg=com.android.systemui }
11-04 13:08:28.978  1672 15703 W ActivityManager: burning broadcastIntentLocked sticy true
11-04 13:08:28.978  1672 15703 W ActivityManager: burning broadcastIntentLocked sticy enter
11-04 13:08:28.978  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  UserHandle.USER_ALLtrue
11-04 13:08:28.979  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  userId0 true
11-04 13:08:28.979  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  stickiesCount2
11-04 13:08:28.979  1672 15703 W ActivityManager: burning broadcastIntentLocked stickies  stickiesCount filterIntent { act=send flg=0x10 pkg=com.android.systemui }
11-04 13:08:28.984  6016  6016 D com.example.stickytest.MainActivity: burning null:send
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent userId = 0intent Intent { act=send flg=0x10 }
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent stickies N=2
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent stickies Intent { act=send flg=0x10 pkg=com.android.systemui }
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent stickies Intent { act=send flg=0x10 }
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent stickies filterEqualsIntent { act=send flg=0x10 }
11-04 13:08:28.985  1672 15703 W ActivityManager: burning unbroadcastIntent userId = 0

通过上面的日志可以看出当前应用只接收了不带Package的intent,并且移除了不带package的intent的.因此可以推测带package的intent被相应的包接收了.

但是这样又有一个问题,第二次启动的时候又为何可以接收到带有package的intent?

 BroadcastRecord r = new BroadcastRecord(queue, intent, null,
         null, null, -1, -1, false, null, null, OP_NONE, null, receivers,
         null, 0, null, null, false, true, true, -1, false,
         false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */);
public void enqueueParallelBroadcastLocked(BroadcastRecord r) {
    mParallelBroadcasts.add(r);
    enqueueBroadcastHelper(r);
}
deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false, i);
817                performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
818                        new Intent(r.intent), r.resultCode, r.resultData,
819                        r.resultExtras, r.ordered, r.initialSticky, r.userId);
590                    app.thread.scheduleRegisteredReceiver(receiver, intent, resultCode,
591                            data, extras, ordered, sticky, sendingUser, app.getReportedProcState());
1161        public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
1162                int resultCode, String dataStr, Bundle extras, boolean ordered,
1163                boolean sticky, int sendingUser, int processState) throws RemoteException {
1164            updateProcessState(processState, false);
1165            receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
1166                    sticky, sendingUser);
1167        }

Frida动态获取sticky广播

Hook system_server进程。

写在最后

粘性广播尽量不要使用,一方面是安全问题,另一方面,容易产生令人迷惑的结果。

公众号

更多Android源码内容,欢迎关注我的微信公众号:无情剑客。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

helloworddm

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值