• ContentProvider深度探索


    如果你选择用SQLite数据库存储应用程序数据,我建议你创建ContentProvider,即使存储的数据仅供内部使用。原因是Android提供了一些工具类以及UI相关的类,它们的工作在ContentProvider之上,能够简化开发者的工作。此外,这些类还提供了一个简单的机制,一旦数据有更新就会通知客户端,这让开发者保持用户界面和实际内容的同步变得很简单。

    创建数据库表时,一定要考虑它们的主要目的。数据库是否主要用于读取以及显示用户界面上?数据库是否主要用于写操作并且在后台运行,如记录运动数据?更具不同的用途,读性能可能会比写操作更重要,反过来也有可能。

    1.创建数据库

    要讲解优化ContentProvider之前我们必须创建一下讲解必须要用到的数据库,毕竟前言不搭后语的讲解,读者可能很难理解。

    下面的数据库很简单,只有一个名称,一个ID,一个完整的文件名dir。

    public class ImageProvider extends ContentProvider {
        public static final String AUTHORITY="com.example.liyuanjing.dbcppro.provider";
    public static final int ALL_IMAGE=20;//查询全部信息
    public static final int SINGLE_IMAGE=10;//查询单个信息
    public static final String TABLE_NAME="images";
    public static final String[] ALL_COLUMNS=new String[]{"id,","name","dir"};
    public static UriMatcher mUriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
    public static final String CREATE_TABLE="create table images("+
                "id integer primary key autoincrement,"
    +"name text,"
    +"dir text)";
    public MyDatabaseHelper mOpenHelper;
    static {
            mUriMatcher.addURI(AUTHORITY,"images",ALL_IMAGE);
    mUriMatcher.addURI(AUTHORITY,"images/#",SINGLE_IMAGE);
    }
    
        private class MyDatabaseHelper extends SQLiteOpenHelper{
            public MyDatabaseHelper(Context context) {
                super(context, "imagedb", null, 1);
    }
    
            @Override
    public void onCreate(SQLiteDatabase db) {
                db.execSQL(CREATE_TABLE);
    }
    
            @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
            }
        }

    为了使例子更加清晰,代码示例使用String常量声明SQL语句,更好的办法是把这些字符串存储到应用程序的raw资源目录内的文件中。这样会更容易使用,并能简化测试工作。

    2.优化ContentProvider的查询方法

    查询数据库会调用ContentProvider.query()方法。开发者在实现查询方法时必须解析传入的Uri以决定执行哪个查询,并且还要检查所有传入的参数是安全的。

    下面的代码实现的query()方法,以及用于修改selection和selectionArgs参数的两个工具方法。

    public static String[] fixSelectionArgs(String[] selectionArgs,String imageId){
        if(selectionArgs==null){
            selectionArgs=new String[]{imageId};
    return selectionArgs;
    }else{
            String[] newSelectionArgs=new String[selectionArgs.length+1];
    newSelectionArgs[0]=imageId;
    System.arraycopy(selectionArgs,0,newSelectionArgs,1,selectionArgs.length);
    return  newSelectionArgs;
    }
    }
    public static String fixSelectionString(String selection){
        selection=selection==null?"id=?":"id=? AND ("+selection+")";
    return selection;
    }
    public Cursor query(Uri uri, String[] projection, String selection,
    String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db=mOpenHelper.getReadableDatabase();
    switch (mUriMatcher.match(uri)){
            case ALL_IMAGE:
                return db.query(TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder);
    case SINGLE_IMAGE:
                String tableId=uri.getLastPathSegment();
    selection=fixSelectionString(selection);
    selectionArgs=fixSelectionArgs(selectionArgs,tableId);
    return db.query(TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder);
    default:
                throw new IllegalArgumentException("error:"+uri);
    }
    }
    只有使用Uri定位数据库中特定的记录时才会使用修改selection和selectionArgs参数的两个工具方法。注意:本例会在现有的selection前面加上ID列。这使得查询速度更快,因为对主键列的比较总是非常快的,从而能加快整个查询。
        编写数据库查询要把WHERE语句中简单的比较放在前面。这样做会加快查询,因为它能尽早决定是否要包含某个记录。
    3.数据库事务
        每次在SQLite数据库执行一条SQL语句都会执行一次数据库事务操作。除非自己专门管理事务,否则每条语句都会自动创建一个事务。因为大多数ContentProvider调用最终只会生成一条SQL语句,这样情况下几乎没有必要去手动处理事务。但是,如果应用程序将执行多条SQL语句,比如一次插入很多条记录,记得总是自己管理事务。
        ContentProvider类提供了两个用于管理事务的方法:ContentProvider.bulkInsert()和ContentProvider.applyBatch(),下面的代码演示了如何实现bulkInsert()方法,它会在一个事务中插入多条记录。相比每次有新数据都调用ContentProvider.insert(),这种方法会明显快的多。
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db=mOpenHelper.getWritableDatabase();
    Uri result=doInsert(uri,values,db);
    return result;
    }
    
    private Uri doInsert(Uri uri,ContentValues values,SQLiteDatabase db){
        Uri result=null;
    switch (mUriMatcher.match(uri)){
            case ALL_IMAGE:
                long id=db.insert(TABLE_NAME,"",values);
    if(id==-1) Log.i("ImageProvider","insert error");
    result=Uri.withAppendedPath(uri,String.valueOf(id));
    }
        return result;
    }
    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        SQLiteDatabase db=mOpenHelper.getWritableDatabase();
    int count=0;
    try{
            db.beginTransaction();
    for (ContentValues value:values){
                Uri resultUri=doInsert(uri,value,db);
    if(resultUri!=null){
                    count++;
    }else{
                    count=0;
                    throw new SQLException("error bulk");}
            }
            db.setTransactionSuccessful();
    }finally {
            db.endTransaction();
    }
        return count;
    }
    事务的语义很简单。首先调用SQLiteDatabase.beginTransaction()开始一个新的事务。当成功插入所有记录后调用SQLiteDatabase.setTransactionSuccessful(),然后使用SQLiteDatabase.endTransaction()结束本次事务。如果某条记录插入失败,会抛出SQLExcetion,而之前所有的插入都会回滚,因为在成功之前没有调用过SQLiteDatabase.setTransactionSuccessful()。
        强烈建议在继承ContentProvider时实现该方法,因为它显著提高数据插入的性能。但是,由于此方法只适用于插入操作,开发者可能需要实现另一个方法来处理更复杂的操作。
        如果要在一次事务中执行多次update()或则delete()语句,必须实现ContentProvider.applyBatch()方法。
    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
        SQLiteDatabase db=mOpenHelper.getWritableDatabase();
    ContentProviderResult[] result=new ContentProviderResult[operations.size()];
    try{
            db.beginTransaction();
    for (int i = 0; i < operations.size(); i++) {
                ContentProviderOperation operation=operations.get(i);
    result[i]=operation.apply(this,result,i);
    }
            db.setTransactionSuccessful();
    }finally {
            db.endTransaction();
    }
        return super.applyBatch(operations);
    }
     正如bulkInsert()方法,首先开始一个事务,执行操作,设置本事务执行成功,最后结束本次事务。该API是为了ContentProvider等较复杂的ContentProvider设计的,它们有许多的连接的表,每个都有自己的Uri。另外,如果要批量插入多个表,该API仍然有效。
    4.在ContentProvider中存储二进制数据
        二进制数据包括不能用java中的简单数据类型表示的任何对象,通常是图像或其他一些媒体文件,但它可以是任何类型的专有格式文件。和二进制数据打交道可能会非常棘手,但幸好ContentProvider提供了许多用于处理这个问题的方法。比方说,上表的每条记录都存储了一张图片。
        覆写ContentProvider.openFile()方法返回ParcelFileDescriptor对象。然后就可以使用该对象直接读写文件了。
    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        if(mUriMatcher.match(uri)==SINGLE_IMAGE){
            String imageId=uri.getLastPathSegment();
    File file=new File(Environment.getExternalStorageDirectory().toString()+TABLE_NAME+
    imageId
    );int fileMode = 0;if (mode.contains("w")) fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;if (mode.contains("r")) fileMode |= ParcelFileDescriptor.MODE_READ_ONLY;if (mode.contains("+")) fileMode |= ParcelFileDescriptor.MODE_APPEND;return ParcelFileDescriptor.open(file, fileMode);}else { return super.openFile(uri, mode);}}
     实际调用下面的方法将结果传递给调用客户端。当从ContentProvider中读取某条记录的Bitmap对象时,可以简单地使用该记录的Uri,接下来的事情给框架去处理:
    public static Bitmap readBitmapFromProvider(Uri uri,ContentResolver resolver)throws FileNotFoundException{
        return BitmapFactory.decodeStream(resolver.openInputStream(uri));
    }
        存储文件跟正确的Uri调用ContentResolver.openOutputStream()相似。如果要从ContentProvider读取数据,并在ListView中展示包含文本和图片的数据,它会非常有用,前面的例子也展示了这一点。

    版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 相关阅读:
    centos6.5安装mysql5.7.20
    redis错误总结
    批量杀死MySQL连接的四种方法详解
    VMware虚拟机宿主机与虚拟机通讯慢解决方法
    linux下ssh/scp无密钥登陆方法
    天兔3.8安装 centos7
    《TensorFlow+Keras自然语言处理实战》已出版
    几本技术图书资源下载
    2020年上半年新书
    《Neo4j 图数据库扩展指南:APOC和ALGO》
  • 原文地址:https://www.cnblogs.com/liyuanjinglyj/p/4656570.html
Copyright © 2020-2023  润新知