Android跨进程通信

1. 前言

  • ContentProvider属于 Android的四大组件之一,即内容提供者。
  • 主要作用是进程间 进行数据交互 & 共享,即跨进程通信

1731726854037.png

  • ContentProvider的底层是采用 Android中的Binder机制

2. ContentProvider的使用

2.1 使用方法

image.png

ContentProvider 是一种用于在应用之间共享数据的组件。实现一个 ContentProvider 时,必须重写以下方法:

onCreate()

  • 这个方法在 ContentProvider 被创建时调用,通常用于初始化数据源。

query()

  • 用于从数据源中查询数据。必须实现这个方法,以便其他应用可以检索数据。

insert()

  • 用于向数据源中插入新数据。必须实现这个方法,以便其他应用可以添加数据。

update()

  • 用于更新数据源中的现有数据。必须实现这个方法,以便其他应用可以修改数据。

delete()

  • 用于从数据源中删除数据。必须实现这个方法,以便其他应用可以删除数据。

getType()

  • 用于返回与特定 URI 相关联的数据类型。此方法不是必需的,但通常需要实现以提供 MIME 类型支持。

2.2 数据传递方式

  • ContentProvider主要以 表格的形式 组织数据
  • 同时也支持文件数据,只是表格形式用得比较多
  • 每个表格中包含多张表,每张表包含行 & 列,分别对应记录 & 字段

2.3 方法实现

  • 进程间共享数据的本质是:添加、删除、获取 & 修改(更新)数据

  • 所以ContentProvider的核心方法也主要是上述4个作用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <-- 4个核心方法 -->
    public Uri insert(Uri uri, ContentValues values)
    // 外部进程向 ContentProvider 中添加数据

    public int delete(Uri uri, String selection, String[] selectionArgs)
    // 外部进程 删除 ContentProvider 中的数据

    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    // 外部进程更新 ContentProvider 中的数据

    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 
    // 外部应用 获取 ContentProvider 中的数据

    // 注:
    // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
    // 2. 存在多线程并发访问,需要实现线程同步
    // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
    // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步

    <-- 2个其他方法 -->
    public boolean onCreate()
    // ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
    // 注:运行在ContentProvider进程的主线程,故不能做耗时操作

    public String getType(Uri uri)
    // 得到数据类型,即返回当前 Url 所代表数据的MIME类型

2.4 示例

例如想要删除一个文件,可以重写其中的delete方法:

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 获取文件路径
String filePath = uri.getPath(); // 假设 URI 的路径是文件的绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
File file = new File(filePath);
int rowsDeleted = 0;

// 检查文件是否存在并删除
if (file.exists()) {
if (file.delete()) {
rowsDeleted = 1; // 成功删除文件
}
}

// 通知数据变化
if (rowsDeleted > 0) {
getContext().getContentResolver().notifyChange(uri, null);
} else {
throw new IllegalArgumentException("File not found: " + uri);
}

return rowsDeleted;
}

3. ContentResolver的使用


3.1 使用方法

ContentProvider用于暴露数据,contentResolver用于操作数据。通过Context的getContentResolver()方法获取实例,通过Uri对指定应用的表进行增删改查

3.2 实现方法

query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

  • 查询数据并返回一个 Cursor

insert(Uri uri, ContentValues values)

  • ContentProvider 中插入新数据,并返回新插入数据的 URI。

update(Uri uri, ContentValues values, String selection, String[] selectionArgs)

  • 更新 ContentProvider 中的数据,并返回受影响的行数。

delete(Uri uri, String selection, String[] selectionArgs)

  • ContentProvider 中删除数据,并返回受影响的行数。

getType(Uri uri)

  • 返回与给定 URI 相关联的数据类型的 MIME 类型。

applyBatch(String authority, ArrayList<ContentProviderOperation> operations)

  • 批量执行操作,这可以提高多个插入、更新或删除操作的效率。

notifyChange(Uri uri, ContentObserver observer)

  • 通知观察者数据发生变化。

registerContentObserver(ContentObserver observer)

  • 注册一个观察者,以便在数据变化时接收通知。

unregisterContentObserver(ContentObserver observer)

  • 注销之前注册的观察者。

3.3 示例

同样的,安装上述的删除文件的方法,我们只需要向其中的uri中传入方法调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import android.content.ContentResolver;
import android.net.Uri;

public void deleteFile(Uri fileUri) {
ContentResolver contentResolver = getContentResolver();

// 调用 delete 方法
int rowsDeleted = contentResolver.delete(fileUri, null, null);

if (rowsDeleted > 0) {
// 删除成功
System.out.println("File deleted successfully.");
} else {
// 删除失败或文件不存在
System.out.println("File deletion failed or file not found.");
}
}

