让我们来构建一个关于"小镇广播系统"的故事,彻底理解 RemoteCallbackList
的精妙之处。想象你是一位负责小镇信息发布的广播员(服务端 ),而小镇居民(客户端 )家里都安装了特制的收音机(回调接口)来接收你的重要通知(比如停水停电、节日活动)。
故事:小镇广播系统的升级
旧系统的问题(普通List)
-
纸质登记册: 你有一个纸质登记册(
List<IRadioCallback>
),记录着所有居民的收音机编号(回调接口引用)。 -
手动广播: 每次有通知,你拿起登记册,对着广播麦克风,按顺序念出收音机编号并喊话(调用回调方法)。
-
灾难场景:
- 居民搬走/收音机坏了(进程死亡): 你对着编号喊话,但对方毫无反应(
DeadObjectException
)。你不知道是收音机坏了还是人搬走了,这个"幽灵收音机"永远留在登记册上,你每次广播都徒劳地喊一次,浪费时间和精力(资源泄露、性能下降)。 - 登记册被涂改(并发修改): 你正喊到第3个编号,突然第5个居民跑来说"我搬家了,把我划掉!"。你手忙脚乱去改登记册,结果念第4个编号时,发现登记册的页码顺序可能已经乱了(
ConcurrentModificationException
),广播中断,一片混乱。
- 居民搬走/收音机坏了(进程死亡): 你对着编号喊话,但对方毫无反应(
-
痛点总结: 纸质登记册无法感知居民收音机的状态,也无法在广播过程中安全地处理登记信息的变更。
智能广播系统(RemoteCallbackList)的诞生
为了解决这些问题,镇长拨款研发了一套"智能广播管理系统 "------RemoteCallbackList<IRadioCallback>
。
-
居民登记(
register
):- 新居民入住(客户端绑定服务并注册回调),带着他的收音机来广播站登记。
- 系统不仅记录收音机的型号(回调接口引用),更重要的是,它用一根特殊的电话线(
IBinder
)连接了这台收音机。这根电话线是这台收音机在系统中的唯一身份标识。 - 系统在这根电话线上安装了一个微型断线检测器(
DeathRecipient
) 。这个检测器的作用是:一旦居民搬走或收音机彻底损坏(电话线物理断开,意味着客户端进程死亡),它能立即向广播站报警!
-
居民搬走/收音机报废(进程死亡):
- 一旦断线检测器报警(
DeathRecipient.binderDied()
被触发),智能系统立即自动 、悄无声息地将这个居民的收音机信息从系统中移除。广播员完全不用操心,系统永远只维护着有效的、能接通的收音机信息。
- 一旦断线检测器报警(
-
发布重要通知(广播消息):
-
当有重要通知(如台风预警)需要广播时,广播员不再直接翻登记册喊话。
-
第一步:生成临时广播名单(
beginBroadcast()
)广播员按下"开始广播"按钮。智能系统立刻行动:
- 锁定登记室: 暂时禁止新的登记或注销(内部同步锁
synchronized
)。 - 抄写有效名单: 系统检查所有通过电话线连接着的、状态良好的收音机,将它们的信息(回调接口)抄写在一张临时便签纸(一个内部数组快照) 上。
- 统计数量: 系统告诉广播员:"当前有
N
台有效的收音机可以广播,这是您的临时便签(快照)。" - 解锁登记室: 允许新的居民登记或注销(不影响当前广播)。
- 锁定登记室: 暂时禁止新的登记或注销(内部同步锁
-
第二步:按便签安全广播(循环
getBroadcastItem(i)
)广播员拿着这张临时便签(快照) ,放心大胆地按顺序(从
0
到N-1
)对着麦克风喊话:javafor (int i = 0; i < N; i++) { IRadioCallback radio = mCallbacks.getBroadcastItem(i); // 获取便签上第 i 台收音机 radio.onBroadcastMessage("台风预警:请关好门窗!"); // 对着这台收音机喊话 }
为什么安全?
- 这张便签是按下"开始广播"按钮那一刻 的有效收音机清单的只读副本。
- 广播过程中,即使有居民搬来登记新收音机,或者有居民跑来注销旧收音机,或者有收音机突然断线,广播员手里的便签都不会变。当前广播循环不受任何干扰。
- 广播员只需要关注便签上的
N
个条目,按顺序喊完即可。
-
第三步:销毁临时便签(
finishBroadcast()
)广播员喊完便签上所有收音机后,必须 按下"结束广播"按钮。这时,智能系统会立即销毁这张临时便签 (释放快照数组)。
⚠️ 不按这个按钮的后果很严重! 广播站会堆满用过的临时便签(内存泄露),最终导致广播站瘫痪(内存不足)。
-
-
居民主动搬走(
unregister
):-
居民来广播站说"我要搬家了,请注销我的收音机"。
-
智能系统会:
- 在登记册中移除他的收音机信息。
- 拆除他收音机电话线上的断线检测器 (
binder.unlinkToDeath()
)。 - 安全地断开电话线连接。
-
RemoteCallbackList 的实现原理(拆解智能广播系统)
-
核心数据库(ArrayMap<IBinder, Callback>):
- 系统内部有一个特殊的登记册(通常是
ArrayMap
),它的 Key 是连接收音机的电话线对象(callback.asBinder()
) ,它的 Value 是一个包含收音机信息(回调接口)和断线检测器(DeathRecipient
)的小盒子(内部类Callback
)。 - 这个登记册就是所有有效收音机的真实记录。
- 系统内部有一个特殊的登记册(通常是
-
断线检测器(DeathRecipient):
-
register(callback)
时:- 获取电话线:
IBinder binder = callback.asBinder()
。 - 创建检测器:
DeathRecipient dr = new DeathRecipient() { void binderDied() { ... 移除登记 ... } }
。 - 安装检测器:
binder.linkToDeath(dr, 0)
。这是生死感知的关键! 它告诉底层通信系统:如果这根电话线断了(客户端进程死),请调用我的binderDied()
方法。
- 获取电话线:
-
binderDied()
被调用时:- 系统知道这根电话线对应的收音机失效了。
- 自动从核心登记册(
ArrayMap
)中移除对应的条目。
-
unregister(callback)
或binderDied()
时:- 拆除检测器:
binder.unlinkToDeath(dr, 0)
。避免无效报警。
- 拆除检测器:
-
-
安全广播三部曲:
-
beginBroadcast()
:- 加锁:
synchronized (mCallbacks) { ... }
锁住核心登记册,防止广播过程中被修改(登记/注销/死亡)。 - 抄便签(创建快照): 将当前登记册中所有有效的小盒子(
Callback
)的信息(主要是回调接口引用)复制 到一个新的数组(mActiveBroadcast
)中。这就是那张临时便签。 - 记录数量:
mBroadcastCount = N
(有效回调数量)。 - 解锁: 允许其他操作修改核心登记册。
- 返回 N: 告诉广播员便签上有多少条。
- 加锁:
-
getBroadcastItem(int index)
:- 检查
index
是否在[0, mBroadcastCount - 1]
范围内。 - 直接从那张只读的临时便签数组(
mActiveBroadcast
) 中取出第index
个小盒子里的收音机信息(回调接口IRadioCallback
) 返回给你。
- 检查
-
finishBroadcast()
:- 销毁便签: 将
mActiveBroadcast
数组设置为null
。这是防止内存泄露的最关键一步! 如果不调用,这个包含 Binder 对象引用的数组会一直被持有。 - 重置
mBroadcastCount = -1
。
- 销毁便签: 将
-
如何使用 RemoteCallbackList(广播员操作手册)
服务端(广播站)代码
java
public class TownBroadcastService extends Service {
// 核心:创建智能广播系统!
private final RemoteCallbackList<IRadioCallback> mRadioCallbacks = new RemoteCallbackList<>();
// 实现 AIDL 定义的镇长服务接口 (I镇长Service.Stub)
private final ITownService.Stub mBinder = new ITownService.Stub() {
@Override
public void registerRadio(IRadioCallback callback) throws RemoteException {
if (callback != null) {
// 新居民带着收音机来登记
mRadioCallbacks.register(callback);
}
}
@Override
public void unregisterRadio(IRadioCallback callback) throws RemoteException {
if (callback != null) {
// 居民主动来注销收音机
mRadioCallbacks.unregister(callback);
}
}
@Override
public void broadcastStormWarning(String message) throws RemoteException {
// 1. 开始广播:生成临时便签,获取有效收音机数量 N
final int numRadios = mRadioCallbacks.beginBroadcast();
try {
// 2. 安全遍历:按照临时便签上的顺序广播
for (int i = 0; i < numRadios; i++) {
try {
// 获取便签上第 i 个收音机
IRadioCallback radio = mRadioCallbacks.getBroadcastItem(i);
// 对着这个收音机喊话 (发送消息)
radio.onBroadcastMessage(message);
} catch (RemoteException e) {
// 极端情况:广播时刚好有收音机断线?(概率极低,因为便签是快照)
// 系统下次会自动清理,这里可忽略或记录日志
}
}
} finally {
// 3. 结束广播:销毁临时便签!(必须放在finally块确保执行)
mRadioCallbacks.finishBroadcast();
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
客户端(居民)代码
java
public class ResidentActivity extends AppCompatActivity {
private ITownService mTownService;
private IRadioCallback mMyRadio; // 我家的收音机
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 连接到镇长服务(广播站)
mTownService = ITownService.Stub.asInterface(service);
try {
// 创建我家收音机的接收功能
mMyRadio = new IRadioCallback.Stub() {
@Override
public void onBroadcastMessage(String message) throws RemoteException {
// 收到广播站的消息!(注意:此方法运行在Binder线程)
runOnUiThread(() -> {
TextView tv = findViewById(R.id.message_view);
tv.setText("广播站通知: " + message);
});
}
};
// 带着我家收音机去广播站登记
mTownService.registerRadio(mMyRadio);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mTownService = null; // 镇长服务暂时失联
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_resident);
// 绑定到镇长服务 (广播站)
Intent intent = new Intent(this, TownBroadcastService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mTownService != null && mMyRadio != null) {
try {
// 我要搬家了!主动去广播站注销我的收音机 (好习惯)
mTownService.unregisterRadio(mMyRadio);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(mConnection); // 解除绑定
}
}
小镇广播箴言(关键总结)
-
为什么必须用它? 管理跨进程回调的核心痛点在于客户端进程随时可能"消失"。
RemoteCallbackList
通过 Binder 死亡监听(断线检测器) 和 广播快照(临时便签) 机制,完美解决了:- 自动清理僵尸回调: 客户端进程死亡时自动移除回调。
- 安全遍历:
beginBroadcast()
/getBroadcastItem()
/finishBroadcast()
三步曲保证了在遍历通知过程中,即使有回调注册、注销或死亡,当前广播循环也绝对安全 ,不会崩溃(ConcurrentModificationException
)。 - 防止资源泄露: 自动清理死亡回调,避免无用的 Binder 引用堆积。
-
死亡监听是基石:
linkToDeath()
是实现自动清理的核心魔法。RemoteCallbackList
帮你封装了这个复杂逻辑。 -
广播三部曲是铁律:
int N = beginBroadcast()
:拿便签 。获取当前有效回调数量N
,开始一次广播会话。for (int i=0; i < N; i++) { ... getBroadcastItem(i) ... }
:按便签喊话。遍历快照,安全调用回调。finishBroadcast()
:撕毁便签 。必须调用! 释放快照资源。忘记这一步会导致严重的内存泄露! (务必放在finally
块中)。
-
主动注销是好习惯: 虽然客户端死亡系统会自动清理,但客户端在不需要回调时(如
Activity.onDestroy()
)主动调用unregister
,能更及时地释放服务端资源。 -
回调线程要注意: 服务端调用
onBroadcastMessage()
是在 Binder 线程池线程执行的。如果回调方法里需要更新 UI(客户端),必须 切回主线程(如runOnUiThread()
,Handler
,LiveData.postValue()
)。 -
快照是只读且瞬态的:
beginBroadcast()
返回的N
和getBroadcastItem(i)
获取的回调,只在这次begin
/finish
会话内有效且安全。不要试图保存它们或在此会话外使用。
通过这个小镇广播系统的故事和拆解,希望你对 RemoteCallbackList
为什么是 Android AIDL 跨进程回调管理的"黄金标准",它的核心机制如何运作,以及如何正确使用它有了清晰、深刻的印象。记住:"拿便签(begin)、按便签喊话(get)、撕便签(finish) " 这个广播员操作流程,你就能驾驭好跨进程回调了!