Android 如何自定义一个ContentProvider

本文详细介绍了如何在Android中自定义ContentProvider,包括实现步骤、uri注册和访问过程。通过ContentProvider,可以实现应用间的数据共享,其核心机制基于Binder。文章还探讨了ContentResolver的CUDR操作和数据变更监听,以及进程间通信的其他方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    一,写在前面 

       我们知道Android有四大组件,ContentProvider是其中之一,顾名思义:内容提供者。什么是内容提供者呢?一个抽象类,可以暴露应用的数据给其他应用。应用里的数据通常说的是数据库,事实上普通的文件,甚至是内存中的对象,也可以作为内容提供者暴露的数据形式。为什么要使用内容提供者呢?从上面定义就知道,内容提供者可以实现应用间的数据访问,一般是暴露表格形式的数据库中的数据。内容提供者的实现机制是什么呢?由于是实现应用间的数据通信,自然也是两个进程间的通信,其内部实现机制是Binder机制。那么,内容提供者也是实现进程间通信的一种方式。

         

        事实上在开发中,很少需要自己写一个ContentProvider,一般都是去访问其他应用的ContentProvider。本篇文章之所以去研究如何自己写一个ContentProvider,也是为了更好的在开发中理解:如何访问其他应用的内容提供者。

二,实现一个ContentProvider

接下来介绍如何自己去实现一个内容提供者,大致分三步进行:

        1,继承抽象类ContentProvider,重写onCreate,CUDR,getType六个方法;

        2,注册可以访问内容提供者的uri

        3,清单文件中配置provider

        第一步,onCreate()方法中,获取SQLiteDatabase对象;CUDR方法通过对uri进行判断,做相应的增删改查数据的操作;getType方法是返回uri对应的MIME类型。

        第二步,创建静态代码块,static{...code},在类加载的时候注册可以访问内容提供者的uri,使用类UriMatcher的addURI(...)完成。

        第三步,注册内容提供者,加入authorities属性,对外暴露该应用的内容提供者。


直接上代码,应用B的MyContentProvider,如下:

public class MyContentProvider extends ContentProvider {
	private DbOpenHelper helper;
	private SQLiteDatabase db;
	private static UriMatcher uriMatcher;
	public static final String AUTHORITY = "com.example.mycontentprovider.wang";
	public static final int CODE_PERSON = 0;
	static {
		uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		uriMatcher.addURI(AUTHORITY, "person", CODE_PERSON);
	}
	
	
	@Override
	public boolean onCreate() {
		helper = DbOpenHelper.getInstance(getContext());
		db = helper.getWritableDatabase();
		//在数据库里添加一些数据
		initData();
		return true;
	}
	
	public void initData() {
		for (int i = 0; i < 5; i++) {
			ContentValues values = new ContentValues();
			values.put("name", "kobe" + (i + 1));
			values.put("age", 21 + i);
			db.insert("person", null, values);
		}
	}
	
	@Override
	public String getType(Uri uri) {
		return null;
	}

	public String getTableName(Uri uri) {
		if (uriMatcher.match(uri) == CODE_PERSON) {
			return "person";
		} else {
			//...
		}
		return null;
	}
	
	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		String tableName = getTableName(uri);
		if (tableName == null) {
			throw new IllegalArgumentException("uri has not been added by urimatcher");
		}
		Cursor cursor = db.query(tableName, projection, selection, selectionArgs, null, null, null);
		return cursor;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		String tableName = getTableName(uri);
		if (tableName == null) {
			throw new IllegalArgumentException("uri has not been added by urimatcher");
		}
		db.insert(tableName, null, values);
		
		//数据库中数据发生改变时,调用
		getContext().getContentResolver().notifyChange(uri, null);
		return uri;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		String tableName = getTableName(uri);
		if (tableName == null) {
			throw new IllegalArgumentException("uri has not been added by urimatcher");
		}
		int row = db.delete(tableName, selection, selectionArgs);
		if (row > 0) {
			getContext().getContentResolver().notifyChange(uri, null);
		}
		