4. 完整示例

4.1 项目描述:

在ContentProvider项目中创建一个SQLite学生表,通过另一个ContentResolver程序项目对ContentProvider中的数据进行读写

4.2 ConstantData

为了在两个项目中统一使用到uri,表名等相同参数的元素,创建一个包含相同数据内容的ConstantData,这个文件在两个项目中都需要申明使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.contentprovide;

import android.content.UriMatcher;
import android.net.Uri;

public class ConstantData {
public static final String AUTHORITY = "jcut.android.contentProvider";
public static final String TABLE_NAME = "student";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);

// Uri 匹配器预定义返回值常量
public static final int STUDENTS = 1;
public static final int STUDENT = 2;

// 数据源字段名
public static final String SID = "sid";
public static final String STU_NO = "stu_no";
public static final String NAME = "name";
public static final String CLAZZ = "clazz";

// Uri 匹配器
public static final UriMatcher uriMatcher;

// 通过静态代码块,初始化一些 Uri 值
static {
// 无匹配时的返回值
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 增加数据集路径
uriMatcher.addURI(AUTHORITY, TABLE_NAME, STUDENTS);
// 增加单条数据路径
uriMatcher.addURI(AUTHORITY, TABLE_NAME + "/#", STUDENT);
}
}

4.3 ContentProvider

在数据提供类中,我们根据上面可知需要完成对数据的CRUD方式,在此之前,我们需要先将学生表(StudentDBHelper)创建:

4.3.1 StudentDBHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.contentprovide;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;


public class StudentDBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "student_db";
private static final String TABLE_NAME = "student";
private static final int DATABASE_VERSION = 2;

public StudentDBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
String strSQL = "create table " + TABLE_NAME
+ "(sid integer primary key autoincrement ,"
+ "stu_no varchar(100),name varchar( 100),"
+ " clazz varchar(100))";
db.execSQL(strSQL);
String sql = "insert into " + TABLE_NAME + " values(null, '202001', '李富贵', '计算机1班')";
db.execSQL(sql);
}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

}
}

4.3.2 StudentDao

在ContentProvider中,我们查询的方法返回的数据需要修改,以达到我们想要输出的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.contentprovide;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class StudentDao {
private SQLiteDatabase db;

public SQLiteDatabase getDB() {
return db;
}

public StudentDao(SQLiteOpenHelper dbHelper) {
db = dbHelper.getWritableDatabase();
}

public void execQuery(StringBuffer resultSB, final String strSQL) {
try {
Cursor cursor = db.rawQuery(strSQL, null);
cursor.moveToFirst();
resultSB.delete(0, resultSB.length());
while (!cursor.isAfterLast()) {
StringBuffer sb = new StringBuffer(" ");
resultSB.append(cursor.getInt(0)).append("\t\t\t\t");
resultSB.append(cursor.getString(1)).append("\t\t\t\t");
resultSB.append(cursor.getString(2)).append("\t\t\t\t");
resultSB.append(cursor.getString(3)).append("\n");
cursor.moveToNext();
}
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}

4.3.3 StudentContentProvider

这个类需要实现ContentProvider,因为ContentResolver6也是基于这个文件中重写的方法进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.example.contentprovide;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class StudentContentProvider extends ContentProvider {
private StudentDBHelper dbOpenHelper = null;

@Override
public boolean onCreate() {
dbOpenHelper = new StudentDBHelper(this.getContext());
return true;
}

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (ConstantData.uriMatcher.match(uri)) {
case ConstantData.STUDENTS:
return db.query(ConstantData.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
default:
throw new IllegalArgumentException("Unknown URl " + uri);
}
}

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

@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (ConstantData.uriMatcher.match(uri)) {
case ConstantData.STUDENTS:
db.insert(ConstantData.TABLE_NAME, null, values);
return null;
default:
throw new IllegalArgumentException("Unknown URl " + uri);
}
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
int count = 0;
switch (ConstantData.uriMatcher.match(uri)) {
case ConstantData.STUDENTS:
count = db.delete(ConstantData.TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URl " + uri);
}
return count;
}

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
switch (ConstantData.uriMatcher.match(uri)) {
case ConstantData.STUDENTS:
return db.update(ConstantData.TABLE_NAME, values, selection, selectionArgs);
default:
throw new IllegalArgumentException("Unknown URl " + uri);
}
}
}

至此,ContentProvider的数据暴露方已经编写完成,Main中只需要对其中的数据进行查询和调用即可

1
2
3
4
5
private void refreshData() {
String sql = "select * from student";
studentDao.execQuery(resultSB, sql);
dataTV.setText(resultSB.toString());
}

4.3.4 服务声明

由于需要对项目中的数据端口暴露,就需要添加相应的权限声明:其中的authorities是在ConstantData中声明需要暴露出的端口号。

1
2
3
4
5
<provider
android:authorities="jcut.android.contentProvider"
android:name=".StudentContentProvider"
android:exported="true"
android:enabled="true"/>

4.4 ContentResolver

对于数据发送方需要进行的操作就简单了,只需将需要的操作提交到暴露出的端口号即可

4.4.1 数据传递

由于传递的数据比较简单,这里就不使用面向对象的方法操作来将对象和方法封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.example.contentresolver;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
Button insert, delete, query;
EditText txtStuNo, txtName, txtClazz;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
findViews();
insert.setOnClickListener(new InsertListener());
query.setOnClickListener(new QueryListener());
delete.setOnClickListener(new DeleteListener());
}

