ContentProvider实践

ContentProvider

简介

ContentProvider(内容提供者)是提供跨程序数据访问的Android组件。简而言之,相当于一个数据接口,可以将程序内部的数据向其他应用程序公开,让其他应用程序可以增删查改操作。
Android原生的应用很多都应用了ContentProvider例如通讯录,文件系统等。

存在的原因

  1. 解决应用程序的数据库在私有目录下,其他应用程序使用常规方法无法访问数据库。
  2. 程序内部的数据库结构复杂,即使可以访问,但是如果不了解表的构造也很难访问,所以通过ContentProvider封装对数据的访问,给外部程序访问数据的一个直观的接口。
  3. 如果直接提供其他应用程序访问数据的权限不加以控制,那么数据安全也是一个问题,所以通过ContentProvider即可以提供数据接口并且可以控制访问权限。

简单的使用

主要步骤:数据库提供方:继承ContentProvider实现有关数据库的相关操作,然后再AndroidMainfest.xml文件中注册。 数据库访问方: 通过'URI'访问对应的ContentProvider

PS: 一定要在AndroidMainfest.xml文件中注册,每一个ContentProvider对应一个authorities,需要注意的是authorities在整个系统必须是唯一的,否则安装会有冲突。并且要注明是android:exported="true"才能被外部访问。
注册例子如下:

 <provider
            android:name="com.doris.support.provider.SettingsProvider"
            android:authorities="com.doris.contact.provider"
            android:exported="true" >
        </provider>

上面只是粗略的总结了一下用法,不过肯定这几行字,你们还是不太清楚具体如何使用。接下来我会一步一步的带着大家来自定义ContentProvider以及使用它。接下来要长篇大论了,做好准备啦,千万别晕!

这里我们统一一下,把提供数据的应用程序称之为服务端,把请求数据的应用程序称之为客户端。

服务端

  • 在服务器自定义一个ContentProvider提供数据访问接口。

1.自定义ContentProvider

自定义ContentPorvider继承自ContentPorvider,这里我们自定义一个ContactProvider用于获取Contact数据表的数据,Contact表用于存储联系人信息。先贴一下完整代码:

package com.doris.provide;

import com.orhanobut.logger.Logger;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;

/**
 * Created by Doris on 2017/2/24.
 */

public class ContactProvider extends ContentProvider {
    public static final int     CODE_CONTACT            = 0X108;
    private static final String TAG                         = ContactProvider.class.getSimpleName();
    private static final String AUTHORITYS = "com.doris.contact.provider";
    private static final String TABLE_NAME = "Contact";
    //通过UriMatcher匹配Uri从而执行对应的操作。
    private static UriMatcher   mUriMatcher                 = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        mUriMatcher.addURI(AUTHORITYS, TABLE_NAME, CODE_CONTACT);
    }
    @Override
    public boolean onCreate() {
        Logger.d(TAG, "onCreate");
        return true;
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Cursor cursor = null;
        switch (mUriMatcher.match(uri)) {
            case CODE_CONTACT :
                cursor = DBHelper.getInstance().query(TABLE_NAME, selection, selectionArgs, sortOrder);
                break;
        }
        return cursor;
    }
    
    @Override
    public String getType(Uri uri) {
        return uri.toString();
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        switch (mUriMatcher.match(uri)) {
            case static {
        mUriMatcher.addURI(AUTHORITYS, TABLE_NAME, CODE_CONTACT);
    } : {
                long rowId2 = DBHelper.getInstance().insert(TABLE_NAME, values);
                if (rowId2 > 0) {
                    Uri noteUri = ContentUris.withAppendedId(uri, rowId2);
                    getContext().getContentResolver().notifyChange(noteUri, null);
                    return noteUri;
                }
            }
                break;
        }
        
        return null;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
        switch (mUriMatcher.match(uri)) {
            case CODE_CONTACT :
                count = DBHelper.getInstance().delete(TABLE_NAME, selection, selectionArgs);
                break;
        }
        return count;
    }
    
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int count = 0;
        switch (mUriMatcher.match(uri)) {
            case CODE_CONTACT :
                count = DBHelper.getInstance().update(CPosterManager.TABLE_NAME, values, selection, selectionArgs);
                break;
        }
        return count;
    }
}

