這一節將分析一個實際案例,即插入一張SD卡引發的事件及其處理過程。在分析之前,還是應先介紹MountService。
1. MountService的介紹
有些應用程序需要檢測外部存儲卡的插入/拔出事件,這些事件是由MountService通過Intent廣播發出的,例如外部存儲卡插入后,MountService就會發送ACTION_MEDIA_MOUNTED消息。從某種意義上說,可把MountService看成是Java世界的Vold。來簡單認識一下這個MountService,它的代碼如下所示:
**MountService.java**
~~~
class MountService extends IMountService.Stub
implements INativeDaemonConnectorCallbacks {
//MountService實現了INativeDaemonConnectorCallbacks接口
......
public MountService(Context context) {
mContext = context;
......
//創建一個HandlerThread,在第5章中曾介紹過。
mHandlerThread = new HandlerThread("MountService");
mHandlerThread.start();
/*
創建一個Handler,這個Handler使用HandlerThread的Looper,也就是說,派發給該Handler
的消息將在另外一個線程中處理。可回顧第5章的內容,以加深印象。
*/
mHandler = new MountServiceHandler(mHandlerThread.getLooper());
......
/*
NativeDaemonConnector用于Socket通信,第二個參數“vold”表示將和Vold通信,也就是
和CL模塊中的那個socket建立通信連接。第一個參數為INativeDaemonConnectorCallbacks
接口。它提供兩個回調函數:
onDaemonConnected:當NativeDaemonConnector連接上Vold后回調。
onEvent:當NativeDaemonConnector收到來自Vold的數據后回調。
*/
mConnector = new NativeDaemonConnector(this, "vold",
10, "VoldConnector");
mReady= false;
//再啟動一個線程用于和Vold通信。
Threadthread = new Thread(mConnector,
NativeDaemonConnector.class.getName());
thread.start();
}
......
}
~~~
MountService通過NativeDaemonConnector和Vold的CL模塊建立通信連接,這部分內容比較簡單,讀者可自行研究。下面來分析SD卡插入后所引發的一連串處理。
2. 設備插入事件的處理
(1)Vold處理Uevent事件
在插入SD卡后,Vold的NM模塊接收到Uevent消息,假設此消息的內容是前面介紹Uevent知識時使用的add消息,它的內容如下所示:
**SD卡插入的Uevent消息**
~~~
add@/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
ACTION=add //add表示設備插入動作,另外還有remove和change等動作
//DEVPATH表示該設備位于/sys目錄中的設備路徑
DEVPATH=/devices/platform/msm_sdcc.2/mmc_host/mmc1/mmc1:c9f2/block/mmcblk0
/*
SUBSYSTEM表示該設備屬于哪一類設備,block為塊設備,磁盤也屬于這一類設備,另外還有
character(字符)設備等類型。
*/
SUBSYSTEM=block
MAJOR=179//MAJOR和MINOR分別表示該設備的主次設備號,二者聯合起來可以標識一個設備
MINOR=0
DEVNAME=mmcblk0
DEVTYPE=disk//設備Type為disk
NPARTS=3 //表示該SD卡上的分區,我的SD卡上有三塊分區
SEQNUM=1357//序號
~~~
根據前文分析可知,NM模塊中的NetlinkHandler會處理此消息,請回顧一下相關代碼:
**NetlinkHandler.cpp**
~~~
void NetlinkHandler::onEvent(NetlinkEvent *evt){
VolumeManager *vm = VolumeManager::Instance();
constchar *subsys = evt->getSubsystem();
......
//根據上面Uevent消息的內容可知,它的subsystem對應為block,所以我們會走下面這個if分支
if (!strcmp(subsys, "block")) {
vm->handleBlockEvent(evt); //調用VM的handleBlockEvent
}
......
}
~~~
**VolumeManager.cpp**
~~~
voidVolumeManager::handleBlockEvent(NetlinkEvent *evt) {
constchar *devpath = evt->findParam("DEVPATH");
VolumeCollection::iterator it;
boolhit = false;
for(it = mVolumes->begin(); it != mVolumes->end(); ++it) {
//調用各個Volume的handleBlockEvent
if(!(*it)->handleBlockEvent(evt)) {
hit = true;
break;
}
}
......
}
~~~
我的G7手機只有一個Volume,其實際類型就是之前介紹過的DirectVolume。請看它是怎么對待這個Uevent消息的,代碼如下所示:
**DirectVolume.cpp**
~~~
int DirectVolume::handleBlockEvent(NetlinkEvent*evt) {
constchar *dp = evt->findParam("DEVPATH");
PathCollection::iterator it;
for(it = mPaths->begin(); it != mPaths->end(); ++it) {
if(!strncmp(dp, *it, strlen(*it))) {
int action = evt->getAction();
const char *devtype =evt->findParam("DEVTYPE");
if (action == NetlinkEvent::NlActionAdd) {
int major = atoi(evt->findParam("MAJOR"));
int minor = atoi(evt->findParam("MINOR"));
char nodepath[255];
snprintf(nodepath,
sizeof(nodepath),"/dev/block/vold/%d:%d",
major, minor);
//內部調用mknod函數創建設備節點
if (createDeviceNode(nodepath, major, minor)) {
SLOGE("Error makingdevice node '%s' (%s)", nodepath,
strerror(errno));
}
if (!strcmp(devtype, "disk")) {
//對應Uevent消息的DEVTYPE值為disk,所以走這個分支
handleDiskAdded(dp, evt);
} else {
//處理DEVTYPE為Partition的情況
handlePartitionAdded(dp,evt);
}
} else if (action == NetlinkEvent::NlActionRemove) {
//對應Uevent的ACTION值為remove
......
} else if (action == NetlinkEvent::NlActionChange) {
//對應Uevent的ACTION值為change
......
}
......
return 0;
}
}
errno= ENODEV;
return-1;
}
~~~
插入SD卡,首先收到的Uevent消息中DEVTYPE的值是“disk”,這將導致DirectVolume的handleDiskInserted被調用。下面來看它的工作。
**DirectVolume.cpp**
~~~
void DirectVolume::handleDiskAdded(const char*devpath, NetlinkEvent *evt) {
mDiskMajor = atoi(evt->findParam("MAJOR"));
mDiskMinor = atoi(evt->findParam("MINOR"));
constchar *tmp = evt->findParam("NPARTS");
if(tmp) {
mDiskNumParts = atoi(tmp);//這個disk上的分區個數。
} else{
......
mDiskNumParts = 1;
}
charmsg[255];
intpartmask = 0;
inti;
/*
Partmask會記錄這個Disk上分區加載的情況,前面曾介紹過,如果一個Disk有多個分區,
它后續則會收到多個分區的Uevent消息。
*/
for (i= 1; i <= mDiskNumParts; i++) {
partmask |= (1 << i);
}
mPendingPartMap = partmask;
if(mDiskNumParts == 0) {
......//如果沒有分區,則設置Volume的狀態為Idle。
setState(Volume::State_Idle);
}else {
......//如果還有分區未加載,則設置Volume狀態為Pending
setState(Volume::State_Pending);
}
/*
設置通知內容,snprintf調用完畢后msg的值為:
“Volume sdcard/mnt/sdcard disk inserted (179:0)”
*/
snprintf(msg, sizeof(msg), "Volume %s %s disk inserted(%d:%d)",
getLabel(), getMountpoint(), mDiskMajor, mDiskMinor);
/*
getBroadcaster函數返回setBroadcaster函數設置的那個Broadcaster,也就是CL對象。
然后調用CL對象的sendBroadcast給MountService發送消息,注意它的第一個參數是ResponseCode::VolumeDiskInserted。
*/
mVm->getBroadcaster()->sendBroadcast(ResponseCode::VolumeDiskInserted,
msg, false);
}
~~~
handleDiskAdded把Uevent消息做一些轉換后發送給了MountService,實際上可認為CL模塊和MountService通信使用的是另外一套協議。那么,MountService會做什么處理呢?
(2)MountService的處理
**MountService.javaonEvent函數**
~~~
publicboolean onEvent(int code, String raw, String[] cooked) {
Intent in = null;
//關于onEvent函數,MountService的介紹中曾提過,當NativeDaemonConnector收到
//來自vold的數據后都會調用這個onEvent函數。
......
if(code == VoldResponseCode.VolumeStateChange) {
......
}else if (code == VoldResponseCode.ShareAvailabilityChange) {
......
}else if ((code == VoldResponseCode.VolumeDiskInserted) ||
(code ==VoldResponseCode.VolumeDiskRemoved) ||
(code ==VoldResponseCode.VolumeBadRemoval)) {
final String label = cooked[2]; //label值為”sdcard”
final String path = cooked[3]; //path值為”/mnt/sdcard”
int major = -1;
int minor = -1;
try {
String devComp = cooked[6].substring(1, cooked[6].length() -1);
String[] devTok = devComp.split(":");
major = Integer.parseInt(devTok[0]);
minor = Integer.parseInt(devTok[1]);
} catch (Exception ex) {
......
}
if (code == VoldResponseCode.VolumeDiskInserted) {
//收到handleDiskAdded發送的VolumeDiskInserted消息了
//然后單獨啟動一個線程來處理這個消息。
new Thread() {
public void run() {
try {
int rc;
//調用doMountVolume處理。
if ((rc =doMountVolume(path)) !=
StorageResultCode.OperationSucceeded) {
}
} catch (Exception ex){
......
}
}
}.start();
}
~~~
doMountVolume函數的代碼如下所示:
**MountService.java**
~~~
private int doMountVolume(String path) {
int rc = StorageResultCode.OperationSucceeded;
try {
//通過NativeDaemonConnector給Vold發送請求,請求內容為:
//volume mount /mnt/sdcard
mConnector.doCommand(String.format("volume mount %s", path));
}catch (NativeDaemonConnectorException e) {
......//異常處理
}
~~~
走了一大圈,最后又回到Vold了。CL模塊將收到這個來自MountService的請求,請求的內容為字符串“volume mount/mnt/sdcard”,其中的volume表示命令的名字,CL會根據這個名字找到VolumeCmd對象,并交給它處理這個命令。
(3)Vold處理MountService的命令
Vold處理MountService命令的代碼如下所示:
**CommandListener.cppVolumeCmd類**
~~~
intCommandListener::VolumeCmd::runCommand(SocketClient *cli,
int argc, char **argv) {
......
VolumeManager *vm = VolumeManager::Instance();
int rc= 0;
if(!strcmp(argv[1], "list")) {
return vm->listVolumes(cli);
} elseif (!strcmp(argv[1], "debug")) {
......
} elseif (!strcmp(argv[1], "mount")) {
......//調用VM模塊的mountVolume來處理mount命令,參數是“/mnt/sdcard”
rc= vm->mountVolume(argv[2]);
} elseif (!strcmp(argv[1], "unmount")) {
......
rc= vm->unmountVolume(argv[2], force);
} elseif (!strcmp(argv[1], "format")) {
......
rc = vm->formatVolume(argv[2]);
} elseif (!strcmp(argv[1], "share")) {
......
rc= vm->shareVolume(argv[2], argv[3]);
} elseif (!strcmp(argv[1], "unshare")) {
......
rc= vm->unshareVolume(argv[2], argv[3]);
}
......
if(!rc) {
//發送處理結果給MountService
cli->sendMsg(ResponseCode::CommandOkay, "volume operationsucceeded", false);
}
......
return0;
}
~~~
看mountVolume函數:
**VolumeManager.cpp**
~~~
int VolumeManager::mountVolume(const char*label) {
/*
根據label找到對應的Volume。label這個參數的名字含義上有些歧義,根據loopupVolume
的實現來看,它其實比較的是Volume的掛載路徑,也就是vold.fstab中指定的那個
/mnt/sdcard。而vold.fstab中指定的label叫sdcard。
*/
Volume*v = lookupVolume(label);
......
returnv->mountVol();//mountVol由Volume類實現。
}
~~~
找到對應的DirectVolume后,也就找到了代表真實存儲卡的對象。它是如何處理這個命令的呢?代碼如下所示:
**Volume.cpp**
~~~
int Volume::mountVol() {
dev_tdeviceNodes[4];
int n,i, rc = 0;
charerrmsg[255];
......
//getMountpoint返回掛載路徑,即/mnt/sdcard
//isMountpointMounted判斷這個路徑是不是已經被mount了
if(isMountpointMounted(getMountpoint())) {
setState(Volume::State_Mounted);//設置狀態為State_Mounted
return 0;//如果已經被mount了,則直接返回
}
n =getDeviceNodes((dev_t *) &deviceNodes, 4);
......
for (i= 0; i < n; i++) {
char devicePath[255];
sprintf(devicePath, "/dev/block/vold/%d:%d",MAJOR(deviceNodes[i]),
MINOR(deviceNodes[i]));
......
errno = 0;
setState(Volume::State_Checking);
//默認SD卡為FAT分區,只有這樣,當加載為磁盤的時候才能被Windows識別。
if(Fat::check(devicePath)) {
......
return -1;
}
/*
先把設備mount到/mnt/secure/staging,這樣/mnt/secure/staging下的內容
就是該設備的存儲內容了
*/
errno = 0;
if(Fat::doMount(devicePath, "/mnt/secure/staging", false, false, 1000,
1015, 0702,true)) {
......
continue;
}
/*
下面這個函數會把存儲卡中的autorun.inf文件找出來并刪掉,這個文件就是“臭名昭著”的
自動運行文件,在Windows系統上,把SD卡掛載為磁盤后,雙擊這個磁盤就會自動運行
這個文件,很多病毒和木馬都是通過它傳播的。為了安全起見,要把這個文件刪掉。
*/
protectFromAutorunStupidity();
//①下面這個函數比較有意思,需要看看:
if(createBindMounts()) {
......
return -1;
}
//將存儲卡mount路徑從/mnt/secure/staging移到/mnt/sdcard
if(doMoveMount("/mnt/secure/staging", getMountpoint(), false)) {
......
return -1;
}
//②設置狀態為State_Mounted,這個函數將發送狀態信息給MountService
setState(Volume::State_Mounted);
mCurrentlyMountedKdev = deviceNodes[i];
return 0;
}
......
setState(Volume::State_Idle);
return-1;
}
~~~
上面代碼中有個比較有意思的函數,就是createBindMounts,其代碼如下所示:
**Volume.cpp**
~~~
int Volume::createBindMounts() {
unsigned long flags;
/*‘
將/mnt/secure/staging/android_secure目錄名改成
/mnt/secure/staging/.android_secure,SEC_STG_SECIMGDIR的值就是
/mnt/secure/staging/.android_secure,也就是把它變成Linux平臺上的隱藏目錄
*/
if(!access("/mnt/secure/staging/android_secure", R_OK | X_OK)&&
access(SEC_STG_SECIMGDIR, R_OK | X_OK)) {
if(rename("/mnt/secure/staging/android_secure", SEC_STG_SECIMGDIR)) {
SLOGE("Failed to rename legacy asec dir (%s)",strerror(errno));
}
}
......
/*
使用mount命令的bind選項,可將/mnt/secure/staging/.android_secure這個目錄
掛載到/mnt/secure/asec目錄下。/mnt/secure/asec目錄是一個secure container,
目前主要用來保存一些安裝在SD卡上的APP信息。APP2SD是Android 2.2引入的新機制,它
支持將APP安裝在SD卡上,這樣可以節約內部的存儲空間。
mount的bind選項允許將文件系統的一個目錄掛載到另外一個目錄下。讀者可以通過man mount
查詢具體信息。
*/
if(mount(SEC_STG_SECIMGDIR, SEC_ASECDIR, "", MS_BIND, NULL)) {
......
return -1;
}
......
/*
將tempfs設備掛載到/mnt/secure/staging/.android_secure目錄,這樣之前
.android_secure目錄中的內容就只能通過/mnt/secure/asec訪問了。由于那個目錄只能
由root訪問,所以可以起到安全和保護的作用。
*/
if(mount("tmpfs", SEC_STG_SECIMGDIR, "tmpfs", MS_RDONLY,"size=0,mode=000,uid=0,gid=0")){
......
umount("/mnt/asec_secure");
return -1;
}
return0;
}
~~~
createBindMounts的作用就是將存儲卡上的.android_secure目錄掛載到/mnt/secure/asec目錄下,同時對.android_secure進行一些特殊處理,這樣,沒有權限的用戶就不能更改或破壞.android_secure目錄中的內容了,因此它起到了一定的保護作用。
在手機上,受保護的目錄內容,只能在用adb shell登錄后,進入/mnt/secure/asec目錄來查看。注意,這個asec目錄的內容就是.android_secure未掛載tmpfs時的內容(亦即它保存著那些安裝在存儲卡上的應用程序的信息)。另外,可把SD卡拔出來,通過讀卡器直接插到臺式機上,此時,這些信息就能在.android_secure目錄中被直接看到了。
(4)MountService處理狀態通知
volume的mountVol完成相關工作后,就通過下面的函數,發送信息給MountService:
~~~
setState(Volume::State_Mounted); //感興趣的讀者可自行分析此函數的實現。
~~~
MountService依然會在onEvent函數中收到這個消息。
**MountService.java**
~~~
public boolean onEvent(int code, String raw,String[] cooked) {
Intent in = null;
......
if(code == VoldResponseCode.VolumeStateChange) {
/*
狀態變化由notifyVolumeStateChange函數處理,由于Volume的狀態
被置成Mounted,所以下面這個notifyVolumeStateChange會發送
ACTION_MEDIA_MOUNTED這個廣播。我們就不再分析這個函數了,讀者
可自行研究。
*/
notifyVolumeStateChange(
cooked[2], cooked[3],Integer.parseInt(cooked[7]),
Integer.parseInt(cooked[10]));
}
~~~
實例分析就到這里。中間略去了一些處理內容,例如對分區的處理等,讀者可自行研讀,相信已沒有太大難度了。另外,在上述處理過程中,稍微難懂的是mountVol這個函數在掛載方面的處理過程。用圖9-6來總結一下這個處理過程:
:-: 
圖9-6 SD卡插入事件處理流程圖
由上圖可知,Vold在安全性上還是做了一定考慮的。如果沒有特殊需要,讀者了解上面這些知識也就夠了。
- 前言
- 第1章 閱讀前的準備工作
- 1.1 系統架構
- 1.1.1 Android系統架構
- 1.1.2 本書的架構
- 1.2 搭建開發環境
- 1.2.1 下載源碼
- 1.2.2 編譯源碼
- 1.3 工具介紹
- 1.3.1 Source Insight介紹
- 1.3.2 Busybox的使用
- 1.4 本章小結
- 第2章 深入理解JNI
- 2.1 JNI概述
- 2.2 學習JNI的實例:MediaScanner
- 2.3 Java層的MediaScanner分析
- 2.3.1 加載JNI庫
- 2.3.2 Java的native函數和總結
- 2.4 JNI層MediaScanner的分析
- 2.4.1 注冊JNI函數
- 2.4.2 數據類型轉換
- 2.4.3 JNIEnv介紹
- 2.4.4 通過JNIEnv操作jobject
- 2.4.5 jstring介紹
- 2.4.6 JNI類型簽名介紹
- 2.4.7 垃圾回收
- 2.4.8 JNI中的異常處理
- 2.5 本章小結
- 第3章 深入理解init
- 3.1 概述
- 3.2 init分析
- 3.2.1 解析配置文件
- 3.2.2 解析service
- 3.2.3 init控制service
- 3.2.4 屬性服務
- 3.3 本章小結
- 第4章 深入理解zygote
- 4.1 概述
- 4.2 zygote分析
- 4.2.1 AppRuntime分析
- 4.2.2 Welcome to Java World
- 4.2.3 關于zygote的總結
- 4.3 SystemServer分析
- 4.3.1 SystemServer的誕生
- 4.3.2 SystemServer的重要使命
- 4.3.3 關于 SystemServer的總結
- 4.4 zygote的分裂
- 4.4.1 ActivityManagerService發送請求
- 4.4.2 有求必應之響應請求
- 4.4.3 關于zygote分裂的總結
- 4.5 拓展思考
- 4.5.1 虛擬機heapsize的限制
- 4.5.2 開機速度優化
- 4.5.3 Watchdog分析
- 4.6 本章小結
- 第5章 深入理解常見類
- 5.1 概述
- 5.2 以“三板斧”揭秘RefBase、sp和wp
- 5.2.1 第一板斧--初識影子對象
- 5.2.2 第二板斧--由弱生強
- 5.2.3 第三板斧--破解生死魔咒
- 5.2.4 輕量級的引用計數控制類LightRefBase
- 5.2.5 題外話-三板斧的來歷
- 5.3 Thread類及常用同步類分析
- 5.3.1 一個變量引發的思考
- 5.3.2 常用同步類
- 5.4 Looper和Handler類分析
- 5.4.1 Looper類分析
- 5.4.2 Handler分析
- 5.4.3 Looper和Handler的同步關系
- 5.4.4 HandlerThread介紹
- 5.5 本章小結
- 第6章 深入理解Binder
- 6.1 概述
- 6.2 庖丁解MediaServer
- 6.2.1 MediaServer的入口函數
- 6.2.2 獨一無二的ProcessState
- 6.2.3 時空穿越魔術-defaultServiceManager
- 6.2.4 注冊MediaPlayerService
- 6.2.5 秋風掃落葉-StartThread Pool和join Thread Pool分析
- 6.2.6 你徹底明白了嗎
- 6.3 服務總管ServiceManager
- 6.3.1 ServiceManager的原理
- 6.3.2 服務的注冊
- 6.3.3 ServiceManager存在的意義
- 6.4 MediaPlayerService和它的Client
- 6.4.1 查詢ServiceManager
- 6.4.2 子承父業
- 6.5 拓展思考
- 6.5.1 Binder和線程的關系
- 6.5.2 有人情味的訃告
- 6.5.3 匿名Service
- 6.6 學以致用
- 6.6.1 純Native的Service
- 6.6.2 扶得起的“阿斗”(aidl)
- 6.7 本章小結
- 第7章 深入理解Audio系統
- 7.1 概述
- 7.2 AudioTrack的破解
- 7.2.1 用例介紹
- 7.2.2 AudioTrack(Java空間)分析
- 7.2.3 AudioTrack(Native空間)分析
- 7.2.4 關于AudioTrack的總結
- 7.3 AudioFlinger的破解
- 7.3.1 AudioFlinger的誕生
- 7.3.2 通過流程分析AudioFlinger
- 7.3.3 audio_track_cblk_t分析
- 7.3.4 關于AudioFlinger的總結
- 7.4 AudioPolicyService的破解
- 7.4.1 AudioPolicyService的創建
- 7.4.2 重回AudioTrack
- 7.4.3 聲音路由切換實例分析
- 7.4.4 關于AudioPolicy的總結
- 7.5 拓展思考
- 7.5.1 DuplicatingThread破解
- 7.5.2 題外話
- 7.6 本章小結
- 第8章 深入理解Surface系統
- 8.1 概述
- 8.2 一個Activity的顯示
- 8.2.1 Activity的創建
- 8.2.2 Activity的UI繪制
- 8.2.3 關于Activity的總結
- 8.3 初識Surface
- 8.3.1 和Surface有關的流程總結
- 8.3.2 Surface之乾坤大挪移
- 8.3.3 乾坤大挪移的JNI層分析
- 8.3.4 Surface和畫圖
- 8.3.5 初識Surface小結
- 8.4 深入分析Surface
- 8.4.1 與Surface相關的基礎知識介紹
- 8.4.2 SurfaceComposerClient分析
- 8.4.3 SurfaceControl分析
- 8.4.4 writeToParcel和Surface對象的創建
- 8.4.5 lockCanvas和unlockCanvasAndPost分析
- 8.4.6 GraphicBuffer介紹
- 8.4.7 深入分析Surface的總結
- 8.5 SurfaceFlinger分析
- 8.5.1 SurfaceFlinger的誕生
- 8.5.2 SF工作線程分析
- 8.5.3 Transaction分析
- 8.5.4 關于SurfaceFlinger的總結
- 8.6 拓展思考
- 8.6.1 Surface系統的CB對象分析
- 8.6.2 ViewRoot的你問我答
- 8.6.3 LayerBuffer分析
- 8.7 本章小結
- 第9章 深入理解Vold和Rild
- 9.1 概述
- 9.2 Vold的原理與機制分析
- 9.2.1 Netlink和Uevent介紹
- 9.2.2 初識Vold
- 9.2.3 NetlinkManager模塊分析
- 9.2.4 VolumeManager模塊分析
- 9.2.5 CommandListener模塊分析
- 9.2.6 Vold實例分析
- 9.2.7 關于Vold的總結
- 9.3 Rild的原理與機制分析
- 9.3.1 初識Rild
- 9.3.2 RIL_startEventLoop分析
- 9.3.3 RIL_Init分析
- 9.3.4 RIL_register分析
- 9.3.5 關于Rild main函數的總結
- 9.3.6 Rild實例分析
- 9.3.7 關于Rild的總結
- 9.4 拓展思考
- 9.4.1 嵌入式系統的存儲知識介紹
- 9.4.2 Rild和Phone的改進探討
- 9.5 本章小結
- 第10章 深入理解MediaScanner
- 10.1 概述
- 10.2 android.process.media分析
- 10.2.1 MSR模塊分析
- 10.2.2 MSS模塊分析
- 10.2.3 android.process.media媒體掃描工作的流程總結
- 10.3 MediaScanner分析
- 10.3.1 Java層分析
- 10.3.2 JNI層分析
- 10.3.3 PVMediaScanner分析
- 10.3.4 關于MediaScanner的總結
- 10.4 拓展思考
- 10.4.1 MediaScannerConnection介紹
- 10.4.2 我問你答
- 10.5 本章小結