事件起因:
我们的测试报出一个问题,插着U盘开机(我们的机顶盒),多媒体文件未扫描到(U盘里有视频文件还有音乐文件)。说本地的视频文件列表还有音乐列表也是空的。
我是做应用的,兼顾Framework仓库的维护。负责媒体扫描的是MediaProvider.apk。。。义不容辞,这个问题当然是我来负责解决。
问题分析:
1.首先,我显示查看MediaProvider.apk所对应的源码实现。其源码实现很简单,下面我做一下简单的说明。
主要的两个类如下:MediaScannerReceiver.java和MediaScannerService.java。
MediaScannerReceiver.java源码
private final static String TAG = "MediaScannerReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "onReceive,action:" + action);
Uri uri = intent.getData();
if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
// 收到开机广播,扫描内部存储
scan(context, MediaProvider.INTERNAL_VOLUME);
} else {
if (uri.getScheme().equals("file")) {
// 处理扫描外设的Intent
String path = uri.getPath();
String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
Log.d(TAG, "action: " + action + " path: " + path);
if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
// 扫描已挂载的外设
scan(context, MediaProvider.EXTERNAL_VOLUME);
} else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) &&
path != null && path.startsWith(externalStoragePath + "/")) {
scanFile(context, path);
}
}
}
}
//扫描指定的Volume
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
//扫描指定的Path
private void scanFile(Context context, String path) {
Bundle args = new Bundle();
args.putString("filepath", path);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
从这段代码就可以知道,在收到开机广播和Intent.ACTION_MEDIA_MOUNTED广播都会进行文件的扫描。外设的扫描是接收到Intent.ACTION_MEDIA_MOUNTED广播时进行。
2.从开机抓的log分析,确实是没有收到Intent.ACTION_MEDIA_MOUNTED广播,但是在终端使用df命令可以看到U盘已经正常挂载了。
3.外设状态的广播发送都是的MountService里面进行的。所以接下来我们来看一下开机时MountService都做了些什么。
MountService.java的启动是在SystemServer.jar中进行的,ServiceManager.addService("mount",new MountService(context))。这样就是MountService.java的构造函数就会被回调。
MountService.java的构造函数中跟今天研究相关的关键的两个处理就是
1)mContext.registerReceiver(mBroadcastReceiver, filter, null, null);
2)mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG);
Thread thread = new Thread(mConnector, VOLD_TAG);
thread.start();
操作1)中的广播接收器接收的是Intent.ACTION_BOOT_COMPLETED(开机完成广播)。接收到开机广播后,最要的处理就是针对mVolumeStates中保存的外设路径做外设的挂载动作,挂载中的各种状态通知是在onEvent(int code, String raw, String[] cooked)回调中进行的。onEvent中调用的notifyVolumeStateChange方法会发送外设状态广播通知。所以现在最重要的是mVolumeStates中的路径是何时保存进去的?这就涉及到操作2)中的实现。
操作2)中的NativeDaemonConnector 是一个implements Runnable类, 当 thread.start()线程启动时,NativeDaemonConnector.java的run()方法就会执行。run()方法中关键的实现是了NativeDaemonConnector.java类中内部方法listenToSocket(),listenToSocket()方法中重要的实现是mCallbacks.onDaemonConnected(),onDaemonConnected()方法调用的是 MountService.java类中的onDaemonConnected()方法。onDaemonConnected()方法中重要的实现是:String[] vols = mConnector.doListCommand("volume list", VoldResponseCode.VolumeListResult); 其功能就是从底层获取当前的外设列表。然后通过调用updatePublicVolumeState(path, state);方法更新mVolumeStates。这样就解决了操作1)中的疑惑。
4.通过对步骤3的了解,最终定位到在notifyVolumeStateChange方法里调用getVolumeState(String mountPoint)时throw new IllegalArgumentException(),这样notifyVolumeStateChange方法里的外设状态广播通知没有进行。
5.为什么步骤 4中抛出异常呢?是因为getVolumeState(String mountPoint)里通过mVolumeStates.get(mountPoint);没有获得对应挂载点的状态。即该挂载点不再步骤3中的2)里从底层获取当前的外设列表里。
6.外设列表是从底层查询的(vold给的),onEvent里面的挂载点也是vold给的。那就是vold提供的这两个不一致。。。不一致我就找对应的负责人沟通了。。。他给了一句“臣妾做不到一致”,讲了一大堆,大概意思就是说有的U盘带分区,有的不带,他那边处理的要兼容的话就做不到那一点。
7.那怎么处理呢???既然U盘能正常挂载,就是没有通知广播,没有通知广播的原因也是抛出异常引起,那就不抛异常了。。。这样可以解决问题,但是不足之处在于别的地方通过MountService.java的getVolumeState(String mountPoint)查询挂载点状态时,传入真正的挂载点有可能获取不到对应的状态。这也是没有办法的办法了。。。