		return row;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		String tableName = getTableName(uri);
		if (tableName == null) {
			throw new IllegalArgumentException("uri has not been added by urimatcher");
		}
		int row = db.update(tableName, values, selection, selectionArgs);
		if (row > 0) {
			getContext().getContentResolver().notifyChange(uri, null);
		}
		return row;
	}

}
DbOpenHelper代码如下:

public class DbOpenHelper extends SQLiteOpenHelper {

	public DbOpenHelper(Context context, String name, CursorFactory factory,
			int version) {
		super(context, name, factory, version);
	}

	private static DbOpenHelper helper;
	public static synchronized DbOpenHelper getInstance(Context context) {
		if (helper == null) {
			//创建数据库
			helper = new DbOpenHelper(context, "my_provider.db", null, 1);
		}
		return helper;
	}
	
	//创建表
	@Override
	public void onCreate(SQLiteDatabase db) {
		String sql = "create table person (_id integer primary key autoincrement, name Text, age integer)";
		db.execSQL(sql);
	}

	//数据库升级时,回调该方法
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

	}

}
          在MyContentProvider$onCreate方法中,通过一个抽象帮助类SQLiteOpenHelper的子类实例,调用getWritableDatabase()获取SQLiteDatabase实例。先简单介绍下SQLiteOpenHelper,DbOpenHelper中我们提供一个getInstance的方法,用于获得SQLiteOpenHelper的一个子类实例,并采用单例设计模式;onCreate方法:创建数据库的表,且可以创建多个表;onUpgrade方法:在数据库版本发生改变时,该方法被回调,可以加入修改表的操作的代码。在MyContentProvider$onCreate方法中获取了SQLiteDatabase实例就可以操作数据库,下面分析第二步的注册uri。

    

       注册uri的目的就是确定哪些URI可以访问应用的数据,通常这些uri是由其他应用传递过来的,在后面访问uri的模块中会有所了解。UriMatcher可以用于注册uri,看起来就像一个容器,可以存储uri,还可以判断容器中是否有某一个uri。事实上,UriMatcher内部维护了一个ArrayList集合。查看UriMatcher的构造函数,代码如下:

public UriMatcher(int code)
    {
        mCode = code;
        mWhich = -1;
        mChildren = new ArrayList<UriMatcher>();
        mText = null;
    }

        由此可见UriMatcher并不是一个什么陌生的东西,就是学习Java时接触到的ArrayList集合,只是将添加uri,判断uri的操作做了相应的封装。addURI(String authority,String path, int code),authority,path后面会讲到;code:与uri一一对应的int值,后面在判断uri是否添加到UriMatcher时,是先将该uri转化为code,再进行判断。

       

         接下里分析CUDR操作,我们重写了这样四个方法:query,insert,delete,update,这个四个方法的参数都是想访问该应用的其他用户传递过来的,重点看uri。那么这个uri是如何构成的呢?uri = scheme + authorities + path。先看这样一个uri,

uri = "content://com.example.mycontentprovider.wang/a/b/c",

        scheme:"content://";

        authorities:com.example.mycontentprovider.wang;authorities就是在清单文件中配置的authorities属性的值,唯一标识该应用的内容提供者。

        path:/a/b/c;path里面常常放的是一些表名,字段信息,确定访问该数据库中哪个表的哪些数据,具体是访问哪些数据还要看CUDR对该uri做了怎样的操作。

          

        在getTableName方法中,我们调用uriMatcher.match(uri)获取uri对应的code,如果该code没有注册过,则抛出异常IllegalArgumentException。也就是说,在其他应用访问本应用的内容提供者时,如果uri“不合法”,那么会抛出IllegalArgumentException异常。

        然后调用SQLiteDatabase的query,insert,delete,update四个方法进行增删改查数据,值得一提的是,在增加,删除,修改数据后,需要调用内容解决者ContentResolver的notifyChange(uri,observer),通知数据发生改变。getType方法返回uri请求文件的MIME类型,这里返回null;


          清单文件中注册provider代码如下:

