Android 深入了解相册内部 二

      通过上篇博客我们知道了是系统对外暴露出来的ContentProvider来获取数据库中的图片信息的,使我们知道了如何去实现一个简单的相册了,而不是仅仅去跳转到系统中去做处理了,这么方便的操作极大的满足了我们平常的开发的一些特殊的需求。但是我们在实现完成功能之后我们更多的是要去了解其内部的原理以及是如何运行操作的这样子才能更好的有助于我们水平的提高,同时阅读别人优秀的代码也是对自己的一种提高。

    我们主要是使用系统uri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI)来获取系统图片信息


public static final String AUTHORITY = "media";

private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";

/**
 * The content:// style URI for the "primary" external storage
 * volume.
 */
public static final Uri EXTERNAL_CONTENT_URI = getContentUri("external");


/**
 * Get the content:// style URI for the image media table on the
 * given volume.
 *
 * @param volumeName the name of the volume to get the URI for
 * @return the URI to the image media table on the given volume
 */
public static Uri getContentUri(String volumeName) {
    return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + "/images/media");
}

      根据源代码以及代码注释我们 EXTERNAL_CONTENT_URI获取的是第一张SD卡的文件里面的 image 信息,其 AUTHORITY 为 media,因为我们是通过调用ContentProvider去获取数据库的信息,那么系统代码的 AndroidManifest.xml必然会注册一个这样子的provider的,于是乎我们就可以通过使用Source Insight去搜寻整个Android的代码,同时我们还可以过滤只搜寻.xml的文件中是否存在 android:authorities=”media” 的就行了。最后发现整个该ContentProvider路径为: /packages/providers/MediaProvider/。MediaProvider则是我们要查找的Provider。

代码分析

Uri路径匹配

   &nbsp通过上面的分析我们知道获取图片信息是通过调用系统的ContentProvider的来获取系统数据库中保存的图片的信息了。平时我们在写ContentProvider的时候一般都会写静态代码块用于保存不同Uri的路径和信息的,以后我们就会用该类去匹配各种不同的Uri了。上篇文章的时候我们已经介绍了获取图片的uri地址为:content://media/external/images/media,接着我们看看MediaProvider类中静态代码Uri信息匹配。

static
    {
        //匹配的是所有的图片的所有信息,也就是我们本次的目标
        URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
        //根据Image的id来获取对应的图片信息
        URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);

        .....
        //获取音频文件的所有信息
        URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
        //根据所给的音频文件的编号获取音频的信息
        URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
        //获取音频文件的艺术家信息
        URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
        URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
        //获取视频文件的所有信息
        URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
        //根据视频文件的编号获取其信息
        URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
        ....

        // Used by MTP implementation
        URI_MATCHER.addURI("media", "*/file", FILES);
        URI_MATCHER.addURI("media", "*/file/#", FILES_ID);

        ......
    }

    从上面的代码中我们可以很快的发现图片的路径匹配的code是IMAGES_MEDIA,所以我们在通过Provider查询图片信息的时候就知道是其匹配的Code则是 IMAGES_MEDIA。通过上面的静态代码块的分析我们很快的就知道了我们要分析的uri的code,下面我们就分析query函数:

query 函数的分析
    通过静态代码块我们根据Uri分析出了对应的code之后,下面我们就可以通过查询方法去查找对应的数据库了,因为在查询的时候是需要匹配Uri的。

public Cursor query(Uri uri, String[] projectionIn, String selection,
                                                    String[] selectionArgs, String sort) {

    uri = safeUncanonicalize(uri);

    int table = URI_MATCHER.match(uri);
    List<String> prependArgs = new ArrayList<String>();
    /**
     * 这里是根据对应的Uri来获取对应的DatabaseHelper,有了该类之后我们就可以获取DataBase了
     * 所以getDatabaseForUri()就是一个非常重要的方法。因为会根据不同的uri获取不同DataBaseHelper。
     */
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        return null;
    }
    helper.mNumQueries++;
    SQLiteDatabase db = helper.getReadableDatabase();
    if (db == null) return null;
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

    ........

    switch (table) {
        //这个就是我们之前匹配成功的uri对应的code。
        case IMAGES_MEDIA:
            qb.setTables("images");
            if (uri.getQueryParameter("distinct") != null)
                qb.setDistinct(true);

            // set the project map so that data dir is prepended to _data.
            //qb.setProjectionMap(mImagesProjectionMap, true);
            break;
            .......
    }
    .......
    //这里我们通过SQLiteQueryBuilder以及参数db去查询具体的信息
    Cursor c = qb.query(db, projectionIn, selection,
                combine(prependArgs, selectionArgs), groupBy, null, sort, limit);

    if (c != null) {
        String nonotify = uri.getQueryParameter("nonotify");
        if (nonotify == null || !nonotify.equals("1")) {
            c.setNotificationUri(getContext().getContentResolver(), uri);
        }
    }

    return c;
}

