Android中的IPC进程通信方式第四篇

本文系转载文章,阅读原文可获取源码,文章末尾有原文链接

ps:本文讲的是 使用 ContentProvider 进行进程间通信,demo 是用 kotlin 语言写的

1、ContentProvider

ContentProvider 可用于 Android 中不同的应用间进行数据共享,也就是可以进行进程间的通信;ContentProvider 的底层是用 Binder 实现的,它的使用过程要比前面学的 AIDL 简单很多;ContentProvider 分为系统的和自定义的,系统的也就是例如联系人,图片等数据,要想跨进程访问这些信息,只要通过 ContentResolver 的 query、update、insert 和 delete 这些方法就可以了。

google 对 ContentProvider 是这样描述的,内容提供者将一些特定的应用程序数据供给其它应用程序使用;数据可以是存储在文件系统、SQLite 数据库或其它方式,没有其他格式要求;内容提供者继承于 ContentProvider 类,为其它应用程序取用和存储它管理的数据实现了一套标准方法;但是,应用程序并不直接使用这些方法,而是使用一个 ContentResolver 对象,调用它的方法作为替代;ContentResolver 可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。上面这段话简单的概括为:ContentProvider 可以跨进程通信,对数据格式没有要求,实现了一套标准的方法,通过 ContentResolver 访问数据。

一套标准的方法,也就是 ContentProvider 中的 onCreate、query、insert、update、delete 和 getType 方法,除了onCreate 方法由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在 Binder 线程池中;下面对6个方法进行说明:

1)onCreate 方法在创建 ContentProvider 时调用,用于初始化。

2)query(Uri, String[], String, String[], String) 用于查询指定 Uri 的ContentProvider,返回一个Cursor。

3)insert(Uri, ContentValues) 用于添加数据到指定 Uri 的ContentProvider中。

4)update(Uri, ContentValues, String, String[]) 用于更新指定 Uri 的 ContentProvider 中的数据。

5)delete(Uri, String, String[]) 用于从指定 Uri 的 ContentProvider 中删除数据。

6)getType(Uri) 用于返回指定的 Uri 中的数据的 MIME 类型。

下面写一个 demo 演示一下,这次的 demo 是在同一个 APP 里开2个进程进行 ContentProvider 通信,它和用2个 APP 进行 ContentProvider 通信的效果是一样的;

(1)创建一个 kt 类 DbOpenHelper 并继承于 SQLiteOpenHelper:

class DbOpenHelper: SQLiteOpenHelper {

companion object {
    var BOOK_TABLE_NAME: String = "book"
    var DB_NAME: String = "book_provider.db"
    var DB_VERSION: Int = 1
}
var CREATE_BOOK_TABLE: String = "CREATE TABLE IF NOT EXISTS " + BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)"
constructor(context: Context):super(context, DB_NAME, null, DB_VERSION) {
}
override fun onCreate(db: SQLiteDatabase?) {
    db!!.execSQL(CREATE_BOOK_TABLE)
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}

}

(2)新建一个 kt 类 MyContentProvider(包名com.xe.ipcprocess) 并继承于 ContentProvider:

class MyContentProvider: ContentProvider() {

var TAG: String = "MyContentProvider"
var mDb: SQLiteDatabase? = null
var mContext: Context? = null
companion object {
    var URI: String = "content://com.zyb.my_provider"
    var BOOK_URI_CODE: Int = 0
    var sUriMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH);
    init {
        sUriMatcher.addURI(URI,"book",BOOK_URI_CODE)
    }
}

override fun insert(uri: Uri?, values: ContentValues?): Uri {
    Log.d(TAG,"------insert----currentThread------" + Thread.currentThread().getName());
    var table = getTableName(uri!!);
    mDb!!.insert(table,null,values);
    mContext!!.getContentResolver().notifyChange(uri,null);
    return uri;
}

override fun query(uri: Uri?, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor {
    Log.d(TAG,"------query----currentThread------" + Thread.currentThread().getName());
    var table = getTableName(uri!!);
    return mDb!!.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
}

fun initProviderData() {
    mDb = DbOpenHelper(mContext!!).getWritableDatabase();
    Thread() {
        kotlin.run {
            mDb!!.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
            mDb!!.execSQL("insert into book values(3,'Android');");
            mDb!!.execSQL("insert into book values(1,'Ios');");
            mDb!!.execSQL("insert into book values(2,'html');");
        }
    }.start()
}

override fun onCreate(): Boolean {
    Log.d(TAG,"------onCreate----currentThread------" + Thread.currentThread().getName());
    mContext = getContext();
    initProviderData();
    return false;
}

fun getTableName(uri: Uri): String{
    var tableName: String = DbOpenHelper.BOOK_TABLE_NAME
    return tableName
}

override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
    Log.d(TAG,"------update----currentThread------" + Thread.currentThread().getName());
    var table = getTableName(uri!!);
    var row = mDb!!.update(table,values,selection,selectionArgs);
    if (row > 0) {
        getContext().getContentResolver().notifyChange(uri,null);
    }
    return row;
}

override fun delete(uri: Uri?, selection: String?, selectionArgs: Array?): Int {
    Log.d(TAG,"------delete----currentThread------" + Thread.currentThread().getName());
    var table = getTableName(uri!!);
    var count = mDb!!.delete(table,selection,selectionArgs)
    if (count > 0) {
        getContext().getContentResolver().notifyChange(uri,null);
    }
    return count;
}

override fun getType(uri: Uri?): String {
    Log.d(TAG,"------getType----currentThread------" + Thread.currentThread().getName());
    return null!!
}

}

