4.2 分析Java层

在MediaScanner系统中,JNI的调用关系为:

MediaScanner -------------libmedia_jni.so -------------libmedia.so

在Android系统中,MediaScanner的功能是扫描媒体文件,得到诸如歌曲时长、歌曲作者等信息,并将这些信息存放到媒体数据库中,以供其他应用程序使用。

在JNI应用中,Java层MediaScanner的实现文件为:

        /frameworks/base/media/java/android/media/MediaScanner.java

在本节的内容中,将详细讲解MediaScanner系统中Java层的具体实现过程。

4.2.1 加载JNl库

在文件MediaScanner.java中,首先定义类MediaScanner并加载JNI库,然后定义JNI的Native(本地)函数。主要代码如下所示:

        public class MediaScanner
        {
            static {
              System.loadLibrary("media_jni");
              native_init();
            }
            private final static String TAG = "MediaScanner";
            private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
                  Files.FileColumns._ID,//0
                  Files.FileColumns.DATA,//1
                  Files.FileColumns.FORMAT,//2
                  Files.FileColumns.DATE_MODIFIED,//3
            };
            private static final String[] ID_PROJECTION = new String[] {
                  Files.FileColumns._ID,
            };
            private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0;
            private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1;
            private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2;
            private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3;
            private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] {
                  Audio.Playlists.Members.PLAYLIST_ID,//0
            };
            private static final int ID_PLAYLISTS_COLUMN_INDEX = 0;
        private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1;
        private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2;
        private static final String RINGTONES_DIR = "/ringtones/";
        private static final String NOTIFICATIONS_DIR = "/notifications/";
        private static final String ALARMS_DIR = "/alarms/";
        private static final String MUSIC_DIR = "/music/";
        private static final String PODCAST_DIR = "/podcasts/";
        ……
        private static native final void native_init(); //声明一个native函数,native为关键字
        private native final void native_setup();
        ……
    }

函数native_init位于包android.media中,其完整路径名为:

        android.media.MediaScanner.nantive_init

根据规则其对应的JNI层函数名称为:

        android_media_MediaScanner_native_init

在调用函数native之前需要先加载JNI库,一般在类的static中加载调用函数System.loadLibrary()。在加载了相应的JNI库之后,如果要使用相应的native函数,只需使用native声明需要被调用的函数即可:

        private native void processDirectory(String path, String extensions, MediaScannerClient client);
        private native void processFile(String path, String mimeType, MediaScannerClient client);
        public native void setLocale(String locale);

4.2.2 实现扫描工作

在文件MediaScanner.java中,通过函数scanDirectories实现扫描工作,具体实现代码如下所示:

            public void scanDirectories(String[] directories, String volumeName) {
                try {
                    long start = System.currentTimeMillis();
                    initialize(volumeName); //初始化
                    prescan(null, true); //扫描前的预处理
                    long prescan = System.currentTimeMillis();
                    if (ENABLE_BULK_INSERTS) {
                     //create MediaInserter for bulk inserts
                      mMediaInserter = new MediaInserter(mMediaProvider, 500);
                    }
                    //函数processDirectory是一个Native函数,功能是对目标文件夹进行扫描
                    for (int i = 0; i < directories.length; i++) {
                      processDirectory(directories[i], mClient);
                    }
                    if (ENABLE_BULK_INSERTS) {
                     //flush remaining inserts
                      mMediaInserter.flushAll();
                      mMediaInserter = null;
                    }
                    long scan = System.currentTimeMillis();
                    postscan(directories); //扫描后的处理
                    long end = System.currentTimeMillis();
                    if (false) {
                      Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
                      Log.d(TAG, "     scan time: " + (scan - prescan) + "ms\n");
                      Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
                      Log.d(TAG, "   total time: " + (end - start) + "ms\n");
                    }
                } catch (SQLException e) {
                   //this might happen if the SD card is removed while the media scanner is running
                      Log.e(TAG, "SQLException in MediaScanner.scan()", e);
                  } catch (UnsupportedOperationException e) {
                     //this might happen if the SD card is removed while the media scanner is running
                      Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
                  } catch (RemoteException e) {
                      Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
                  }
                }

