文章目录
广播
作为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源码内容,欢迎关注我的微信公众号:无情剑客。