在上面这段代码中,我们自定义了一个ContactProvider提供了对Contact表的增删查改操作,而增删查改动作又是通过DBHelper类来实现的,DBHelper类封装了对数据库的操作。
我们来解析一下代码。以上代码的关键就是UriMatch如其名,用来匹配Uri来解读对应的操作CODE,从而对应到某个操作上。因为其他程序都是通过uri来访问ContactProvider的所以需要匹配区分,访问同一个Provider的时候分别是要请求什么数据。比如:content:\\com.doris.contact.provider\contact,通过UriMathc匹配则是匹配到CODE_CONTACT.
ContactProvider里面只有一个CODE就是CODE_CONTACT,通过这个来标示操作Contact表。

static {
      //在这句代码里面就是通过addURI来添加UriMatcher的一个匹配规则。
      mUriMatcher.addURI(AUTHORITYS, TABLE_NAME, CODE_CONTACT);
  }
  
  //addURI的方法如下:
  
  /**
   * Add a URI to match, and the code to return when this URI is
   * matched. URI nodes may be exact match string, the token "*"
   * that matches any text, or the token "#" that matches only
   * numbers.
   * <p>
   * Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
   * this method will accept a leading slash in the path.
   *
   * @param authority the authority to match
   * @param path the path to match. * may be used as a wild card for
   * any text, and # may be used as a wild card for numbers.
   * @param code the code that is returned when a URI is matched
   * against the given components. Must be positive.
   */
  public void addURI(String authority, String path, int code){
      .......
  }
  
  //从上面的注解可以知道,addURI就是用于添加一个匹配的URi给URIMatch,第三个参数code就是当uri匹配的时候返回的CODE。然后开发人员通过CODE 来做相应的处理。

所以如果你想要ContentProvider能够接受访问某个表的请求是就要用addURI添加匹配规则,这样就其他程序就可以通过这个ContentProvider来访问该表的数据了。Are you get it?

在上面的代码中的query,delete, insert,update方法中都是利用UriMatch来匹配第一个uri参数,然后根据返回的Code来匹配的对应的操作。这里当我们匹配到是CODE_CONTACT则通过DBHelper来操作Contact表,从这里也可以看出来一个ContentProvider可以对应多个表,多个数据。

  • 在服务端AndroidMainfest.xml文件里面注册自定义的ContentProvider

    ContentProvider也是Android组件之一,所以也需要在AndroidMainfest.xml文件中声明才能使用。声明语句如下:

<provider
          android:name="com.doris.support.provider.ContactProvider"
          android:authorities="com.doris.contact.provider"
          android:exported="true" >
      </provider>

此处的authorities是类似于ContactProvider的访问链接或者是可以理解为一个指针当你在uri组合这个authorities来使用的时候就指向了ContactProviderandroid:exported="true"这句也是很关键的一句,它表明你同意将这个Provider被外部程序访问,如果这个是false则其他应用就不能访问数据,所以一定要是true

经过上面两步,你就可以在其他程序中访问ContactProvider了。至于DBHelper其实就是一个访问数据库的帮助类。

客户端

其实客户端访问ContentProvder是通过ContentResolver来访问的,这个类就是通过URI来跨进程连接ContentProvider的。所以客户端要访问需要通过ContentResolver来请求数据。客户端示例程序如下:

// 考虑到程序的拓展性,多态等特性我们先写一个抽象的类里面对ContentResolver访问数据进行了封装。
public  abstract class BaseContentHelper {
    
    protected Context mContext;
    
    public BaseContentHelper(Context context) {
        this.mContext = context.getApplicationContext();
    }
    
    protected abstract Uri getUri();
    
    public Cursor query(String[] projection, String whereSql, String[] whereValue, String orderBy) {
        ContentResolver contentResolver = mContext.getContentResolver();
        return contentResolver.query(getUri(), projection, whereSql, whereValue, orderBy);
        
    }

    public Uri insert(ContentValues cv) {
        ContentResolver contentResolver = mContext.getContentResolver();
        return contentResolver.insert(getUri(), cv);
    }
    