在上述代码中用到了函数initialize,此函数的功能是实现初始化操作,具体实现代码如下所示:

            private void initialize(String volumeName) {
                //打开MediaProvider,获得它的一个实例
                mMediaProvider = mContext.getContentResolver().acquireProvider("media");
                //得到一些uri
                mAudioUri = Audio.Media.getContentUri(volumeName);
                mVideoUri = Video.Media.getContentUri(volumeName);
                mImagesUri = Images.Media.getContentUri(volumeName);
                mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
                mFilesUri = Files.getContentUri(volumeName);
                //如果需要外部存储的话,则可以支持播放列表,用缓存池实现,例如mGenreCache等
                if (! volumeName.equals("internal")) {
                 //we only support playlists on external media
                  mProcessPlaylists = true;
                  mProcessGenres = true;
                  mPlaylistsUri = Playlists.getContentUri(volumeName);
                  mCaseInsensitivePaths = true;
                }
            }

4.2.3 读取并保存信息

在文件MediaScanner.java中,函数prescan的功能是读取之前扫描的数据库中和文件相关的信息并保存起来。此函数创建了一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。函数prescan的具体实现代码如下所示:

            private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
                Cursor c = null;
                String where = null;
                String[] selectionArgs = null;
               //mPlayLists保存从数据库中获取的信息
                if (mPlayLists == null) {
                  mPlayLists = new ArrayList<FileEntry>();
                } else {
                  mPlayLists.clear();
                }
                if (filePath ! = null) {
                  //只有一个文件查询
                  where = MediaStore.Files.FileColumns._ID + ">? " +
                      " AND " + Files.FileColumns.DATA + "=? ";
                  selectionArgs = new String[] { "", filePath };
                } else {
                  where = MediaStore.Files.FileColumns._ID + ">? ";
                  selectionArgs = new String[] { "" };
                }
                //告诉提供者不删除文件.
                //如果不需要删除文件,则需要避免意外删除这个文件的机制
                //这可能在系统未被安装和未安装在扫描仪之前发生
                Uri.Builder builder = mFilesUri.buildUpon();
                builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
                MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
                //根据内容提供者建立文件列表
                try {
                    if (prescanFiles) {
                        //首先从文件表读到现有文件
                       //因为可能存在删除不存在文件的情况,所以要小批量的实现数据库查询以避免这个问题
                        long lastId = Long.MIN_VALUE;
                        Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
                        mWasEmptyPriorToScan = true;
                        while (true) {
                          selectionArgs[0] = "" + lastId;
                          if (c ! = null) {
                              c.close();
                              c = null;
                          }
                          c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
                                where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
                          if (c == null) {
                              break;
                          }
                          int num = c.getCount();
                          if (num == 0) {
                              break;
                          }
                          mWasEmptyPriorToScan = false;
                          while (c.moveToNext()) {
                              long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
                              String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
                              int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
                              long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
                              lastId = rowId;
                             //Only consider entries with absolute path names.
                             //This allows storing URIs in the database without the
                             //media scanner removing them.
                              if (path ! = null && path.startsWith("/")) {
                                boolean exists = false;
                                try {
                                    exists = Libcore.os.access(path, libcore.io.OsConstants.F_OK);
                                } catch (ErrnoException e1) {
                                }
                                if (! exists && ! MtpConstants.isAbstractObject(format)) {
                                   //do not delete missing playlists, since they may have been
                                   //modified by the user.
                                   //The user can delete them in the media player instead.
                                   //instead, clear the path and lastModified fields in the row
                                    MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
                                    int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
                                    if (! MediaFile.isPlayListFileType(fileType)) {
                                      deleter.delete(rowId);
                                      if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                                          deleter.flush();
                                          String parent = new File(path).getParent();
                                          mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
                                      }
                                    }
                                }
                              }
                          }
                        }
                    }
                }
                finally {
                    if (c ! = null) {
                        c.close();
                    }
                    deleter.flush();
                }
                //计算图像的原始尺寸
                mOriginalCount = 0;
                c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
                if (c ! = null) {
                    mOriginalCount = c.getCount();
                    c.close();
                }
            }