/**
 * Looks up the database based on the given URI.
 * @param uri The requested URI
 * @returns the database for the given URI
 */
private DatabaseHelper getDatabaseForUri(Uri uri) {
    synchronized (mDatabases) {
        if (uri.getPathSegments().size() >= 1) {
            return mDatabases.get(uri.getPathSegments().get(0));
        }
    }
    return null;
}

    我们之前图片的Uri为content://media/external/images/media通过调用getPathSegments()返回的则是一个List,比如:”external”,”images”,”media”,具体不清楚的则需要详细的看看Uri的规则与协议的。mDatabases是一个HashMap

@Override
public boolean onCreate() {
    mDatabases = new HashMap<String, DatabaseHelper>();
    attachVolume(INTERNAL_VOLUME);

    .....

    StorageManager storageManager =
            (StorageManager)context.getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    // open external database if external storage is mounted
    String state = Environment.getExternalStorageState();
    //首先这里会判断SD卡是否已经挂载或者是是否可读
    if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        attachVolume(EXTERNAL_VOLUME);
    }
    ...
 }

    在ContentProvider的onCreate()方法中我们可以很清楚的知道了attachVolume()就是创建或者打开数据库的核心方法,从这里我们可以看出这里将会创建两个数据库:internal.db和external,通过我们之前的获取getPathSegments(uri)可以很明确的知道了图片的信息都是存放在数据库external.db,该数据库主要是用来存放外置SD卡的内容的,internal.db主要是用于存放手机内部存储卡的信息

/**
 * Attach the database for a volume (internal or external).
 * Does nothing if the volume is already attached, otherwise
 * checks the volume ID and sets up the corresponding database.
 *
 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
 * @return the content URI of the attached volume.
 */
private Uri attachVolume(String volume) {

    ......

    synchronized (mDatabases) {//这里表示mDatabases已经保存了DataBaseHelper则返回
        if (mDatabases.get(volume) != null) {  // Already attached
            return Uri.parse("content://media/" + volume);
        }

        Context context = getContext();
        DatabaseHelper helper;
        //创建internal 数据库
        if (INTERNAL_VOLUME.equals(volume)) {
            helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
                    false, mObjectRemovedCallback);
        } else if (EXTERNAL_VOLUME.equals(volume)) {
            //首先这里判断SD是否可以被移除
            if (Environment.isExternalStorageRemovable()) {
                //获取SD卡的VolumeId编号
                String path = mExternalStoragePaths[0];
                int volumeID = FileUtils.getFatVolumeId(path);
                if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID);

                if (volumeID == -1) {
                    String state = Environment.getExternalStorageState();
                    if (Environment.MEDIA_MOUNTED.equals(state) ||
                            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
                        Log.e(TAG, "Can't obtain external volume ID even though it's mounted.");
                    } else {
                        Log.i(TAG, "External volume is not (yet) mounted, cannot attach.");
                    }
                    throw new IllegalArgumentException("Can't obtain external volume ID for " +
                            volume + " volume.");
                }

                // generate database name based on volume ID
                String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
                helper = new DatabaseHelper(context, dbName, false,
                        false, mObjectRemovedCallback);
                mVolumeId = volumeID;
            } else {
                // external database name should be EXTERNAL_DATABASE_NAME
                // however earlier releases used the external-XXXXXXXX.db naming
                // for devices without removable storage, and in that case we need to convert
                // to this new convention
                File dbFile = context.getDatabasePath(EXTERNAL_DATABASE_NAME);
                //这里是判断安装包下是否存在了该数据库文件
                if (!dbFile.exists()) {
                    // find the most recent external database and rename it to
                    // EXTERNAL_DATABASE_NAME, and delete any other older
                    // external database files
                    File recentDbFile = null;
                    for (String database : context.databaseList()) {
                        if (database.startsWith("external-") && database.endsWith(".db")) {
                            File file = context.getDatabasePath(database);
                            if (recentDbFile == null) {
                                recentDbFile = file;
                            } else if (file.lastModified() > recentDbFile.lastModified()) {
                                context.deleteDatabase(recentDbFile.getName());
                                recentDbFile = file;
                            } else {
                                context.deleteDatabase(file.getName());
                            }
                        }
                    }
                    if (recentDbFile != null) {
                        if (recentDbFile.renameTo(dbFile)) {
                            Log.d(TAG, "renamed database " + recentDbFile.getName() +
                                    " to " + EXTERNAL_DATABASE_NAME);
                        } else {
                            Log.e(TAG, "Failed to rename database " + recentDbFile.getName() +
                                    " to " + EXTERNAL_DATABASE_NAME);
                            // This shouldn't happen, but if it does, continue using
                            // the file under its old name
                            dbFile = recentDbFile;
                        }
                    }
                    // else DatabaseHelper will create one named EXTERNAL_DATABASE_NAME
                }
                helper = new DatabaseHelper(context, dbFile.getName(), false,
                        false, mObjectRemovedCallback);
            }
        } else {
            throw new IllegalArgumentException("There is no volume named " + volume);
        }

        //将创建的数据库保存到HashMap<String, DataBasehelper>中
        mDatabases.put(volume, helper);

        .......
    }

    return Uri.parse("content://media/" + volume);
}

    上面就是创建internal.db和external.db数据库的一些逻辑,具体的逻辑也不是很难的,这里我就不一一的进行的分析了,我们最关键的就是需要查找图片的信息是存储在哪里。以及后面我们所说的那些什么视频文件,音频文件以及压缩包文件等等信息是如何查询的。通过对上面的认识我们可以很清楚的知道了图片信息都是保存在包名为com.android.providers.media的安装包的安装目录下的。该应用程序的名字叫Media Storage,只是它没有主界面的,只有ContentProvider以及Service以及Receiver而已的。主要的作用就是去管理SD卡中的文件,并将这些文件的信息进行分配检索等等。

    上面我们知道了系统中有一个专门的程序在管理着手机中的文件信息,下面我们就adb shell进入我们的手机里面看看具体的内容(由于进入的目录是/data/data目录下,所以手机是需要root权限才可以看到的)。