<provider 
            android:name="com.example.mycontentprovider.provider.MyContentProvider"
            android:authorities="com.example.mycontentprovider.wang"
            android:exported="true" >
        </provider>
       authorities(也称,授权)属性必须指定相应的值,唯一标识该内容提供者,每个内容提供者的authorities的值都不同,它是访问的uri的一部分。

       exported属性:若没有intent-filter,则默认false,不可访问;若有intent-filter,则默认true,可以访问。亦可手动设置

       还可以添加权限属性,有兴趣的哥们可以自己去研究。以上就是自己写一个内容提供者的过程,分三步完成。下面展示另一个应用A,如何访问该应用的ContentProvider。

三,访问ContentProvider

应用A的代码,xml布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

	<Button 
	    android:id="@+id/btn_add"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:text="添加一条name为Tom,age为21的数据"/>
	<Button 
	    android:id="@+id/btn_delete"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:text="删除name为Tom的数据"/>
	<Button 
	    android:id="@+id/btn_update"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:text="更改最后一条数据的name为paul"/>
	<Button 
	    android:id="@+id/btn_query"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:text="查询所有数据"/>
	
</LinearLayout>

实体类Person代码如下:

package com.example.mcontentprovider.domain;

public class Person {
	public int _id;
	public String name;
	public int age;
	public Person() {
		super();
	}

	public Person(int _id, String name, int age) {
		super();
		this._id = _id;
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}

MainActivity代码如下:

public class MainActivity extends Activity implements OnClickListener {

	private Button btn_add;
	private Button btn_deleteAll;
	private Button btn_query;
	private Button btn_update;
	private ContentResolver cr;
	private static final String AUTHORITIES = "com.example.mycontentprovider.wang";
	private MyContentObserver observer;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		cr = getContentResolver();
		observer = new MyContentObserver(new Handler());
		cr.registerContentObserver(Uri.parse(uri), false, observer);
		initView();
	}
	
	public void initView() {
		btn_add = (Button) findViewById(R.id.btn_add);
		btn_deleteAll = (Button) findViewById(R.id.btn_delete);
		btn_query = (Button) findViewById(R.id.btn_query);
		btn_update = (Button) findViewById(R.id.btn_update);
		
		btn_add.setOnClickListener(this);
		btn_deleteAll.setOnClickListener(this);
		btn_query.setOnClickListener(this);
		btn_update.setOnClickListener(this);
	}

	private String uri = "content://" + AUTHORITIES + "/person";
	@Override
	public void onClick(View v) {
		switch (v.getId()) {
		case R.id.btn_add:
			new Thread(){
				public void run() {
					//休眠3秒,模拟异步任务
					SystemClock.sleep(3000);
					add();
				};
			}.start();
			break;
		case R.id.btn_delete:
			Log.e("MainActivity", "删除名字为Tom的数据");
			cr.delete(Uri.parse(uri), "name = ?", new String[]{"Tom"});
			break;
		case R.id.btn_query:
			Cursor cursor = cr.query(Uri.parse(uri), null, null, null, null);
			ArrayList<Person> persons = new ArrayList<Person>();
			while (cursor.moveToNext()) {
				int _id = cursor.getInt(0);
				String name = cursor.getString(1);
				int age = cursor.getInt(2);
				persons.add(new Person(_id, name, age));
			}
			Log.e("MainActivity", persons.toString());
			break;
		case R.id.btn_update:
			Log.e("MainActivity", "更改最后一条数据的name为paul");
			ContentValues values2 = new ContentValues();
			values2.put("name", "paul");
			//获取数据库的行数
			Cursor cursor2 = cr.query(Uri.parse(uri), null, null, null, null);
			int count = cursor2.getCount();
			cr.update(Uri.parse(uri), values2, "_id = ?", new String[]{count + ""});
			break;

		default:
			break;
		}
	}