4.2.4 删除SD卡外的信息

在文件MediaScanner.java中,函数postscan的功能是删除不存在于SD卡中的文件信息。函数postscan的具体实现代码如下所示:

            private void postscan(String[] directories) throws RemoteException {
                //触发播放列表后能够知道对应存储的媒体文件
                if (mProcessPlaylists) {
                  processPlayLists();
                }
                if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
                  pruneDeadThumbnailFiles();
                //允许GC清理
                mPlayLists = null;
                mMediaProvider = null;
            }

4.2.5 直接转向JNl

在文件MediaScanner.java中,processDirectory是一个本地方法,能够直接转向JNI。具体实现代码如下所示:

        static void android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path,
        jstring extensions, jobject client)
        {   //获取MediaScanner
            MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
            //参数判断,并抛出异常
            if (path == NULL) {
                jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
                return;
            }
            if (extensions == NULL) {
                jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
                return;
            }
            const char *pathStr = env->GetStringUTFChars(path, NULL);
            if (pathStr == NULL) { //Out of memory
                jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
                return;
            }
            const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
            if (extensionsStr == NULL) { //Out of memory
                env->ReleaseStringUTFChars(path, pathStr);
                jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
                return;
            }
            //初始化client实例
            MyMediaScannerClient myClient(env, client);
            //mp调用processDirectory
            mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
            //gc
            env->ReleaseStringUTFChars(path, pathStr);
            env->ReleaseStringUTFChars(extensions, extensionsStr);
        }

4.2.6 扫描函数scanFile

在此讲解Java层的函数scanFile,功能是调用函数doScanFile对指定的文件进行扫描,具体实现代码如下所示:

                  public void scanFile(String path, long lastModified, long fileSize,
                        boolean isDirectory, boolean noMedia) {
                      //这是来自本地代码的回调函数
                     //Log.v(TAG, "scanFile: "+path);
                      doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
                  }

4.2.7 JNl中的异常处理

为了处理Java实现的方法中或者C/C++实现的方法中抛出的Java异常,JNI也提供了一套异常处理机制函数集,专门用于检查、分析和处理异常情况。例如在文件jni.h中,定义了主要的异常函数,具体代码如下所示:

        //抛出异常
        jint          (*Throw)(JNIEnv*,  jthrowable);
        //抛出新的异常
        jint          (*ThrowNew)(JNIEnv  *,  jclass,  const  char  *);
        //异常产生
        jthrowable   (*ExceptionOccurred)(JNIEnv*);
        void          (*ExceptionDescribe)(JNIEnv*);
        //清除异常
        void          (*ExceptionClear)(JNIEnv*);
        void          (*FatalError)(JNIEnv*,  const  char*);

例如在Camera模块中也用到了异常处理,在文件android_hardware_Camera.cpp中,也涉及到了异常操作,具体代码例如下所示:

        void  JNICameraContext::copyAndPost(JNIEnv*  env,  const  sp<IMemory>&  dataPtr,  int
         msgType)
        {
          ……
          if  (obj  ==  NULL)  {
                      LOGE("Couldn't  allocate  byte  array  for  JPEG  data");
                      env->ExceptionClear();
                  }  else  {
                      env->SetByteArrayRegion(obj,  0,  size,  data);
                  }
              }  else  {
                  LOGE("image  heap  is  NULL");
              }
            }
            ……
        }

在文件android_hardware_Camera.cpp中,函数android_hardware_Camera_startPreview()也同样用到了异常处理机制,具体代码如下所示:

        static void android_hardware_Camera_startPreview(JNIEnv *env, jobject thiz)
        {
            LOGV("startPreview");
            sp<Camera> camera = get_native_camera(env, thiz, NULL);
            if  (camera  ==  0)  return;
            if (camera->startPreview() ! = NO_ERROR) {
              jniThrowRuntimeException(env,  "startPreview  failed");
              return;
            }
        }

在上述代码中,android_hardware_Camera_startPreview()如果发现startPreview()函数返回错误,则会抛出异常并返回。这里的异常与Java中的异常机制很相似,读者可以对比分析它们的原理。