欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ContentProvider 的批处理操作

程序员文章站 2022-04-28 23:08:19
...

Overview

ContentProvider是android 系统核心组件之一,其封装了数据的访问接口,其底层数据一般都是保存在数据库中或者保存在云端。

大多数情况下其实我们用不到ContentProvider,如果自己的应用程序与别的应用什么交互,直接使用SQLite数据库即可(想使用ContentProvider之前先看下官方文档,再决定是否真的需要)。不过自从有了一些ORM的开源库,我们甚至很少自己来操作数据库了。

ContentProvider和ContentResolver为我们提供了基础的接口,能满足大部分需求。但是当处理的数据量比较大的时候,我们可以选择调用多次ContentResolver的对应函数 或者 使用批处理操作。当然 后者性能会比较好些。

[bulkInsert](https://developer.android.com/reference/android/content/ContentProvider.html#bulkInsert(android.net.Uri, android.content.ContentValues[]))

如果只是涉及到单表的批量插入,我们可以直接使用 bulkInsert(Uri uri, ContentValues[] values) 进行批量插入即可。

ContentProviderOperation

为了使批量更新、插入、删除数据更加方便,android系统引入了 ContentProviderOperation
类。
在官方开发文档中推荐使用ContentProviderOperations,有一下原因:

  1. 所有的操作都在一个事务中执行,这样可以保证数据完整性
  2. 由于批量操作在一个事务中执行,只需要打开和关闭一个事务,比多次打开关闭多个事务性能要好些
  3. 使用批量操作和多次单个操作相比,减少了应用和content provider之间的上下文切换,这样也会提升应用的性能,并且减少占用CPU的时间,当然也会减少电量的消耗。

ContentProviderOperation.Builder

要创建ContentProviderOperation对象,则需要使用 ContentProviderOperation.Builder类,通过调用下面几个静态函数来获取一个Builder 对象:

函数 用途
newInsert 创建一个用于执行插入操作的Builder(支持多表)
newUpdate 创建一个用于执行更新操作的Builder
newDelete 创建一个用于执行删除操作的Builder
newAssertQuery 可以理解为断点查询,也就是查询有没有符合条件的数据,如果没有,会抛出一个OperationApplicationException异常

这个Buidler对象使用了著名的Builder设计模式,由于Builder对象的函数都返回了自己,所以通过一系列的函数链式调用即可生成最终的ContentProviderOperation对象。

    /*
     * Prepares the batch operation for inserting a new raw contact and its data. Even if
     * the Contacts Provider does not have any data for this person, you can't add a Contact,
     * only a raw contact. The Contacts Provider will then add a Contact automatically.
     */

     // Creates a new array of ContentProviderOperation objects.
    ArrayList<ContentProviderOperation> ops =
            new ArrayList<ContentProviderOperation>();

    /*
     * Creates a new raw contact with its account type (server type) and account name
     * (user's account). Remember that the display name is not stored in this row, but in a
     * StructuredName data row. No other data is required.
     */
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

当然 你还可以使用熟悉的ContentValues对象,对应的函数为withValues(values)。

Builder的核心函数

Builder对象核心函数的介绍(可以直接查看API):

函数 用途
withSelection (String selection, String[] selectionArgs) 指定需要操作的数据条件。只能用于update, delete, or assert。
withSelectionBackReference(int selectionArgIndex, int previousResult) 添加一个“向后引用” 作为查询条件。之前通过withSelection(String, String[])指定的selectionArgIndex位置的值会被覆盖掉。只能用于update, delete, or assert。
withValue (String key, Object value) 定义一列的数据值,类似于向ConetentValues中加入一条数据。只能用于 insert, update, or assert。
withValues (ContentValues values) 定义多列的数据值。 只能用于 insert, update, or assert
withValueBackReference(String key, int previousResult) 添加一个“向后引用” 。使用“向后引用” 中的值来设置指定“key”列的值,所谓向后引用其实就是一组Operation中第previousResult个ContentProviderOperation完成之后返回的ContentProviderResult,如果是insert操作则会使用ContentProviderOperation返回的uri中的ID,如果是update或者assert就是使用返回的count。这个值会覆盖之前withValues(ContentValues)设置的值。只能用于 insert, update, or assert。
withValueBackReferences(ContentValues backReferences) 添加一个“向后引用” 。使用ContentValues来完成多次withValueBackReference操作。ContentValues 中的key和value就对应于"列名"和"previousResult的索引",参考withValueBackReference的参数。value会被作为String来添加。这个值会覆盖之前withValues(ContentValues)设置的值。只能用于 insert, update, or assert。
withExpectedCount(int count) 验证影响的行数,如果跟count不相同,会抛出OperationApplicationException 。只能用于update, delete, or assert操作。

关于"向后引用"可以参考官网的一些说明

最后通过ContentResolver 的applyBatch()函数来应用批量操作:

try {
   getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (RemoteException e) {
   // do s.th.
} catch (OperationApplicationException e) {
   // do s.th.
}

工作原理

从ContentProviderOperation.Builder的build()方法开始,可以看到构造出了一个新的 ContentProviderOperation()。

        /** Create a ContentProviderOperation from this {@link Builder}. */
        public ContentProviderOperation build() {
            if (mType == TYPE_UPDATE) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
                    throw new IllegalArgumentException("Empty values");
                }
            }
            if (mType == TYPE_ASSERT) {
                if ((mValues == null || mValues.size() == 0)
                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)
                        && (mExpectedCount == null)) {
                    throw new IllegalArgumentException("Empty values");
                }
            }
            return new ContentProviderOperation(this);
        }

