这一节将分析一个实际案例,即插入一张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来总结一下这个处理过程:
:-: ![](http://img.blog.csdn.net/20150802164428372?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
图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 本章小结