Android源码篇-深入理解粘性广播(1)
文章目录
- 广播
- 基本概念
- 发送广播
- plantuml
- sendStickyBroadcast
- 声明
- 流程图
- 流程中关键代码
- userid uid appid 多用户
- 数据结构
- stickyBroadcast使用
- 源代码
- 运行结果
- 解惑
- Frida动态获取sticky广播
- 写在最后
- 公众号
广播
作为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: createVirtualDisplayInternalactivate DisplayManagerServiceDisplayManagerService -> VirtualDisplayAdapter: createVirtualDisplayLockedVirtualDisplayAdapter --> DisplayManagerService: DisplayDevice deviceDisplayManagerService ->DisplayManagerService: handleDisplayDeviceAddedLockedactivate DisplayManagerService #DarkSalmonDisplayManagerService ->DisplayManagerService: addLogicalDisplayLocked(device);activate DisplayManagerService #00fa9aDisplayManagerService -> DisplayManagerService:assignDisplayIdLockednote left: displayIdactivate DisplayManagerService #AB82FFdeactivateDisplayManagerService -> DisplayManagerService:assignLayerStackLocked(displayId)note left:layerStackactivate DisplayManagerService #AB82FFdeactivateDisplayManagerService -> LogicalDisplay: new LogicalDisplayLogicalDisplay --> DisplayManagerService:LogicalDisplay displayDisplayManagerService ->DisplayManagerService: configureColorModeLockedactivate DisplayManagerService #AB82FFdeactivateDisplayManagerService ->DisplayManagerService: sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);activate DisplayManagerService #AB82FFdeactivatedeactivateDisplayManagerService -> DisplayManagerService:updateDisplayStateLockedactivate DisplayManagerService #AB82FFdeactivateDisplayManagerService -> DisplayManagerService:scheduleTraversalLocked(false)activate DisplayManagerService #AB82FFdeactivatedeactivateDisplayManagerService -> DisplayManagerService:findLogicalDisplayForDeviceLocked(device)activate DisplayManagerService #AB82FFdeactivateDisplayManagerService --> DisplayManagerGlobal:display.getDisplayIdLocked()deactivate
DisplayManagerGlobal -> DisplayManagerGlobal:getRealDisplay(displayId)activate DisplayManagerGlobal #AB82FFdeactivate
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是不同的。
@Overrideprotected 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() {@Overridepublic 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();@Overridepublic 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 descriptorsif (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源码内容,欢迎关注我的微信公众号:无情剑客。
Android源码篇-深入理解粘性广播(1)相关推荐
- Android源码分析-全面理解Context
前言 Context在android中的作用不言而喻,当我们访问当前应用的资源,启动一个新的activity的时候都需要提供Context,而这个Context到底是什么呢,这个问题好像很好回答又好像 ...
- Android源码分析之理解Window和WindowManager
Window和WindowManager概述 Window是一个抽象类,它的具体实现是PhoneWindow,创建一个Window通过WindowManager 就可以完成.WindowManager ...
- android 浏览器源码分析,从源码出发深入理解 Android Service
原标题:从源码出发深入理解 Android Service 原文链接: 建议在浏览器上打开,删除了大量代码细节,:) 本文是 Android 系统学习系列文章中的第三章节的内容,介绍了 Android ...
- 《深入理解Android内核设计思想(第2版)(上下册)》之Android源码下载及编译
本文摘自人民邮电出版社异步社区<深入理解Android内核设计思想(第2版)(上下册)> 购书地址:http://item.jd.com/12212640.html 试读地址:http:/ ...
- 《深入理解Android内核设计思想(第2版)(上下册)》之Android源码下载及编译...
本文摘自人民邮电出版社异步社区<深入理解Android内核设计思想(第2版)(上下册)> 购书地址:item.jd.com/12212640.ht- 试读地址:www.epubit.com ...
- Android源码分析 - Framework层的Binder(客户端篇)
开篇 本篇以aosp分支android-11.0.0_r25作为基础解析 我们在之前的文章中,从驱动层面分析了Binder是怎样工作的,但Binder驱动只涉及传输部分,待传输对象是怎么产生的呢,这就 ...
- Android Jetpack架构组件之 Room(使用、源码篇)
2019独角兽企业重金招聘Python工程师标准>>> 1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发 ...
- Android源码解析(一)动画篇-- Animator属性动画系统
Android源码解析-动画篇 Android源码解析(一)动画篇-- Animator属性动画系统 Android源码解析(二)动画篇-- ObjectAnimator Android在3.0版本中 ...
- android 使用4大组件的源码,Android Jetpack架构组件之 Paging(使用、源码篇)
1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...
最新文章
- 关于常见的底层驱动源码资料
- python自学流程-Python系统学习流程图,教你一步步学习python
- Spring boot日志关系
- apache目录 vscode_[PHP] php, apache, VS Code安装与配置
- .NET MYSQL数据库操作基类( C#源码)
- 基于 HTML5 WebGL 的 3D 服务器与客户端的通信
- oracle11g设置开机自启动,oracle11g在linux系统下开机自启动设置
- sql azure 语法_在Azure Data Studio中学习用于SQL Notebook的Markdown语言
- 语音识别技术突飞猛进
- bzoj 1237: [SCOI2008]配对(DP)
- 安装Labview2012 “labview 2012 未定义必须的 NIPathsDir属性 maxAFWDIR”
- 列表套字典三者匹配对应关系
- Vista 自动激活工具(最新 最权威 所有版本 可升级)
- ubuntu2004上使用python以及postgresql处理数据 - 针对comp3311
- 沿海当地平均海面与85面高程关系
- sql导出的身份证后几位是000
- 使用 Swift 在 iOS 10 中集成 Siri —— SiriKit 教程(Part 1) 1
- 利用Photoshop进行快速切图
- percona toolkit系列(gh-ost)
- 中文颜色名称与RGB颜色对照表
热门文章
- C# particle class
- 学计算机用什么cpu,做设计用什么cpu
- 中关村商城广告切换纯净代码
- 地理国情监测arcpy将文件夹多个gdb下的要素类featureClass,使用FeatureClassToShapefile_conversion转换成shapefile文件。
- PHP最新我约微博整站系统源码+功能非常多
- php写个发红包_使用PHP编写发红包程序
- 如何获取微信用户的Openid详解(微信网页授权)
- 大学生最应该考的十大最有价值证书
- 君子签用区块链打造电子合同证据链闭环,提升电子合同证据效力
- 摔跤吧爸爸-影评感悟(匍匐泥泞,不忘星空-目标)