1. 怎样理解内容提供器
- 内容提供器主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用内容提供器是 Android 实现跨程序共享数据的标准方式(内存映射文件、
AIDL
) - 不同于文件存储和
SharedPreferences
存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证程序中隐私数据不会有泄露的风险
2. Android 6.0 以前权限申请是怎样的,作用是
Android 6.0 以前只需在 AndroidManifest.xml 文件中声明所需的权限即可。作用有两方面
- 如果用户在低于 6.0 系统的设备上安装该程序,会在安装界面有权限提醒,这样用户就可以清楚地知道该程序一共申请了哪些权限,从而决定是否要安装这个程序
- 安装之后,用户还可以随时在程序的设置管理页面查看该程序的权限申请情况
如果认可程序申请的所有权限就会安装该程序,否则拒绝安装。但很多软件都会有“店大欺客”的情况,一些比较敏感的权限它们也会申请,导致我们如果想使用这个软件则不得不妥协,带来了不好的用户体验
3. 怎样理解 6.0 系统中的运行时权限
- 用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中动态地对某一项权限申请进行授权,从而决定是否让该软件得到某种权限进行操作,这样提高了用户体验
- 但对于用户来说,不停地授权也很烦琐。所以,Android 团队将所有的权限分为了两类:一类是普通权限,一类是危险权限
- 授权后,如果用户后悔了,还可以到程序的设置页面手动关闭授予的危险权限
- RxJava 中提供有 RxPermissions 可以简化权限申请等相关操作
4. 怎样理解普通权限和危险权限
- 普通权限:指那些不会直接威胁到用户的安全和隐私的权限。对于这部分权限的申请,系统会自动帮我们进行授权,不需要用户手动操作
- 危险权限:指那些可能会触及用户隐私,或者对设备安全性造成影响的权限。对于这部分权限的申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能
- 危险权限的权限组:每个危险权限都属于一个权限组,用户在进行运行时权限处理时使用的是权限名,但是用户一旦同意授权了,那么该权限对应的权限组中所有的其他权限也会同时被授权
- 数量:Android 6.0 中一共有上百种权限,危险权限一共有 9 组 24 个,剩下的都是普通权限
5. Andorid 6.0 中危险权限有哪些
权限组名 | 权限名 |
---|---|
CALENDAR | READ_CALENDAR / WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS / WRITE_CONTACTS / GET_CONTACTS |
LOCATION | ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE / CALL_PHONE / READ_CALL_LOG / WRITE_CALL_LOG / ADD_VOICEEMAIL / USE_SIP / PROCESS_OUTGOING_CALLS |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS / RECEIVE_SMS / READ_SMS / RECEIVE_WAP_PUSH / RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE |
6. Intent.ACTION_DIAL
和 Intent.ACTION_CALL
这两个隐式 Intent
的 action
的区别是
Intent.ACTION_DIAL
:表示打开拨号界面,不需要声明权限Intent.ACTION_CALL
:是一个系统内置的可以直接拨打电话的动作,必须声明权限,因为可能涉及到手机资费的隐私问题
7. 写一个 Andorid 6.0 系统上申请 CALL_PHONE
危险权限的 Demo
1 | //首先,在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.permission.CALL_PHONE" /> |
8. 内容提供器的用法
- 使用现有的内容提供器来读取和操作相应程序中的数据,核心类是
ContentResolver
- 创建自己的内容提供器给我们程序的数据提供外部访问接口,核心类是
ContentProvider
9. 写一个通过使用现有的内容提供器的方式读取系统电话簿中联系人信息的 Demo
1 | //思路:获取到系统电话簿程序的内容 URI,然后借助 ContentResolver 进行 CRUD(insert、update、delete、query) 增删改查操作 |
10. 给自己的程序创建内容提供器的步骤
- 为自己的应用程序新建一个内容提供器类去继承
ContentProvider
- 全部重写
ContentProvider
类中的 6 个抽象方法public boolean onCreate()
:初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true
表示内容提供器初始化成功,返回false
表示初始化失败。注意,只有当存在ContentResolver
尝试访问我们程序中的数据时,内容提供器才会被初始化public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
:从内容提供器中查询数据。使用Uri
参数来确定查询哪张表,projection
参数用于确定查询哪些列,selection
和selectionArgs
参数用于约束查询哪些行,sortOrder
参数用于对结果进行排序,查询的结果存放在Cursor
对象中返回public Uri insert(Uri uri, ContentValues values)
:向内容提供器中添加一条数据。使用uri
参数来确定要添加到的表,待添加的数据保存在values
参数中。添加完成后,返回一个用于表示这条新记录的URI
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
:更新内容提供器中已有的数据。使用uri
参数来确定更新哪一张表中的数据,新数据保存在values
参数中,selection
和selectionArgs
参数用于约束更新哪些行,受影响的行数将作为返回值返回public int delete(Uri uri, String selection, String[] selectionArgs)
:从内容提供器中删除数据。使用uri
参数来确定删除哪一张表中的数据,selection
和selectionArgs
参数用于约束删除哪些行,被删除的行数将作为返回值返回public String getType(Uri uri)
:根据传入的内容URI
来返回相应的MIME
类型
11. 怎样理解内容 URI
不同于
SQLiteDatabase
,ContentResolver
中的增删改查方法都是不接收表名参数的,而是使用一个 Uri 参数代替,这个参数被称为内容 URI。调用Uri.parse()
方法就可以将内容 URI 字符串解析成Uri
对象内容 URI 给内容提供器中的数据建立了唯一标识符,除了需要在内容 URI 字符串的头部加上协议声明 content,它主要由两部分组成
authority
:用于对不同的应用程序做区分,一般为了避免冲突,都会采用程序包名的方式来进行命名path
:用于对同一应用程序中不同的表做区分,通常都会添加到authrority
后面
还可以在字符串后面加上一个
id
。所以,一个标准的内容 URI 写法是这样的:content://com.example.app.provider/table1/1
:表明调用方期望访问的是com.example.app
这个应用的table1
表中id
为 1 的数据所以,内容 URI 的格式主要就只有两种:以路径结尾就表示期望访问表中所有的数据,以 id 结尾就表示期望访问表中拥有相应 id 的数据。可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下
*
:表示匹配任意长度的字符#
:表示匹配任意长度的数字
所以,一个能够匹配任意表的内容 URI 格式就可以写成:
content://com.example.app.provider/*
;一个能够匹配 table 表中任意一行数据的内容 URI 格式就可以写成:content://com.example.app.provider/table1/#最后,借助
UriMatcher
类就可以实现匹配内容 URI 的功能(具体 Demo 请参考《Android 第二行代码》第 263 页)
12. 怎样理解 getType()
方法
它是所有的内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型
一个内容 URI 所对应的 MIME 字符串主要由 3 部分组成,Andoird 对这 3 个部分做了如下格式规定
- 必须以 vnd 开头
- 如果内容 URI 以路径结尾,则后接
android.cursor.dir/
;如果内容 URI 以 id 结尾,则后接android.cursor.item/
- 最后接上
vnd.<authority>.<path>
所以
- 对于
content://com.example.app.provider/table1
这个内容 URI,它所对应的MIME
类型就可以写成:vnd.android.cursor.dir/vnd.com.example.app.provider.table1
- 对于
content://com.example.app.provider/table1/1
这个内容 URI,它所对应的MIME
类型就可以写成:vnd.android.cursor.item/vnd.com.example.app.provider.table1
- 对于
13. 内容提供器怎样保证数据安全(隐私数据不被泄露)
- 由于内容提供器的机制,所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行,而我们当然不可能向
UriMatcher
中添加隐私数据的 URI(如果没有手残的话) - 所以这部分数据根本无法被外部程序访问到,这样就保证了数据的安全性,即外部可以访问的本程序的数据是我们开发时可控的
14. 写一个通过创建自己的内容提供器的方式给本程序的 SQLiteDatabase
加入外部访问接口的 Demo
自定义
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
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//查询数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[] { categoryId}, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
//添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
break;
default:
break;
}
return updatedRows;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegements().get(1);
deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item./vnd.com.example.databasetest.provider.category";
}
return null;
}
}主程序
MainAcivity
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
68public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
//查询数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if(cursor != null) {
while(cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor, getColumndIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book pages is " + price);
}
cursor.close();
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//更新数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "AStorm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//删除数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
15. 使用 ContentProvider
时需要注意的几个方面
启动性能
ContentProvider
的生命周期默认在Application#onCreate()
之前,而且都是在主线程创建的。自定义的ContentProvider
类的构造函数、静态代码块、onCreate()
函数都尽量不要做耗时操作,否则会拖慢启动速度ContentProvider
还有一个多进程模式,它可以和 AndroidManifest 中的 multiprocess 属性结合使用。这样调用进程会直接在自己进程里创建一个 push 进程的Provider
实例,就不需要跨进程调用了。需要注意的是,这样也会带来Provider
的多实例问题
稳定性
ContentProvider
在进行跨进程数据传递时,利用了 Android 的 Binder 和匿名共享内存机制。简单来说,就是通过Binder
传递CursorWindow
对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的基于 mmap 的匿名共享内存机制也是有代价的。当传输的数据量非常小的时候,可能不一定划算。所以
ContentProvider
提供了一种call()
函数,它会直接通过Binder
来传输数据Android 的 Binder 传输是有大小限制的,一般来说限制是 1~2 MB。
ContentProvider
的接口调用参数和call()
函数调用并没有使用匿名共享机制,比如要批量插入很多数据,那么就会出现一个插入数据的数组,如果这个数组太大了,那么这个操作就可能会出现数据超大异常
安全性
- 虽然
ContentProvider
为应用程序之间的数据共享提供了很好的安全机制,但是如果ContentProvider
是exported
,当支持执行 SQL 语句时就需要注意 SQL 注入的问题。另外,如果传入的参数是一个文件路径,然后返回文件的内容,此时也要校验合法性,不然整个应用的私有数据都有可能被窃取,在Intent
传递参数的时候可能经常会犯这个错误
- 虽然
总结:
ContentProvider
的六要素优缺点