image_1bomne7fr8f8fge11qoi72gio9.png-14.3kB

    通过实践我们证实了我们之前的猜想是正确的,下面我们就把external.db导出到电脑上并且用SQLiteExpertPers打开看看里面的东西。其实我们可以从代码中去查看具体的表的信息,但是也可以根据数据库中的表来猜测其具体的功能。

image_1bomo3g8ffep1teac1t1aul86bm.png-78kB

    由于我手机上的文件比较多所以数据库的大小最后都有90MB了,打开文件之后我们可以看到有很多的table,具体的信息我这里就不做介绍了,其实这些表的结构以及信息都可以在MediaProvider的内部类DatabaseHelper中看到的。

image_1bomoh3a3ohp1u55lip1d556ol13.png-58.2kB
image_1bomohe71nhu11qo161aidm16gg1g.png-52.7kB

    在上篇博客的时候我们介绍了查询图片信息的时候用到了一些查询条件一些内容,通过表中我们就可以很快的知道了这些东西背后的实际情况,_data就是图片存储的路径,_size就是图片的大小(不过个人不怎么喜欢用这个大小的,还是喜欢通过实际的文件去获取的),_mime_type就是文件的类型,有些可能是image/png的,_display_name就是图片的文件名字改名字是截图_data路径的后面的那个名字,date_added则是图片添加的时间,date_modified则是图片的修改时间等等;下面我就不一一的例举了,因为我们知道了数据库的路径以及它内部的一些实现等等,具体的分析和深入研究要看具体的实际需求了。

image_1bomou721tqe4vh16311991nfn1t.png-152.7kB

    在数据库的files表中我们可以看到手机SD卡上所有文件的信息,在我手机上就有大概210w条记录,注意:虽然数据库里有这么多的记录,但是手机可能没有这么多的文件有些文件可能在删除的时候并没有马上的通知系统(也就是app)更新数据库,还有一些则是比较深的目录系统则不会去扫描的,系统只会去扫描一些特定的目录,或者是我们主动告诉系统我们的文件存放的路径,如果我们在删除文件或者是更新文件的时候去告诉系统的话,则系统监测到了则会马上更新数据库文件的

    本文主要是讲解Media模块的MediaProvider,通过一个简单的获取文件图片的信息来找出其背后的原理,其实我们获取的图片信息只是Media模块的一个非常小的部分的,它还有音频、视频、压缩包、日志等等其他的操作以及文件搜索、删除、更新等等。其实本文只是简单的介绍了一下Media的MediaProvider也就是文件存储的结构的介绍,详细的一些细节则需要个人根据具体的业务深入的去分析了。Media Storage的所有操作都是在packages/providers/MediaProvider/目录下的,我们平时可以如果想对这个方面有更多了解的话就可以直接查看该目录下的源代码的,同时你也要更加的关注android.provider.MediaStore,因为大部分有关文件的uri都是在这个类里面,获取手机文件中的信息的话,我们只能通过系统暴露给我们的ContentProvider接口去查询的。

总结

    本次文章我们总结了文件信息的存放路径以及一些简单的代码,当我们阅读了这些代码之后发现其实也没有我们想象中的这么难了,我们之后就可以更加深入的了解了这个里面的原理以及操作,同时在查看源代码的时候我们也可以学到别人写代码的优点和长处了。其实我们在看代码的时候更多的时候是看人家的思想以及思考人家为什么这么设计,如果是我来做的话我会怎么去设计这个系统呢?会去怎么做这件事情呢?所以设计的思想是非常关键,通过深入看了代码之后我们发现其实我们也可以写出来的。下篇文章我们将会分析文件添加和删除的一些通知以及系统是如何扫描文件的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值