原文出處——>[Android應用程序注冊廣播接收器(registerReceiver)的過程分析](http://blog.csdn.net/luoshengyang/article/details/6737352)
前面我們介紹了Android系統的廣播機制,從本質來說,它是一種消息訂閱/發布機制,因此,使用這種消息驅動模型的第一步便是訂閱消息;而對Android應用程序來說,訂閱消息其實就是注冊廣播接收器,本文將探討Android應用程序是如何注冊廣播接收器以及把廣播接收器注冊到哪里去的。
在Android的廣播機制中,ActivityManagerService扮演著廣播中心的角色,負責系統中所有廣播的注冊和發布操作,因此,Android應用程序注冊廣播接收器的過程就把是廣播接收器注冊到ActivityManagerService的過程。Android應用程序是通過調用ContextWrapper類的registerReceiver函數來把廣播接收器BroadcastReceiver注冊到ActivityManagerService中去的,而ContextWrapper類本身又借助ContextImpl類來注冊廣播接收器。
在Android應用程序框架中,Activity和Service類都繼承了ContextWrapper類,因此,我們可以在Activity或者Service的子類中調用registerReceiver函數來注冊廣播接收器。Activity、Service、ContextWrapper和ContextImpl這四個類的關系可以參考前面Android系統在新進程中啟動自定義服務過程(startService)的原理分析一文中描述的Activity類圖。
這篇文章還是繼續以實例來進行情景分析,所用到的例子便是上一篇文章Android系統中的廣播(Broadcast)機制簡要介紹和學習計劃里面介紹的應用程序了,所以希望讀者在繼續閱讀本文之前,先看看這篇文章;又由于Android應用程序是把廣播接器注冊到ActivityManagerService中去的,因此,這里又會涉入到Binder進程間通信機制,所以希望讀者對Android系統的Binder進程間通信機制有所了解,具體請參考Android進程間通信(IPC)機制Binder簡要介紹和學習計劃一文。
開始進入主題了,在Android系統中的廣播(Broadcast)機制簡要介紹和學習計劃一文所介紹的例子中,注冊廣播接收器的操作是MainActivity發起的,我們先來看看注冊過程的序列圖:

在分析這個序列圖之前,我們先來看一下MainActivity是如何調用registerReceiver函數來注冊廣播接收器的:
~~~
public class MainActivity extends Activity implements OnClickListener {
......
@Override
public void onResume() {
super.onResume();
IntentFilter counterActionFilter = new IntentFilter(CounterService.BROADCAST_COUNTER_ACTION);
registerReceiver(counterActionReceiver, counterActionFilter);
}
......
}
~~~
MainActivity在onResume函數里,通過其父類ContextWrapper的registerReceiver函數注冊了一個BroadcastReceiver實例counterActionReceiver,并且通過IntentFilter實例counterActionFilter告訴ActivityManagerService,它要訂閱的廣播是CounterService.BROADCAST_COUNTER_ACTION類型的,這樣,ActivityManagerService在收到CounterService.BROADCAST_COUNTER_ACTION類型的廣播時,就會分發給counterActionReceiver實例的onReceive函數。
接下來,就開始分析注冊過程中的每一個步驟了。
**Step 1. ContextWrapper.registerReceiver**
這個函數實現在frameworks/base/core/java/android/content/ContextWrapper.java文件中:
~~~
public class ContextWrapper extends Context {
Context mBase;
......
@Override
public Intent registerReceiver(
BroadcastReceiver receiver, IntentFilter filter) {
return mBase.registerReceiver(receiver, filter);
}
......
}
~~~
這里的成員變量mBase是一個ContextImpl實例,想知道為什么,可以回過頭去看看Android應用程序啟動過程源代碼分析這篇文章>~<。
**Step 2. ContextImpl.registerReceiver**
這個函數實現在frameworks/base/core/java/android/app/ContextImpl.java文件中:
~~~
class ContextImpl extends Context {
......
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
return registerReceiver(receiver, filter, null, null);
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler) {
return registerReceiverInternal(receiver, filter, broadcastPermission,
scheduler, getOuterContext());
}
private Intent registerReceiverInternal(BroadcastReceiver receiver,
IntentFilter filter, String broadcastPermission,
Handler scheduler, Context context) {
IIntentReceiver rd = null;
if (receiver != null) {
if (mPackageInfo != null && context != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
rd = mPackageInfo.getReceiverDispatcher(
receiver, context, scheduler,
mMainThread.getInstrumentation(), true);
} else {
......
}
}
try {
return ActivityManagerNative.getDefault().registerReceiver(
mMainThread.getApplicationThread(),
rd, filter, broadcastPermission);
} catch (RemoteException e) {
return null;
}
}
......
}
~~~
通過兩個函數的中轉,最終就進入到ContextImpl.registerReceiverInternal這個函數來了。這里的成員變量mPackageInfo是一個LoadedApk實例,它是用來負責處理廣播的接收的,在后面一篇文章講到廣播的發送時(sendBroadcast),會詳細描述。參數broadcastPermission和scheduler都為null,而參數context是上面的函數通過調用函數getOuterContext得到的,這里它就是指向MainActivity了,因為MainActivity是繼承于Context類的,因此,這里用Context類型來引用。
由于條件mPackageInfo != null和context != null都成立,而且條件scheduler == null也成立,于是就調用mMainThread.getHandler來獲得一個Handler了,這個Hanlder是后面用來分發ActivityManagerService發送過的廣播用的。這里的成員變量mMainThread是一個ActivityThread實例,在前面Android應用程序啟動過程源代碼分析這篇文章也描述過了。我們先來看看ActivityThread.getHandler函數的實現,然后再回過頭來繼續分析ContextImpl.registerReceiverInternal函數。
**Step 3. ActivityThread.getHandler**
這個函數實現在frameworks/base/core/java/android/app/ActivityThread.java文件中:
~~~
public final class ActivityThread {
......
final H mH = new H();
private final class H extends Handler {
......
public void handleMessage(Message msg) {
......
switch (msg.what) {
......
}
......
}
......
}
......
final Handler getHandler() {
return mH;
}
......
}
~~~
有了這個Handler之后,就可以分發消息給應用程序處理了。
再回到上一步的ContextImpl.registerReceiverInternal函數中,它通過mPackageInfo.getReceiverDispatcher函數獲得一個IIntentReceiver接口對象rd,這是一個Binder對象,接下來會把它傳給ActivityManagerService,ActivityManagerService在收到相應的廣播時,就是通過這個Binder對象來通知MainActivity來接收的。
我們也是先來看一下mPackageInfo.getReceiverDispatcher函數的實現,然后再回過頭來繼續分析ContextImpl.registerReceiverInternal函數。
**Step 4. LoadedApk.getReceiverDispatcher**
這個函數實現在frameworks/base/core/java/android/app/LoadedApk.java文件中:
~~~
final class LoadedApk {
......
public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
Context context, Handler handler,
Instrumentation instrumentation, boolean registered) {
synchronized (mReceivers) {
LoadedApk.ReceiverDispatcher rd = null;
HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;
if (registered) {
map = mReceivers.get(context);
if (map != null) {
rd = map.get(r);
}
}
if (rd == null) {
rd = new ReceiverDispatcher(r, context, handler,
instrumentation, registered);
if (registered) {
if (map == null) {
map = new HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
mReceivers.put(context, map);
}
map.put(r, rd);
}
} else {
rd.validate(context, handler);
}
return rd.getIIntentReceiver();
}
}
......
static final class ReceiverDispatcher {
final static class InnerReceiver extends IIntentReceiver.Stub {
final WeakReference<LoadedApk.ReceiverDispatcher> mDispatcher;
......
InnerReceiver(LoadedApk.ReceiverDispatcher rd, boolean strong) {
mDispatcher = new WeakReference<LoadedApk.ReceiverDispatcher>(rd);
......
}
......
}
......
final IIntentReceiver.Stub mIIntentReceiver;
final Handler mActivityThread;
......
ReceiverDispatcher(BroadcastReceiver receiver, Context context,
Handler activityThread, Instrumentation instrumentation,
boolean registered) {
......
mIIntentReceiver = new InnerReceiver(this, !registered);
mActivityThread = activityThread;
......
}
......
IIntentReceiver getIIntentReceiver() {
return mIIntentReceiver;
}
}
......
}
~~~
在LoadedApk.getReceiverDispatcher函數中,首先看一下參數r是不是已經有相應的ReceiverDispatcher存在了,如果有,就直接返回了,否則就新建一個ReceiverDispatcher,并且以r為Key值保在一個HashMap中,而這個HashMap以Context,這里即為MainActivity為Key值保存在LoadedApk的成員變量mReceivers中,這樣,只要給定一個Activity和BroadcastReceiver,就可以查看LoadedApk里面是否已經存在相應的廣播接收發布器ReceiverDispatcher了。
在新建廣播接收發布器ReceiverDispatcher時,會在構造函數里面創建一個InnerReceiver實例,這是一個Binder對象,實現了IIntentReceiver接口,可以通過ReceiverDispatcher.getIIntentReceiver函數來獲得,獲得后就會把它傳給ActivityManagerService,以便接收廣播。在ReceiverDispatcher類的構造函數中,還會把傳進來的Handle類型的參數activityThread保存下來,以便后面在分發廣播的時候使用。
現在,再回到ContextImpl.registerReceiverInternal函數,在獲得了IIntentReceiver類型的Binder對象后,就開始要把它注冊到ActivityManagerService中去了。
**Step 5. ActivityManagerProxy.registerReceiver**
這個函數實現在frameworks/base/core/java/android/app/ActivityManagerNative.java文件中:
~~~
class ActivityManagerProxy implements IActivityManager
{
......
public Intent registerReceiver(IApplicationThread caller,
IIntentReceiver receiver,
IntentFilter filter, String perm) throws RemoteException
{
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
data.writeStrongBinder(caller != null ? caller.asBinder() : null);
data.writeStrongBinder(receiver != null ? receiver.asBinder() : null);
filter.writeToParcel(data, 0);
data.writeString(perm);
mRemote.transact(REGISTER_RECEIVER_TRANSACTION, data, reply, 0);
reply.readException();
Intent intent = null;
int haveIntent = reply.readInt();
if (haveIntent != 0) {
intent = Intent.CREATOR.createFromParcel(reply);
}
reply.recycle();
data.recycle();
return intent;
}
......
}
~~~
這個函數通過Binder驅動程序就進入到ActivityManagerService中的registerReceiver函數中去了。
**Step 6. ActivityManagerService.registerReceiver**
這個函數實現在frameworks/base/services/java/com/android/server/am/ActivityManagerService.java文件中:
~~~
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
......
public Intent registerReceiver(IApplicationThread caller,
IIntentReceiver receiver, IntentFilter filter, String permission) {
synchronized(this) {
ProcessRecord callerApp = null;
if (caller != null) {
callerApp = getRecordForAppLocked(caller);
if (callerApp == null) {
......
}
}
List allSticky = null;
// Look for any matching sticky broadcasts...
Iterator actions = filter.actionsIterator();
if (actions != null) {
while (actions.hasNext()) {
String action = (String)actions.next();
allSticky = getStickiesLocked(action, filter, allSticky);
}
} else {
......
}
// The first sticky in the list is returned directly back to
// the client.
Intent sticky = allSticky != null ? (Intent)allSticky.get(0) : null;
......
if (receiver == null) {
return sticky;
}
ReceiverList rl
= (ReceiverList)mRegisteredReceivers.get(receiver.asBinder());
if (rl == null) {
rl = new ReceiverList(this, callerApp,
Binder.getCallingPid(),
Binder.getCallingUid(), receiver);
if (rl.app != null) {
rl.app.receivers.add(rl);
} else {
......
}
mRegisteredReceivers.put(receiver.asBinder(), rl);
}
BroadcastFilter bf = new BroadcastFilter(filter, rl, permission);
rl.add(bf);
......
mReceiverResolver.addFilter(bf);
// Enqueue broadcasts for all existing stickies that match
// this filter.
if (allSticky != null) {
......
}
return sticky;
}
}
......
}
~~~
函數首先是獲得調用registerReceiver函數的應用程序進程記錄塊:
~~~
ProcessRecord callerApp = null;
if (caller != null) {
callerApp = getRecordForAppLocked(caller);
if (callerApp == null) {
......
}
}
~~~
這里得到的便是上一篇文章Android系統中的廣播(Broadcast)機制簡要介紹和學習計劃里面介紹的應用程序Broadcast的進程記錄塊了,MainActivity就是在里面啟動起來的。
~~~
List allSticky = null;
// Look for any matching sticky broadcasts...
Iterator actions = filter.actionsIterator();
if (actions != null) {
while (actions.hasNext()) {
String action = (String)actions.next();
allSticky = getStickiesLocked(action, filter, allSticky);
}
} else {
......
}
// The first sticky in the list is returned directly back to
// the client.
Intent sticky = allSticky != null ? (Intent)allSticky.get(0) : null;
~~~
這里傳進來的filter只有一個action,就是前面描述的CounterService.BROADCAST_COUNTER_ACTION了,這里先通過getStickiesLocked函數查找一下有沒有對應的sticky intent列表存在。什么是Sticky Intent呢?我們在最后一次調用sendStickyBroadcast函數來發送某個Action類型的廣播時,系統會把代表這個廣播的Intent保存下來,這樣,后來調用registerReceiver來注冊相同Action類型的廣播接收器,就會得到這個最后發出的廣播。這就是為什么叫做Sticky Intent了,這個最后發出的廣播雖然被處理完了,但是仍然被粘住在ActivityManagerService中,以便下一個注冊相應Action類型的廣播接收器還能繼承處理。
這里,假設我們不使用sendStickyBroadcast來發送CounterService.BROADCAST_COUNTER_ACTION類型的廣播,于是,這里得到的allSticky和sticky都為null了。
繼續往下看,這里傳進來的receiver不為null,于是,繼續往下執行:
~~~
ReceiverList rl = (ReceiverList)mRegisteredReceivers.get(receiver.asBinder());
if (rl == null) {
rl = new ReceiverList(this, callerApp,
Binder.getCallingPid(),
Binder.getCallingUid(), receiver);
if (rl.app != null) {
rl.app.receivers.add(rl);
} else {
......
}
mRegisteredReceivers.put(receiver.asBinder(), rl);
}
~~~
這里其實就是把廣播接收器receiver保存一個ReceiverList列表中,這個列表的宿主進程是rl.app,這里就是MainActivity所在的進程了,在ActivityManagerService中,用一個進程記錄塊來表示這個應用程序進程,它里面有一個列表receivers,專門用來保存這個進程注冊的廣播接收器。接著,又把這個ReceiverList列表以receiver為Key值保存在ActivityManagerService的成員變量mRegisteredReceivers中,這些都是為了方便在收到廣播時,快速找到對應的廣播接收器的。
再往下看:
~~~
BroadcastFilter bf = new BroadcastFilter(filter, rl, permission);
rl.add(bf);
......
mReceiverResolver.addFilter(bf);
~~~
上面只是把廣播接收器receiver保存起來了,但是還沒有把它和filter關聯起來,這里就創建一個BroadcastFilter來把廣播接收器列表rl和filter關聯起來,然后保存在ActivityManagerService中的成員變量mReceiverResolver中去。
這樣,廣播接收器注冊的過程就介紹完了,比較簡單,但是工作又比較瑣碎,主要就是將廣播接收器receiver及其要接收的廣播類型filter保存在ActivityManagerService中,以便以后能夠接收到相應的廣播并進行處理,在下一篇文章,我們將詳細分析這個過程,敬請關注。
- 前言
- Android組件設計思想
- Android源代碼開發和調試環境搭建
- Android源代碼下載和編譯
- Android源代碼情景分析法
- Android源代碼調試分析法
- 手把手教你為手機編譯ROM
- 在Ubuntu上下載、編譯和安裝Android最新源代碼
- 在Ubuntu上下載、編譯和安裝Android最新內核源代碼(Linux Kernel)
- 如何單獨編譯Android源代碼中的模塊
- 在Ubuntu上為Android系統編寫Linux內核驅動程序
- 在Ubuntu上為Android系統內置C可執行程序測試Linux內核驅動程序
- 在Ubuntu上為Android增加硬件抽象層(HAL)模塊訪問Linux內核驅動程序
- 在Ubuntu為Android硬件抽象層(HAL)模塊編寫JNI方法提供Java訪問硬件服務接口
- 在Ubuntu上為Android系統的Application Frameworks層增加硬件訪問服務
- 在Ubuntu上為Android系統內置Java應用程序測試Application Frameworks層的硬件服務
- Android源代碼倉庫及其管理工具Repo分析
- Android編譯系統簡要介紹和學習計劃
- Android編譯系統環境初始化過程分析
- Android源代碼編譯命令m/mm/mmm/make分析
- Android系統鏡像文件的打包過程分析
- 從CM刷機過程和原理分析Android系統結構
- Android系統架構概述
- Android系統整體架構
- android專用驅動
- Android硬件抽象層HAL
- Android應用程序組件
- Android應用程序框架
- Android用戶界面架構
- Android虛擬機之Dalvik虛擬機
- Android硬件抽象層
- Android硬件抽象層(HAL)概要介紹和學習計劃
- Android專用驅動
- Android Logger驅動系統
- Android日志系統驅動程序Logger源代碼分析
- Android應用程序框架層和系統運行庫層日志系統源代碼分析
- Android日志系統Logcat源代碼簡要分析
- Android Binder驅動系統
- Android進程間通信(IPC)機制Binder簡要介紹和學習計劃
- 淺談Service Manager成為Android進程間通信(IPC)機制Binder守護進程之路
- 淺談Android系統進程間通信(IPC)機制Binder中的Server和Client獲得Service Manager接口之路
- Android系統進程間通信(IPC)機制Binder中的Server啟動過程源代碼分析
- Android系統進程間通信(IPC)機制Binder中的Client獲得Server遠程接口過程源代碼分析
- Android系統進程間通信Binder機制在應用程序框架層的Java接口源代碼分析
- Android Ashmem驅動系統
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)簡要介紹和學習計劃
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)驅動程序源代碼分析
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)在進程間共享的原理分析
- Android系統匿名共享內存(Anonymous Shared Memory)C++調用接口分析
- Android應用程序進程管理
- Android應用程序進程啟動過程的源代碼分析
- Android系統進程Zygote啟動過程的源代碼分析
- Android系統默認Home應用程序(Launcher)的啟動過程源代碼分析
- Android應用程序消息機制
- Android應用程序消息處理機制(Looper、Handler)分析
- Android應用程序線程消息循環模型分析
- Android應用程序輸入事件分發和處理機制
- Android應用程序鍵盤(Keyboard)消息處理機制分析
- Android應用程序UI架構
- Android系統的開機畫面顯示過程分析
- Android幀緩沖區(Frame Buffer)硬件抽象層(HAL)模塊Gralloc的實現原理分析
- SurfaceFlinger
- Android系統Surface機制的SurfaceFlinger服務
- SurfaceFlinger服務簡要介紹和學習計劃
- 啟動過程分析
- 對幀緩沖區(Frame Buffer)的管理分析
- 線程模型分析
- 渲染應用程序UI的過程分析
- Android應用程序與SurfaceFlinger服務的關系
- 概述和學習計劃
- 連接過程分析
- 共享UI元數據(SharedClient)的創建過程分析
- 創建Surface的過程分析
- 渲染Surface的過程分析
- Android應用程序窗口(Activity)
- 實現框架簡要介紹和學習計劃
- 運行上下文環境(Context)的創建過程分析
- 窗口對象(Window)的創建過程分析
- 視圖對象(View)的創建過程分析
- 與WindowManagerService服務的連接過程分析
- 繪圖表面(Surface)的創建過程分析
- 測量(Measure)、布局(Layout)和繪制(Draw)過程分析
- WindowManagerService
- WindowManagerService的簡要介紹和學習計劃
- 計算Activity窗口大小的過程分析
- 對窗口的組織方式分析
- 對輸入法窗口(Input Method Window)的管理分析
- 對壁紙窗口(Wallpaper Window)的管理分析
- 計算窗口Z軸位置的過程分析
- 顯示Activity組件的啟動窗口(Starting Window)的過程分析
- 切換Activity窗口(App Transition)的過程分析
- 顯示窗口動畫的原理分析
- Android控件TextView的實現原理分析
- Android視圖SurfaceView的實現原理分析
- Android應用程序UI硬件加速渲染
- 簡要介紹和學習計劃
- 環境初始化過程分析
- 預加載資源地圖集服務(Asset Atlas Service)分析
- Display List構建過程分析
- Display List渲染過程分析
- 動畫執行過程分析
- Android應用程序資源管理框架
- Android資源管理框架(Asset Manager)
- Asset Manager 簡要介紹和學習計劃
- 編譯和打包過程分析
- Asset Manager的創建過程分析
- 查找過程分析
- Dalvik虛擬機和ART虛擬機
- Dalvik虛擬機
- Dalvik虛擬機簡要介紹和學習計劃
- Dalvik虛擬機的啟動過程分析
- Dalvik虛擬機的運行過程分析
- Dalvik虛擬機JNI方法的注冊過程分析
- Dalvik虛擬機進程和線程的創建過程分析
- Dalvik虛擬機垃圾收集機制簡要介紹和學習計劃
- Dalvik虛擬機Java堆創建過程分析
- Dalvik虛擬機為新創建對象分配內存的過程分析
- Dalvik虛擬機垃圾收集(GC)過程分析
- ART虛擬機
- Android ART運行時無縫替換Dalvik虛擬機的過程分析
- Android運行時ART簡要介紹和學習計劃
- Android運行時ART加載OAT文件的過程分析
- Android運行時ART加載類和方法的過程分析
- Android運行時ART執行類方法的過程分析
- ART運行時垃圾收集機制簡要介紹和學習計劃
- ART運行時Java堆創建過程分析
- ART運行時為新創建對象分配內存的過程分析
- ART運行時垃圾收集(GC)過程分析
- ART運行時Compacting GC簡要介紹和學習計劃
- ART運行時Compacting GC堆創建過程分析
- ART運行時Compacting GC為新創建對象分配內存的過程分析
- ART運行時Semi-Space(SS)和Generational Semi-Space(GSS)GC執行過程分析
- ART運行時Mark-Compact( MC)GC執行過程分析
- ART運行時Foreground GC和Background GC切換過程分析
- Android安全機制
- SEAndroid安全機制簡要介紹和學習計劃
- SEAndroid安全機制框架分析
- SEAndroid安全機制中的文件安全上下文關聯分析
- SEAndroid安全機制中的進程安全上下文關聯分析
- SEAndroid安全機制對Android屬性訪問的保護分析
- SEAndroid安全機制對Binder IPC的保護分析
- 從NDK在非Root手機上的調試原理探討Android的安全機制
- APK防反編譯
- Android視頻硬解穩定性問題探討和處理
- Android系統的智能指針(輕量級指針、強指針和弱指針)的實現原理分析
- Android應用程序安裝過程源代碼分析
- Android應用程序啟動過程源代碼分析
- 四大組件源代碼分析
- Activity
- Android應用程序的Activity啟動過程簡要介紹和學習計劃
- Android應用程序內部啟動Activity過程(startActivity)的源代碼分析
- 解開Android應用程序組件Activity的"singleTask"之謎
- Android應用程序在新的進程中啟動新的Activity的方法和過程分析
- Service
- Android應用程序綁定服務(bindService)的過程源代碼分析
- ContentProvider
- Android應用程序組件Content Provider簡要介紹和學習計劃
- Android應用程序組件Content Provider應用實例
- Android應用程序組件Content Provider的啟動過程源代碼分析
- Android應用程序組件Content Provider在應用程序之間共享數據的原理分析
- Android應用程序組件Content Provider的共享數據更新通知機制分析
- BroadcastReceiver
- Android系統中的廣播(Broadcast)機制簡要介紹和學習計劃
- Android應用程序注冊廣播接收器(registerReceiver)的過程分析
- Android應用程序發送廣播(sendBroadcast)的過程分析