private void findViews() {
insert = findViewById(R.id.insert);
delete = findViewById(R.id.delete);
query = findViewById(R.id.query);
txtStuNo = findViewById(R.id.pdStuNo);
txtName = findViewById(R.id.pdName);
txtClazz = findViewById(R.id.pdClazz);
}

private class InsertListener implements View.OnClickListener {
@Override
public void onClick(View view) {
String stuNo = txtStuNo.getText().toString().trim();
String name = txtName.getText().toString().trim();
String clazz = txtClazz.getText().toString().trim();
if (stuNo.isEmpty() || name.isEmpty() || clazz.isEmpty()) {
Toast.makeText(getApplicationContext(), "请填写完整信息", Toast.LENGTH_SHORT).show();
return;
}

ContentValues cv = new ContentValues();
cv.put(ConstantData.STU_NO, stuNo);
cv.put(ConstantData.NAME, name);
cv.put(ConstantData.CLAZZ, clazz);
ContentResolver cr = getContentResolver();
cr.insert(ConstantData.CONTENT_URI, cv);
txtStuNo.setText("");
txtName.setText("");
txtClazz.setText("");
Toast.makeText(MainActivity.this, "插入成功", Toast.LENGTH_SHORT).show();
}
}

class QueryListener implements View.OnClickListener {
@Override
public void onClick(View view) {
String stuNo = txtStuNo.getText().toString().trim();
String name = txtName.getText().toString().trim();
String clazz = txtClazz.getText().toString().trim();
txtStuNo.setText("");
txtName.setText("");
txtClazz.setText("");

StringBuilder condition = new StringBuilder("1 = 1");
if (!stuNo.isEmpty()) {
condition.append(" AND stu_no LIKE '%").append(stuNo).append("%'");
}
if (!name.isEmpty()) {
condition.append(" AND name LIKE '%").append(name).append("%'");
}
if (!clazz.isEmpty()) {
condition.append(" AND clazz LIKE '%").append(clazz).append("%'");
}

ContentResolver cr = getContentResolver();
Cursor cursor = cr.query(ConstantData.CONTENT_URI, null, condition.toString(), null, null);
if (cursor != null) {
StringBuilder sb = new StringBuilder();
while (cursor.moveToNext()) {
sb.append("\n")
.append(cursor.getInt(0)).append("/")
.append(cursor.getString(1)).append("/")
.append(cursor.getString(2)).append("/")
.append(cursor.getString(3));
}
cursor.close();
Toast.makeText(MainActivity.this, sb.toString(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "没有找到数据", Toast.LENGTH_SHORT).show();
}
}
}

class DeleteListener implements View.OnClickListener {
@Override
public void onClick(View view) {
String stuNo = txtStuNo.getText().toString().trim();
txtStuNo.setText("");
txtName.setText("");
txtClazz.setText("");

if (!stuNo.isEmpty()) {
String condition = "stu_no = " + stuNo;
ContentResolver cr = getContentResolver();
int count = cr.delete(ConstantData.CONTENT_URI, condition, null);
Toast.makeText(MainActivity.this, count + "条数据删除成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "输入要删除的学号", Toast.LENGTH_SHORT).show();
}
}
}
}

5. 注意事项

在ContentResolver中不需要对权限进行声明,只需要将需要传递的数据像网络请求一样发送到对应的端口号就行。

在Android Studio自带的虚拟机中无法达到同时运行两个项目程序,这里推荐使用真机环境进项调试和安装。