Android——ContentProvider 内容提供者

描述

为了在应用程序之间交换数据,Android提供了ContentProvider,它是不同应用程序之间进行数据交换的标准API,当一个应用程序需要把自己的数据暴露给其他程序使用时,该应用程序就可通过提供ContentProvider来实现;其他应用程序就可通过ContentResolver来操作ContentResolver暴露的数据。

ContentProvider以指定Uri的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用ContentResolver根据Uri去访问操作指定数据。

一旦某个应用程序通过 ContentProvider 暴露了自己的数据操作接口,那么不管该应用程序是否启动,其他应用程序都可通过该接口来操作该应用程序的内部数据,包括增加数据、删除数据、修改数据、查询数据等。

从源码分析ContentProvider的初始化

App进程启动 -> ActivityThread#main() -> ActivityThread#attach() -> ActivityManagerNative#attachApplication() ->
ActivityManagerService#attachApplication() -> ActivityManagerService#generateApplicationProvidersLocked() 。
generateApplicationProvidersLocked()这个方法通过 PackageManager 去获取解析后的应用的清单文件中 provider 信息,为每个 provider 新建 ContentProviderRecord 作为 ActivityManagerService 端的 ContentProvider 表现。

即ContentProvider的 onCreate()方法在ActivityThread#main()运行时间接调用,即ContentProvider是在APP启动的时候就初始化了。运行在主线程,不能做耗时的操作。

ContentProvider应用内数据共享

1、创建数据库类(使用数据库做数据共享)

public class DbHelper extends SQLiteOpenHelper {

    // 数据库名
    private static final String DATABASE_NAME = "pkqup.db";
    // 数据库版本号
    private static final int DATABASE_VERSION = 1;

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String ADDRESS_TABLE_NAME = "address";
    public static final String SPECIAL_CHARACTER = "/#";