批量操作是从getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList)开始, 调用最终会走到 ContentProvider.applyBatch(),这个方法中做了两件事情:

  1. 定义了一个ContentProviderResult[]数组,数组的大小等于operations的大小。ContentProviderResult用于保存每个ContentProviderOperation的执行结果。ContentProviderResult会有两种类型,一种具体的“uri”,另一种是此次操作影响到的“count”行数,最后会在“向后引用”中派上用场。
  2. 遍历operations,并且调用相应的ContentProviderOperation.apply操作,把结果返回到对应的ContentProviderResult[]数组中保存起来。
    public @NonNull ContentProviderResult[] applyBatch(
            @NonNull ArrayList<ContentProviderOperation> operations)
                    throws OperationApplicationException {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        return results;
    }

在ContentProviderOperation.apply方法中,有以下几个重要的步骤:

  1. 先调用resolveValueBackReferences(),处理"向后引用"的ContentValue。
  2. 再调用resolveSelectionArgsBackReferences,处理"向后引用"的查询参数。
  3. 如果是insert操作就直接调用了 provider.insert,并insert返回的uri赋值给new ContentProviderResult(newUri)
  4. 同样的,如果是delelte、update,也是直接调用provider.delete、provider.update,并把返回numRows赋值给new ContentProviderResult(numRows)
  5. 相对于AssertQuery,直接调用provider.query把查询出来的数据与期望的Values进行比较,如果一样就返回对应行数的new ContentProviderResult(numRows);如果不一样就报Exception。
  6. 如果mExpectedCount不为空(表示设置了withExpectedCount(int count)),会与numRows进行比较,判断期望值是否与实际操作值一样,不一样就报OperationApplicationException。
    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
            int numBackRefs) throws OperationApplicationException {
        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
        String[] selectionArgs =
                resolveSelectionArgsBackReferences(backRefs, numBackRefs);

        if (mType == TYPE_INSERT) {
            Uri newUri = provider.insert(mUri, values);
            if (newUri == null) {
                throw new OperationApplicationException("insert failed");
            }
            return new ContentProviderResult(newUri);
        }

        int numRows;
        if (mType == TYPE_DELETE) {
            numRows = provider.delete(mUri, mSelection, selectionArgs);
        } else if (mType == TYPE_UPDATE) {
            numRows = provider.update(mUri, values, mSelection, selectionArgs);
        } else if (mType == TYPE_ASSERT) {
            // Assert that all rows match expected values
            String[] projection =  null;
            if (values != null) {
                // Build projection map from expected values
                final ArrayList<String> projectionList = new ArrayList<String>();
                for (Map.Entry<String, Object> entry : values.valueSet()) {
                    projectionList.add(entry.getKey());
                }
                projection = projectionList.toArray(new String[projectionList.size()]);
            }
            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
            try {
                numRows = cursor.getCount();
                if (projection != null) {
                    while (cursor.moveToNext()) {
                        for (int i = 0; i < projection.length; i++) {
                            final String cursorValue = cursor.getString(i);
                            final String expectedValue = values.getAsString(projection[i]);
                            if (!TextUtils.equals(cursorValue, expectedValue)) {
                                // Throw exception when expected values don't match
                                Log.e(TAG, this.toString());
                                throw new OperationApplicationException("Found value " + cursorValue
                                        + " when expected " + expectedValue + " for column "
                                        + projection[i]);
                            }
                        }
                    }
                }
            } finally {
                cursor.close();
            }
        } else {
            Log.e(TAG, this.toString());
            throw new IllegalStateException("bad type, " + mType);
        }

        if (mExpectedCount != null && mExpectedCount != numRows) {
            Log.e(TAG, this.toString());
            throw new OperationApplicationException("wrong number of rows: " + numRows);
        }

        return new ContentProviderResult(numRows);
    }

