0%

Android 详解 ContentProvider

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_DIALIntent.ACTION_CALL 这两个隐式 Intentaction 的区别是

  • Intent.ACTION_DIAL:表示打开拨号界面,不需要声明权限
  • Intent.ACTION_CALL:是一个系统内置的可以直接拨打电话的动作,必须声明权限,因为可能涉及到手机资费的隐私问题

7. 写一个 Andorid 6.0 系统上申请 CALL_PHONE 危险权限的 Demo

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
//首先,在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.permission.CALL_PHONE" />
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { //先判断用户是否已授权
//向用户申请授权,系统会弹出一个权限申请的对话框,不论用户选择同意或者拒绝,最终都会回调到 onRequestPermissionsResult() 方法中,而授权的结果则会封装在 grantResults 参数中
ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manitest.permission.CALL_PHONE }, 1);
} else {
call();
}
}
});
}

private void call() { //直接拨打电话,而不是打开拨号界面
try {
Intent intent = new Intent(Intent.ACTION_CALL); //注意不是 Intent.ACTION_DIAL
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch(SecurityException e) {
e.printStackTrace();
}
}

@Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) { //权限申请回调
switch (requestCode) {
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}

8. 内容提供器的用法

  • 使用现有的内容提供器来读取和操作相应程序中的数据,核心类是 ContentResolver
  • 创建自己的内容提供器给我们程序的数据提供外部访问接口,核心类是 ContentProvider

9. 写一个通过使用现有的内容提供器的方式读取系统电话簿中联系人信息的 Demo

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
//思路:获取到系统电话簿程序的内容 URI,然后借助 ContentResolver 进行 CRUD(insert、update、delete、query) 增删改查操作
//首先,在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.permission.READ_CONTACTS" />
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;

List<String> contactsList = new ArrayList<> ();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String> (this, android.R.layout.simple_list_item_1, contactsList);
contactsView.setAdapter(adapter);
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { //先判断用户是否已授权
//向用户申请授权,系统会弹出一个权限申请的对话框,不论用户选择同意或者拒绝,最终都会回调到 onRequestPermissionsResult() 方法中,而授权的结果则会封装在 grantResults 参数中
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_CONTACTS }, 1);
} else {
readContacts();
}
}

private void readContacts() {
Cursor cursor = null;
try {
//查询联系人数据
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if(cursor != null) {
while(cursor.moveToNext()) { //对 Cursor 对象进行遍历读取数据
//获取联系人姓名
String contactsName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); //已经封装好了内容 URI
//获取联系人手机号
String contactsNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); //已经封装好了内容 URI
contactsList.add(contactsName + "\n" + contactsNumber); //拼接数据
}
adapter.notifyDataSetChanged(); //通知刷新 ListView
}
} catch(Exception e) {
e.printStackTrace();
} finally {
if(cursor != null) {
cursor.close(); //将 Cursor 对象关掉
}
}
}

@Override
public void onRequestPermssionsResult(int requestCode, String[] permissions, int[] grantResults) { //权限申请回调
switch (requestCode) {
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}

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 参数用于确定查询哪些列,selectionselectionArgs 参数用于约束查询哪些行,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 参数中,selectionselectionArgs 参数用于约束更新哪些行,受影响的行数将作为返回值返回
    • public int delete(Uri uri, String selection, String[] selectionArgs)从内容提供器中删除数据。使用 uri 参数来确定删除哪一张表中的数据,selectionselectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回
    • public String getType(Uri uri)根据传入的内容 URI 来返回相应的 MIME 类型

11. 怎样理解内容 URI

  • 不同于 SQLiteDatabaseContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个 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
    139
    public 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
    68
    public 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 生命周期

    • ContentProvider 还有一个多进程模式,它可以和 AndroidManifest 中的 multiprocess 属性结合使用。这样调用进程会直接在自己进程里创建一个 push 进程的 Provider 实例,就不需要跨进程调用了。需要注意的是,这样也会带来 Provider 的多实例问题

  • 稳定性

    • ContentProvider 在进行跨进程数据传递时,利用了 Android 的 Binder匿名共享内存机制。简单来说,就是通过 Binder 传递 CursorWindow 对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的

      ContentProvider 的稳定性

    • 基于 mmap 的匿名共享内存机制也是有代价的。当传输的数据量非常小的时候,可能不一定划算。所以 ContentProvider 提供了一种 call() 函数,它会直接通过 Binder 来传输数据

    • Android 的 Binder 传输是有大小限制的,一般来说限制是 1~2 MBContentProvider 的接口调用参数和 call() 函数调用并没有使用匿名共享机制,比如要批量插入很多数据,那么就会出现一个插入数据的数组,如果这个数组太大了,那么这个操作就可能会出现数据超大异常

  • 安全性

    • 虽然 ContentProvider 为应用程序之间的数据共享提供了很好的安全机制,但是如果 ContentProviderexported,当支持执行 SQL 语句时就需要注意 SQL 注入的问题。另外,如果传入的参数是一个文件路径,然后返回文件的内容,此时也要校验合法性,不然整个应用的私有数据都有可能被窃取,在 Intent 传递参数的时候可能经常会犯这个错误
  • 总结:ContentProvider 的六要素优缺点

ContentProvider 的六要素优缺点

-------------------- 本文结束感谢您的阅读 --------------------