    //在构造方法中指定数据库的 数据库名 和 数据库版本号
    public DbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 创建两个表格:用户表 和地址表
        try {
            db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + " (" + User.INDEX   + " INTEGER PRIMARY KEY AUTOINCREMENT," + User.NAME + " TEXT, " + User.USER_ID + " TEXT)");
            db.execSQL("CREATE TABLE IF NOT EXISTS " + ADDRESS_TABLE_NAME + " (" + Address.INDEX   + " INTEGER PRIMARY KEY AUTOINCREMENT,"+ Address.NAME + " TEXT, " + Address.PHONE + " TEXT)");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        try {
            //数据库版本发生变化,删除旧表
            db.execSQL("DROP TABLE IF EXISTS " + USER_TABLE_NAME);
            db.execSQL("DROP TABLE IF EXISTS " + ADDRESS_TABLE_NAME);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2、自定义 ContentProvider 类

public class MyContentProvider extends ContentProvider {

    private DbHelper dbHelper;
    private SQLiteDatabase db;
    private ContentResolver resolver;

    // UriMatcher类使用:在ContentProvider 中注册URI
    private static final UriMatcher uriMatcher;

    // 用户定义列名->数据库列名的映射
    private static final HashMap<String, String> userHashMap;
    private static final HashMap<String, String> addressHashMap;


    // 定义ContentProvider的授权信息,即唯一标识
    public static final String AUTHORITY = "com.pkqup.android.note";

    // 定义Uri匹配返回码
    public static final int USER_CODE = 1;
    public static final int USER_CODE_SINGLE = 2;
    public static final int ADDRESS_CODE = 3;
    public static final int ADDRESS_CODE_SINGLE = 4;

    // 设置URI
    // Uri uri = Uri.parse("content://com.carson.provider/User/1")
    // 上述URI指向的资源是:名为 com.carson.provider 的 ContentProvider  中表名 为`User` 中的 `id`为1的数据

    // 特别注意:URI模式存在匹配通配符* 和 #

    // *:匹配任意长度的任何有效字符的字符串
    // 以下的URI 表示 匹配provider的任何内容
    // content://com.example.app.provider/*

    // #:匹配任意长度的数字字符的字符串
    // 以下的URI 表示 匹配provider中的table表的所有行
    // content://com.example.app.provider/table/#



    // 在静态代码块中初始化 UriMatcher
    static {
        // 初始化 UriMatcher
        // 常量UriMatcher.NO_MATCH ,不匹配任何路径的返回码 即初始化时不匹配任何东西
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

        // 在ContentProvider 中注册URI(即addURI())
        uriMatcher.addURI(AUTHORITY, DbHelper.USER_TABLE_NAME, USER_CODE);
        uriMatcher.addURI(AUTHORITY, DbHelper.USER_TABLE_NAME + DbHelper.SPECIAL_CHARACTER,
                USER_CODE_SINGLE);

        uriMatcher.addURI(AUTHORITY, DbHelper.ADDRESS_TABLE_NAME, ADDRESS_CODE);
        uriMatcher.addURI(AUTHORITY, DbHelper.ADDRESS_TABLE_NAME + DbHelper.SPECIAL_CHARACTER,
                ADDRESS_CODE_SINGLE);
        // 若URI资源路径 = content://com.pkqup.android.note/user ,则返回注册码USER_CODE,
        // 即 mMatcher.match(User.USER_CONTENT_URI)的返回值
        // 若URI资源路径 = content://com.pkqup.android.note/address ,则返回注册码ADDRESS_CODE,
        // 即 mMatcher.match(Address.ADDRESS_CONTENT_URI)的返回值

        userHashMap = new HashMap<>();
        userHashMap.put(User.INDEX, User.INDEX);
        userHashMap.put(User.NAME, User.NAME);
        userHashMap.put(User.USER_ID, User.USER_ID);

        addressHashMap = new HashMap<>();
        addressHashMap.put(Address.INDEX, Address.INDEX);
        addressHashMap.put(Address.NAME, Address.NAME);
        addressHashMap.put(Address.PHONE, Address.PHONE);
    }


    // onCreate()方法在ActivityThread#main()运行时间接调用,即ContentProvider是在APP启动的时候就初始化了。运行在主线程,不能做耗时的操作。
    @Override
    public boolean onCreate() {
        resolver = getContext().getContentResolver();
        dbHelper = new DbHelper(getContext());
        db = dbHelper.getWritableDatabase();
        return true;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }


    // 注:以下增删改成四个方法的说明:
    // 1、下面4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
    // 2、存在多线程并发访问,需要实现线程同步
    // 3、若ContentProvider的数据存储方式是使用SQLite &
    // 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
    // 4、若ContentProvider的数据存储方式是内存,则需要自己实现线程同步


    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Uri newUri;
        switch (uriMatcher.match(uri)) {
            case USER_CODE:
                long user_id = db.insert(DbHelper.USER_TABLE_NAME, "", values);
                if (user_id < 0) {
                    throw new SQLiteException("Unable to insert " + values + " for " + uri);
                }
                newUri = ContentUris.withAppendedId(uri, user_id);
                resolver.notifyChange(newUri, null);
                break;
            case ADDRESS_CODE:
                long property_id = db.insert(DbHelper.ADDRESS_TABLE_NAME, "", values);
                if (property_id < 0) {
                    throw new SQLiteException("Unable to insert " + values + " for " + uri);
                }
                newUri = ContentUris.withAppendedId(uri, property_id);
                resolver.notifyChange(newUri, null);
                break;
            default:
                throw new IllegalArgumentException("Error Uri: " + uri);
        }
        return newUri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count;
        switch (uriMatcher.match(uri)) {
            case USER_CODE:
                count = db.delete(DbHelper.USER_TABLE_NAME, selection, selectionArgs);
                break;
            case USER_CODE_SINGLE:
                String booking_id = uri.getPathSegments().get(1);
                count = db.delete(DbHelper.USER_TABLE_NAME,
                        User.INDEX + "=" + booking_id
                                + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
                        selectionArgs);
                break;

            case ADDRESS_CODE:
                count = db.delete(DbHelper.ADDRESS_TABLE_NAME, selection, selectionArgs);
                break;
            case ADDRESS_CODE_SINGLE:
                String property_id = uri.getPathSegments().get(1);
                count = db.delete(DbHelper.ADDRESS_TABLE_NAME,
                        User.INDEX + "=" + property_id
                                + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
                        selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unnown URI" + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int count;
        switch (uriMatcher.match(uri)) {
            case USER_CODE:
                count = db.update(DbHelper.USER_TABLE_NAME, values, selection, selectionArgs);
                break;
            case USER_CODE_SINGLE:
                String booking_id = uri.getPathSegments().get(1);
                count = db.update(DbHelper.USER_TABLE_NAME, values,
                        User.INDEX + "=" + booking_id
                                + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
                        selectionArgs);
                break;

            case ADDRESS_CODE:
                count = db.update(DbHelper.ADDRESS_TABLE_NAME, values, selection, selectionArgs);
                break;
            case ADDRESS_CODE_SINGLE:
                String property_id = uri.getPathSegments().get(1);
                count = db.update(DbHelper.ADDRESS_TABLE_NAME, values,
                        Address.INDEX + "=" + property_id
                                + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
                        selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unnown URI" + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        Cursor cursor = null;
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String orderBy;
        switch (uriMatcher.match(uri)) {
            case USER_CODE:
                qb.setTables(DbHelper.USER_TABLE_NAME);
                // 用户定义列名->数据库列名的映射
                qb.setProjectionMap(userHashMap);
                if (TextUtils.isEmpty(sortOrder)) {
                    orderBy = User.DEFAULT_SORT_ORDER;
                } else {
                    orderBy = sortOrder;
                }
                cursor = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
                // 用来为Cursor对象注册一个观察数据变化的URI
                cursor.setNotificationUri(getContext().getContentResolver(), uri);
                break;
            case ADDRESS_CODE:
                qb.setTables(DbHelper.ADDRESS_TABLE_NAME);
                // 用户定义列名->数据库列名的映射
                qb.setProjectionMap(addressHashMap);
                if (TextUtils.isEmpty(sortOrder)) {
                    orderBy = Address.DEFAULT_SORT_ORDER;
                } else {
                    orderBy = sortOrder;
                }
                cursor = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
                // 用来为Cursor对象注册一个观察数据变化的URI
                cursor.setNotificationUri(getContext().getContentResolver(), uri);
                break;
        }
        return cursor;
    }
}

3、在AndroidManifest文件中注册创建的 ContentProvider类


   <!-- 声明访问ContentProvider的权限-->
   <permission
       android:name="com.android.pkqup.androidnote.PROVIDER"
       android:protectionLevel="normal" />


   <provider
           android:name=".content_provider_test.MyContentProvider"
           //定义授权信息
           android:authorities="com.pkqup.android.note"
           //设置外部应用可访问
           android:exported="true"
          //设置自定义权限
android:permission="com.android.pkqup.androidnote.PROVIDER" />

4、进程内访问 ContentProvider的数据
4.1定义实体类

public class User {

    public static final String INDEX = "_index";// 主键
    public static final String NAME = "name";
    public static final String USER_ID = "userId";


    // 定义 user 表的 uri
    public static final Uri USER_CONTENT_URI =
            Uri.parse("content://" + MyContentProvider.AUTHORITY + "/" + DbHelper.USER_TABLE_NAME);

    // 定义 user 表的 uri
    public static final Uri USER_CONTENT_URI_SINGLE =
            Uri.parse("content://" + MyContentProvider.AUTHORITY + "/" + DbHelper.USER_TABLE_NAME
                    + DbHelper.SPECIAL_CHARACTER);

    // 排序方式,定义为主键排序
    public static final String DEFAULT_SORT_ORDER = INDEX;

    private String name;
    private String userId;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

4.2、定义ContentProvider数据库操作类

public class UserUtils {

    private Context context;

    public UserUtils(Context context) {
        this.context = context;
    }

    public void insertUser(String name, String age) {
        ContentValues values = new ContentValues();
        values.put(User.NAME, name);
        values.put(User.USER_ID, age);
        context.getContentResolver().insert(User.USER_CONTENT_URI, values);
    }

    public void deleteUser() {
        context.getContentResolver().delete(User.USER_CONTENT_URI, null, null);
    }

    public void updateUser(String name, String age) {
        ContentValues values = new ContentValues();
        values.put(User.NAME, name);
        values.put(User.USER_ID, age);
        context.getContentResolver().update(User.USER_CONTENT_URI, values, null, null);
    }

    public User queryUser() {
        User user = new User();
        Cursor mCursor = context.getContentResolver().query(User.USER_CONTENT_URI, null, null, null,
                User.DEFAULT_SORT_ORDER);
        if (null != mCursor) {
            while (mCursor.moveToNext()) {
                String name = mCursor.getString(mCursor.getColumnIndexOrThrow(User.NAME));
                String age = mCursor.getString(mCursor.getColumnIndexOrThrow(User.USER_ID));
                user.setName(name);
                user.setUserId(age);
            }
            mCursor.close();
        }
        return user;
    }


    // 数据库查询方法的参数说明
    public void query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        // projection表示要查询哪些列,比如查询媒体库,可能要关注音乐的艺术家,长度,文件位置等。
        // selection表示查询条件,就是查询里面的where语句,
        // selectionArgs是查询条件的值。
        // sortOrder是排序方式。
    }

    // 根据id查询名称
    public String getUserNameFrom(String userId) {
        String userName = "";
        Cursor mCursor = context.getContentResolver().query(User.USER_CONTENT_URI,
                new String[] {User.NAME, User.USER_ID}, "userId=?", new String[] {userId},
                User.DEFAULT_SORT_ORDER);
        if (null != mCursor) {
            while (mCursor.moveToNext()) {
                userName = mCursor.getString(mCursor.getColumnIndexOrThrow(User.NAME));
            }
            mCursor.close();
        }
        return userName;
    }
}

进程间数据共享

1、创建数据库类
见 DbHelper 类。同上。

2、自定义 ContentProvider 类
见 MyContentProvider 类。同上。

3、在AndroidManifest文件中注册创建的 ContentProvider类,注意配置权限和外部可使用属性。
同上。

4、在外部应用中也需要配置相同的权限

<!--自定义上个应用ContentProvider需要的权限-->
<permission
     android:name="com.android.pkqup.androidnote.PROVIDER"
     android:protectionLevel="normal" />

<!--使用该权限-->
<uses-permission android:name="com.android.pkqup.androidnote.PROVIDER"/>

5、外部应用通过提供的Uri 对ContentProvider进行增删改查。
见 UserUtils 类。同上。

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

推荐阅读更多精彩内容