(3)新建一个 kt 类 Book(包名com.xe.ipcdemo4):

class Book{

var mId: Int? = null
var mName: String? = null

override fun toString(): String {
    return "mId = " + mId + ",mName = " + mName
}

}

(4)新建一个 kt 类型的 AppCompatActivity 子类 MainActivity(包名com.xe.ipcdemo4):

class MainActivity: AppCompatActivity() {

var mContentObserver: ContentObserver? = null
companion object {
    var URI: String = "content://com.zyb.my_provider"//com.zyb.provider
    var TAG: String = "MainActivity"
}
var mTv: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    init()
}

fun onClick(v: View) {
    var bookUri: Uri = Uri.parse(URI + "/book");
    var cv: ContentValues = ContentValues();
    cv.put("_id",7);
    cv.put("name","西游记");
    Thread() {
        kotlin.run {
            getContentResolver().insert(bookUri,cv);
        }
    }.start()

    Toast.makeText(this,"插入数据成功", Toast.LENGTH_SHORT).show();
}

fun init() {
    mTv = findViewById(R.id.tv);
    mContentObserver = MyContentObserver(Handler());
    var bookUri: Uri = Uri.parse(URI + "/book");
    var cv: ContentValues = ContentValues();
    cv.put("_id",6);
    cv.put("name","程序设计");
    getContentResolver().insert(bookUri,cv);
    var bookCursor: Cursor = getContentResolver().query(bookUri,arrayOf("_id","name"),null,null,null);
    while (bookCursor.moveToNext()) {
        var book: Book  = Book();
        book.mId = bookCursor.getInt(0)
        book.mName = bookCursor.getString(1)
        Log.d(TAG,"query book:" + book.toString());
    }
    bookCursor.close();
    getContentResolver().registerContentObserver(bookUri,true,mContentObserver);
}


inner class MyContentObserver(h: Handler): ContentObserver(h) {
    var TAG: String = "MyContentObserver"
    var sb: StringBuffer = StringBuffer()
    override fun onChange(selfChange: Boolean) {
        super.onChange(selfChange)
        var bookUri: Uri = Uri.parse(URI + "/book");
        var bookCursor: Cursor = getContentResolver().query(bookUri, arrayOf("_id","name"),null,null,null);
        while (bookCursor.moveToNext()) {
            var book: Book = Book();
            book.mId = bookCursor.getInt(0)
            book.mName = bookCursor.getString(1)
            sb.append(book.toString() + "\n")
            Log.d(TAG,"query book-----------onChange---" + book.toString());
        }
        mTv!!.setText(sb)
        bookCursor.close();
    }
}

override fun onDestroy() {
    super.onDestroy()
    getContentResolver().unregisterContentObserver(mContentObserver)
}

}

(5)新建 MainActivity 对应的布局文件 activity_main.xml:


xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
>

   android:layout_width="match_parent"
   android:text="添加数据"
   android:textAllCaps="false"
   android:onClick="onClick"
   android:layout_height="wrap_content" />

(6)给 AndroidManifest.xml 文件做一下配置:


package="com.xe.ipcdemo4">



    
        
            

            
        
    
    

    

程序一开始运行的主界面如下所示:

图片

点击“添加数据”按钮后,界面变化如下所示:

图片

然后伴随有日志打印,注意将圈出来的地方切换到 “com.xe.ipcprocess:remote”进程中。

图片

总结之前,我们先这里说明一下,客户端 MainActivity 访问另外一个进程服务器端 MyContentProvider 时所用的 URI 要和 AndroidManifest.xml 里配置的 authorities 属性相同,否则访问失败;如果是2个APP进行进程间通信,必须让 provider 配一个 android:exported="true" 属性给外部应用访问;虽然我们只建立了一个表,所以 sUriMatcher.addURI(URI,"book",BOOK_URI_CODE) 这条语句没有任何意义,但如果 ContentProvider 建立多个表的时候,这条语句就起作用了,
我们将 book表指定了Uri,为"content://com.zyb.my_provider/book ”
这个 Uri 所关联的 Uri_Code 是0,将 Uri 和 Uri_Code 关联以后,就可以通过如下方式来获取外界所要访问的数据源,根据 Uri 先取出 Uri_Code,根据 Uri_Code 就可以得到数据表的名称,接下来就可以响应外界的增删改查请求了。

从日志可以看出,ContentProvider 中的 onCreate 方法是运行在主线程的,在本案例中,我们只对数据进行插入和查询,所以验证了 query 和 insert 方法是运行在线程池里的,其他3个方法也是运行在线程池里的,所以不可以在 onCreate 方法里做耗时的操作,由于时间问题,这个就有读者自己去验证了。

在该本例中 MainActivity 中的 ContentObserver 对象,如果客户端对 ContentObserver 对象进行了监听,当服务器端的数据发生改变时,即 getContext().getContentResolver().notifyChange(uri,null) 语句执行时,客户端中的 ContentObserver 对象中的 onChange 方法就会回调,也实现了跨进程回调。

这里需要注意一点 ,query、update、insert、delete 四大方法是存在多线程并发访问的,如果通过多个 SQLiteDatabase 对象来操作数据库就无法保证线程同步,因为 SQLiteDatabase 对象之间不能进行线程同步;如果 ContentProvider 的底层数据集是一块内存的话,例如 ArrayList,这时候 ArrayList 的遍历等操作就需要进行线程同步,不然会引起并发错误。

你可能感兴趣的:(Android中的IPC进程通信方式第四篇)