	private void add() {
		Log.e("MainActivity", "添加一条name为Tom,age为21的数据");
		ContentValues values = new ContentValues();
		values.put("name", "Tom");
		values.put("age", 21);
		cr.insert(Uri.parse(uri), values);
	}
	
	private class MyContentObserver extends ContentObserver {
		public MyContentObserver(Handler handler) {
			super(handler);
		}
		
		@Override
		public void onChange(boolean selfChange) {
			Toast.makeText(getApplicationContext(), "数据改变啦!!!", 0).show();
			super.onChange(selfChange);
		}
	}

}

        在应用A中,我们设定uri = "content://" + AUTHORITIES + "/person",增删改查的操作对应都是该uri。事实上,只要内容提供者注册了的uri都可以访问,这里暂且让uri都相同。有兴趣的哥们可以尝试一下,若uri不合法,确实会抛出IllegalArgumentException异常。在实际开发中,最重要的是寻找到需要的uri,然后进行CUDR操作,如何进行CUDR操作不是本篇重点,不做讲解。

       注意到代码里添加数据时,这里创建了一个线程,使线程休眠了3s,用于模拟添加大量数据时的异步操作。同时注册了一个内容观察者用于监听数据变化,cr.registerContentObserver(Uri.parse(uri), false, observer)。第一个参数:监听的uri。第二个参数:若为true,表示以该uri字串为开头的uri都可以监听;若为false,表示只能监听该uri。第三个参数:ContentObserver子类实例,数据发生改变时回调onChange方法。

       执行点击操作,查看log。

       查询;

       添加->查询;(在点击添加按钮后,过了3秒左右,弹出toast,显示"数据改变啦!!!")

       删除->查询;

       更改->查询;

       log如下:


         这里解释下,在添加数据时,为何模拟异步操作。有这样一个场景:当数据添加进内容提供者的数据库中后,才可以执行某一个操作。那么onChange方法被回调时,就是一个很好的时机去执行某一个操作。

        可能有的哥们要问:在应用A中调用了ContentResolver的CUDR方法,那么怎么应用B中数据库的数据为何能变化呢?表面上可以这样理解:应用A在调用ContentResolver的CUDR方法时,会使应用B中对应的CUDR方法被调用,而uri则是应用A传递给应用B的。而为何“会使应用B中对应的CUDR方法被调用”,但是是Binder机制实现的。包括被回调的onChange方法也是Binder机制才能实现的,试想数据增删改查操作是在应用B完成的,为何在应用B中调用notifyChange方法通知数据改变后,应用A的onChange方法能被回调。

        侃了这么多,拿代码来点一下,查看ContentResolver$notifyChange源码如下:

public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork,
            int userHandle) {
        try {
            getContentService().notifyChange(
                    uri, observer == null ? null : observer.getContentObserver(),
                    observer != null && observer.deliverSelfNotifications(), syncToNetwork,
                    userHandle);
        } catch (RemoteException e) {
        }
    }
继续查看ContentResolver$getContentService方法:

public static IContentService getContentService() {
        if (sContentService != null) {
            return sContentService;
        }
        IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
        if (false) Log.v("ContentService", "default service binder = " + b);
        sContentService = IContentService.Stub.asInterface(b);
        if (false) Log.v("ContentService", "default service = " + sContentService);
        return sContentService;
    }
         sContentService不就是代理对象么,调用代理对象的notifyChange(...)方法:内部会调用transact方法向服务发起请求;然后onTransact(...)被调用,会调用IContentService接口的notifyChange方法完成通信。接口IContentService中方法的重写是在extends IContentService.Stub的类中,也就是ContentService。

四,另外

        好了,上面只是简单点了一下,说明ContentProvider暴露数据给其他应用访问,内部就是Binder机制原理实现的。常用进程间通信方式有:AIDL,ContentProvider,Messenger等。


       这篇文章就分享到这里啦,有疑问可以留言,亦可纠错,亦可补充,互相学习...^_^




          

   

         



 



       

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值