在resolveValueBackReferences方法中会先判断mValuesBackReferences 是否为空,如果为空就直接返回mValues,mValues就是通过withValue 或者 withValues方法填进去的值所组装的ContentValue对象,比如要更新或要插入的值。如果mValuesBackReferences != null(使用了withValueBackReference或withValueBackReferences),就需要处理"向后引用"的值,其实就是找出第“previousResult”个已经完成的ContentProviderOperation所返回的ContentProviderResult的值并与对应的key(列名)绑定起来。查找“向后引用”是在backRefToValue函数中实现,继续往下看。

resolveSelectionArgsBackReferences函数也是类似的作用。

    public ContentValues resolveValueBackReferences(
            ContentProviderResult[] backRefs, int numBackRefs) {
        if (mValuesBackReferences == null) {
            return mValues;
        }
        final ContentValues values;
        if (mValues == null) {
            values = new ContentValues();
        } else {
            values = new ContentValues(mValues);
        }
        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
            String key = entry.getKey();
            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
            if (backRefIndex == null) {
                Log.e(TAG, this.toString());
                throw new IllegalArgumentException("values backref " + key + " is not an integer");
            }
            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
        }
        return values;
    }

在backRefToValue中处理了两种情况,如果ContentProviderResult中uri不为空,就返回uri对应的ID;如果为空就返回count值。所以,从上面的apply函数可以看出,insert对应的是ID;而delete、update、assertQuery则会返回count。

    private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
            Integer backRefIndex) {
        if (backRefIndex >= numBackRefs) {
            Log.e(TAG, this.toString());
            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                    + " but there are only " + numBackRefs + " back refs");
        }
        ContentProviderResult backRef = backRefs[backRefIndex];
        long backRefValue;
        if (backRef.uri != null) {
            backRefValue = ContentUris.parseId(backRef.uri);
        } else {
            backRefValue = backRef.count;
        }
        return backRefValue;
    }

使用事务

参考MediaProvider.java的实现,在applyBatch中使用事务:

  @NonNull
    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            ContentProviderResult[] results = super.applyBatch(operations);
            db.setTransactionSuccessful();
            return results;
        } finally {
            db.endTransaction();
        }
    }

参考

Android Developer 中关于ContentProvider批量操作的介绍 -- 联系人提供程序
Android 联系人提供程序同步适配器 Gibhub sample code
* 上关于withValueBackReference 的解答
Android’s ContentProviderOperation: “withBackReference” explained
Android利用ContentProviderOperation添加联系人