    public int update(ContentValues cv, String where, String[] selectionArgs) {
        ContentResolver contentResolver = mContext.getContentResolver();
        return contentResolver.update(getUri(), cv, where, selectionArgs);
    }
    
    public int delete(String whereSQL, String[] whereValue) {
        ContentResolver contentResolver = mContext.getContentResolver();
        return contentResolver.delete(getUri(), whereSQL, whereValue);
    }
    
    /**
     * clear all record
     * 
     * @return
     */
    public int delete() {
        ContentResolver contentResolver = mContext.getContentResolver();
        return contentResolver.delete(getUri(), null, null);
    }
}

从上面的代码可以看出,增删查改都是通过ContentResolver以及getUri()传递相关的参数来完成的。
其中getUri()是有子类实现用来提供,子类对应的ContentProvider的访问Uri的。

//我们定义一个ContactManager来获取服务端的数据

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import com.doris.db.BaseContentHelper;
import com.orhanobut.logger.Logger;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

public class ContactManager extends BaseContentHelper {
    //定义ContactProvider的authoritys常量用于拼接URi
    public static final String      AUTHORITYS          = "com.doris.support.provider.ContactProvider";
    public static final String      URI_BASE            = "content://" + AUTHORITYS + "/";
    public static final String      TABLE_NAME          = "contact";
    public static final String      PERSON_NAME         = "name";
    public static final String      PERSON_TELE         = "telephone";
    public static final String      PERSON_DESCRIPTION  = "description";
    private static String           TAG                 = ContactManager.class.getSimpleName();
    private static Uri              mUri                = Uri.parse(URI_BASE + "contact"); //最终拼接出来的URI
    private static ContactManager   mInstance;
    
    private ContactManager(Context ctx) {
        super(ctx);
    }
    
    /**
     * singelten
     * 
     * @return
     */
    public static synchronized ContactManager obtain(Context ctx) {
        
        if (mInstance == null)
            mInstance = new ContactManager(ctx);
        return mInstance;
    }
    
    public List<String> getAllPersonName() {
        List<String> allName = new ArrayList<>();
        Cursor cursor = query(null, null, null, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                Persion vo = parserPersonVoFromCursor(cursor);
                allName.add(vo.getName());
            }
            cursor.close();
        }
        return allName;
    }
    public List<Person> getAllPerson() {
        List<Person> allList = new ArrayList<Person>();
        Cursor cursor = query(null, null, null, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                Person vo = parserPersonVoFromCursor(cursor);
                allList.add(vo);
            }
            cursor.close();
        }
        return allList;
    }
    
    /**
     * CN:根据name获取person列表
    
     * 
     * @param name
     *            eg:doris
     * @return
     */
    public List<Person> getPersonList(String name) {
        List<Person> allList = new ArrayList<Person>();
        Cursor cursor = query(null, PERSON_NAME + "=?", new String[]{name}, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                Person vo = parserPersonVoFromCursor(cursor);
                allList.add(vo);
            }
            cursor.close();
        }
        return allList;
    }
    
 
    
    private Person parserPersonVoFromCursor(Cursor cursor) {
        Person vo = new Person();
        vo.setName(cursor.getString(cursor.getColumnIndex(PERSON_NAME)));
        vo.setTelephone(cursor.getString(cursor.getColumnIndex(PERSON_TELE)));
        vo.setDescription(cursor.getString(cursor.getColumnIndex(PERSON_DESCRIPTION)));
        return vo;
    }
    
      
   /**
   重写父类的getUri 返回uri供ContentResolver使用。
   **/
    
    @Override
    protected Uri getUri() {
        return mUri;
    }
}

上面的BaseContentHelper封装了增删查改,ContactManager通过继承BaseContentHelper封装了一些数据操作的接口,比如通过名字获得Person对象,获取所有的Person列表。

其实上面就把客户端如何请求数据以及服务端如何提供ContentProvider给讲完了,只要你开始用起来,你就会发现其实很简单,只要自定义一个ContentProvider并且在AndroidMainfest.xml文件里注册一下就可以被跨程序访问了。 客户端也只需结合ContentResolver以及Uri便可以访问数据了。 是不是炒鸡简单。一开始我也搞不懂,但是用了几次之后,也知道是怎么回事了。又一次验证了重在实践,所以还没用的快用起来吧。写这篇文章有两个目的: 1.让还不熟悉ContentProvider的你能了解如何使用。 2.给自己留下一个涉猎的足迹,毕竟好记性不如烂笔头,以后有疑惑可以翻出来再看看。

疑问点

  • ContentProvider 何时创建?

    在应用一开始的时候,ContentProvider就由系统创建。在应用中,无需直接为它创建一个实例。

  • 其他程序是如何访问到ContentProvider的?

    通过ContentResolver结合uri来访问,当ContentResolver访问某个uri时,系统会识别uri解析出authoritys来匹配对应ContentProvider. 然后传递请求与参数。

  • 注意,同一个安卓系统中,ContentProviderauthoritys必须是唯一的,其实你很容易就可以知道是不是唯一的,如果你运行一个应用,运行不上的时候,就很有可能是因为authoritys冲突了,不过首先你得看看提示信息,再做判断。

  • 其实ContentProvider不仅可以访问数据库中的数据,可以是调用其他程序中的某个方法然后获取返回值。示例如下:


 //在ContactProvider里面重写 call方法  
        private static final String FUNC_GET_MESSAGE    = "func_get_messge";
        private static final String MESSAGE = "message";
    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        Bundle ret = null;
        if (method.equals(FUNC_GET_MESSAGE)) {
            String message = getMessage();
            ret = new Bundle();
            ret.putString(MESSAGE, message);
        }
        return ret;
    }
    

客户端调用:

//在ContactManager里面添加getMessage方法
    private static final String FUNC_GET_MESSAGE    = "func_get_messge";
        private static final String MESSAGE = "message";
public String getMessage() {
        Bundle ret = mContext.getContentResolver().call(getUri(), FUNC_GET_MESSAGE, null, null);
        ret.setClassLoader(getClass().getClassLoader());
        String list = ret.getString(MESSAGE);
        return list;
    }
    //通过这种方式可以调用另一个应用的某个方法来获取某些数据,不过本质上都是获取一些数据。
  • 合理使用ContentProvider,暴露应该暴露的接口,敏感接口不要暴露给用户。

  • 可以通过notifyChange()来通知数据的改变,这个很有用的,当你通过ContentProvider来访问某个数据时,但是又希望在数据改变的时候监听到,那么就可以通过设置resolver.registerContentObserver来监听数据的改变.

//方法声明如下:第一个是uri是用来匹配要监听的数据的,第二个参数是一个ContentObserver对象通过继承ContentObserver,重写onChange方法来再监听到数据改变的时候做相应的操作。
/**
     * Register an observer class that gets callbacks when data identified by a
     * given content URI changes.
     *
     * @param uri The URI to watch for changes. This can be a specific row URI, or a base URI
     * for a whole class of content.
     * @param notifyForDescendents When false, the observer will be notified whenever a
     * change occurs to the exact URI specified by <code>uri</code> or to one of the
     * URI's ancestors in the path hierarchy.  When true, the observer will also be notified
     * whenever a change occurs to the URI's descendants in the path hierarchy.
     * @param observer The object that receives callbacks when changes occur.
     * @see #unregisterContentObserver
     */
registerContentObserver(@NonNull Uri uri, boolean notifyForDescendents,
            @NonNull ContentObserver observer) ;

实例:

 private class SettingsObserver extends ContentObserver {
        public SettingsObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange, Uri uri) {
            String option = uri.getLastPathSegment();
            Slog.d(TAG, "onChange, option = " + option);
            switch (option) {
            ...........
                }
        }
    }

在安卓的一些自带的应用中,其实很多监听系统属性的变化的时候,都是通过ContentProvider以及监听来做相应的处理的。所以你可以运用到你的应用中。让你的应用更加完美。

许久未更新博客,总觉得自己知道的不够高深,但是想想每一个记录都是为别人为自己积累知识,不管简单还是复杂。所以再一次码起了文字。希望大家多多指教,有不对的提出来,一起学习成长。O(∩_∩)O哈哈~。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容