Content Provider Test过程中遇到的坑
Content Provider(内容提供器)
一、什么是Content Provider?
直接贴官方文档简介图,笔者太懒了,而且 坑 不在这😜
说白了,就是谷歌 对不同应用间的数据库共享 提供的一个安全的机制。(比如我们读取联系人列表,其实就是访问通讯录进程的数据库数据)
二、Content Provider应用角度?
1.使用已经存在的Content Provider,进行访问 其他 进程的 数据,同上的读取联系人列表。
2.创建一个Content Provider,来使自己的APP中的可供共享的数据被其他应用程序访问到。
三、Content Provider 的使用方法:
Content Provider 的使用类似 SQLiteDatabases
首先获取 ContentResolver 对象,可以使用getContentResolver()方法获取此对象,ContentResolver 对象提供了 6 个方法,分别是:
1.onCreat() 2.insert() 3.delete() 4.select() 5.updata() 6.getType()
这6个方法中,除了onCreat()之外的其他5个方法都是我们在调用方需要使用的,其中包括了常用的CRUD(增删查改)。
如果我们要创建自己的Content Provider,就需要创建个实体类去 继承 ContentProvider 类 并 重写这6个方法。
客户端应用进程中的 ContentResolver
对象和拥有提供程序的应用中的 ContentProvider
对象可自动处理跨进程通信。 ContentProvider
还可充当其数据存储区和表格形式的数据外部显示之间的抽象层。
在讲解增删查改方法前,我们还需要了解 Uri
首先 要分析 一个完整的Uri由哪些部分组成
一个完整的Uri由三部分构成,第一部分为协议类型(笔者自己起的名字),官方解释为 :scheme为:content:// 是固定形式
第二部分 Authoritis 对应 某一个具体的应用程序,通常用报名来标识,例如程序所在包为com.example.test.app那么对应的 的Authoritis就是com.example.test.app
第三部分 Path ,对应的是 根据Authoritis指定的应用程序中区分不同的数据,比如要操作com.example.test.app程序中的表book,那么就可以在Authoritis的后面加上/book
也就是说 一个完整的Uri应该满足这种形式:content://com.example.test.app/book
或者也可以这样:content://com.example.test.app/book/11,这样表示要访问操作的是程序app中的book表中的id为11的数据
那么,这是我们引进通配符的概念
* 代表任意长度的任何字符
# 代表任意长度的数字
那么当我们想要访问一个程序中的任意表时:content://这里是程序 Authoritis /*
当我们要访问一个程序中的某个表中的任意一条数据时:content://这里是程序 Authoritis/表名/#
好了,讲了这么多,以query操作为例,演示一下,使用以存在的Content Provider(还是贴官方文档图)
好了,基本的使用 就介绍到这里了,详情见官方文档:https://developer.android.com/guide/topics/providers/content-provider-basics.html?hl=zh-cn
四、我的测试过程遇到的坑
笔者在一个之前做过的SQLiteDatabases程序中,新建了一个MyProvider类继承了ContentProvider类并重写了其6个方法,贴代码
1 package com.example.lichangxin.sqlitetest; 2 3 import android.content.ContentProvider; 4 import android.content.ContentValues; 5 import android.content.UriMatcher; 6 import android.database.Cursor; 7 import android.database.sqlite.SQLiteDatabase; 8 import android.net.Uri; 9 import android.support.annotation.NonNull; 10 import android.support.annotation.Nullable; 11 import android.util.Log; 12 13 public class DatabasesProvider extends ContentProvider { 14 15 16 public static final int Book_DIR = 1; 17 public static final int BOOK_ITEM = 2; 18 19 public static final String AUTHORITY = "com.example.lichangxin.sqlitetest.provider"; 20 public static UriMatcher uriMatcher; 21 public MyDatabases myDatabases; 22 23 static { 24 25 uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 26 uriMatcher.addURI(AUTHORITY,"book",Book_DIR); 27 uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM); 28 29 } 30 31 @Override 32 public boolean onCreate() { 33 //Log.d("enen","oncreate success!"); 34 myDatabases = new MyDatabases(getContext(),"waibu.db",null,2); 35 if(myDatabases!=null){ 36 return true; 37 }else { 38 return false; 39 } 40 } 41 42 @Nullable 43 @Override 44 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 45 46 Log.d("enen","insert 1"); 47 SQLiteDatabase sqLiteDatabase = myDatabases.getWritableDatabase(); // 定位 到这里出现问题 48 Log.d("enen","insert 2"); 49 Uri urireturn = Uri.parse("content://" + AUTHORITY + "/book/" + 12); 50 Log.d("enen","insert 3"); 51 52 switch (uriMatcher.match(uri)){ // switch未执行, 判断 uriMatcher()匹配uri 失败 53 case BOOK_ITEM: 54 case Book_DIR: 55 long return_id = sqLiteDatabase.insert("book",null,values); 56 urireturn = Uri.parse("content://" + AUTHORITY + "/book/" + return_id); 57 Log.d("enen","insert success"); 58 break; 59 case UriMatcher.NO_MATCH: 60 urireturn = Uri.parse("content://" + AUTHORITY + "/book/" + 66); 61 break; 62 default: 63 break; 64 } 65 66 return urireturn; 67 68 } 69 70 @Override 71 public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 72 Log.d("enen","delete"); 73 SQLiteDatabase sqLiteDatabase = myDatabases.getWritableDatabase(); 74 int deleterows = 0; // 删除的行数 用于返回值 75 switch (uriMatcher.match(uri)){ 76 case BOOK_ITEM: 77 deleterows = sqLiteDatabase.delete("book",selection,selectionArgs); 78 break; 79 case Book_DIR: 80 deleterows = sqLiteDatabase.delete("book",selection,selectionArgs); 81 break; 82 default: 83 break; 84 } 85 return deleterows; 86 } 87 88 @Nullable 89 @Override 90 public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { 91 Log.d("enen","query"); 92 SQLiteDatabase sqLiteDatabase = myDatabases.getWritableDatabase(); 93 Cursor cursor = null; 94 switch (uriMatcher.match(uri)){ 95 case BOOK_ITEM: 96 String bookid = uri.getPathSegments().get(1); 97 cursor = sqLiteDatabase.query("book",projection,"id=?",new String[]{bookid},null,null,sortOrder); 98 break; 99 case Book_DIR: 100 cursor = sqLiteDatabase.query("book",projection,selection,selectionArgs,null,null,sortOrder); 101 break; 102 default: 103 break; 104 } 105 return cursor; 106 } 107 108 @Override 109 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { 110 Log.d("enen","updata"); 111 SQLiteDatabase sqLiteDatabase = myDatabases.getReadableDatabase(); 112 int updatarows = 0; 113 switch (uriMatcher.match(uri)){ 114 case BOOK_ITEM: 115 String updataid = uri.getPathSegments().get(1); 116 updatarows = sqLiteDatabase.update("book",values,"id=?",new String[]{updataid}); 117 break; 118 case Book_DIR: 119 updatarows = sqLiteDatabase.update("book",values,selection,selectionArgs); 120 break; 121 default: 122 break; 123 } 124 125 return updatarows; 126 } 127 128 @Nullable 129 @Override 130 public String getType(@NonNull Uri uri) { 131 132 switch (uriMatcher.match(uri)){ 133 case BOOK_ITEM: 134 return "vnd.android.cursor.item/vnd." + AUTHORITY + ".book"; 135 case Book_DIR: 136 return "vnd.android.cursor.dir/vnd." + AUTHORITY + ".book"; 137 default: 138 break; 139 } 140 return null; 141 } 142 }
再贴一份 数据库MyDatabases的,方便整体阅读:
1 package com.example.lichangxin.sqlitetest; 2 3 import android.content.Context; 4 import android.database.sqlite.SQLiteDatabase; 5 import android.database.sqlite.SQLiteOpenHelper; 6 import android.widget.Toast; 7 8 /** 9 * Created by lichangxin on 2017/6/17. 10 */ 11 12 public class MyDatabases extends SQLiteOpenHelper { 13 14 public static final String CREATE_BOOK = "create table book(" 15 + "id integer primary key autoincrement," 16 + "name text," 17 + "author text," 18 + "price real," 19 + "pages integer)"; 20 21 // public static final String CREATE_CATEGORY = "create table Category(" 22 // + "id integer primary key autoincrement," 23 // + "category_name text," 24 // + "enen text," 25 // + "category_code integer)"; 26 27 private Context mcontext; 28 29 public MyDatabases(Context context,String name,SQLiteDatabase.CursorFactory factory,int version){ 30 super(context,name,factory,version); 31 mcontext = context; 32 } 33 34 35 @Override 36 public void onCreate(SQLiteDatabase db) { 37 38 db.execSQL(CREATE_BOOK); // 建表语句, 这样可以保证数据库创建完成时的同时创建Book表 39 // db.execSQL(CREATE_CATEGORY); 40 // Toast.makeText(mcontext,"创建 成功",Toast.LENGTH_SHORT).show(); 41 42 } 43 44 @Override 45 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 46 // db.execSQL("drop table if exists book"); 47 // db.execSQL("drop table if exists Category"); 48 // onCreate(db); 49 } 50 }
完成以上的操作后,还要在AndroidManifest.xml中进行简单的编辑
1 <?xml version="1.0" encoding="utf-8"?> 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 package="com.example.lichangxin.sqlitetest"> 4 5 <application 6 android:allowBackup="true" 7 android:icon="@mipmap/ic_launcher" 8 android:label="@string/app_name" 9 android:roundIcon="@mipmap/ic_launcher_round" 10 android:supportsRtl="true" 11 android:theme="@style/AppTheme"> 12 <activity android:name=".MainActivity"> 13 <intent-filter> 14 <action android:name="android.intent.action.MAIN"/> 15 16 <category android:name="android.intent.category.LAUNCHER"/> 17 </intent-filter> 18 </activity> 19 20 <provider 21 android:name=".DatabasesProvider" 22 android:authorities="com.example.lichangxin.sqlitetest.provider" 23 android:enabled="true" 24 android:exported="true"> 25 </provider> 26 27 </application> 28 29 30 </manifest>
可以看到 我们在<application>标签内 加入了<provider>元素
具体的关于 权限细节 和 <privider>元素 属性,下面贴几张官方文档截图
然后又新建了一个App,以下称此App为Provider_diaoyong;
在这边提供了四个按钮用来对应 操作 提供程序 的增删查改:
1 package com.example.lichangxin.sqlite_provider_test_peihe; 2 3 import android.content.ContentValues; 4 import android.database.Cursor; 5 import android.net.Uri; 6 import android.support.v7.app.AppCompatActivity; 7 import android.os.Bundle; 8 import android.util.Log; 9 import android.view.View; 10 import android.widget.Button; 11 import android.widget.Toast; 12 13 public class MainActivity extends AppCompatActivity implements View.OnClickListener{ 14 15 private String newid = null; 16 17 private static final String ABC = "com.example.lichangxin.sqlitetest.provider"; 18 19 @Override 20 protected void onCreate(Bundle savedInstanceState) { 21 super.onCreate(savedInstanceState); 22 setContentView(R.layout.activity_main); 23 24 Button insert = (Button)findViewById(R.id.insert_button); 25 Button delete = (Button)findViewById(R.id.delete_button); 26 Button query = (Button)findViewById(R.id.query_button); 27 Button updata = (Button)findViewById(R.id.updata_button); 28 29 insert.setOnClickListener(this); 30 delete.setOnClickListener(this); 31 query.setOnClickListener(this); 32 updata.setOnClickListener(this); 33 34 } 35 36 @Override 37 public void onClick(View v) { 38 switch (v.getId()){ 39 case R.id.insert_button: 40 //添加 41 Log.d("enen","insert"); 42 Uri uri = Uri.parse("content://" + ABC + "/book"); 43 ContentValues values = new ContentValues(); 44 values.put("name","外部添加name"); 45 values.put("author","外部添加name"); 46 values.put("pages",999); 47 values.put("price",999); 48 Uri newuri = getContentResolver().insert(uri,values); 49 50 if(newuri!=null){ 51 Log.d("enen","1111"); 52 }else{ 53 Log.d("enen","22222"); //说明 getContentResolver().insert(uri,values); 返回了一个null 54 } 55 56 try{ 57 newid = newuri.getPathSegments().get(1); 58 }catch (NullPointerException e){ 59 Log.d("enen","异常-> " + e.getMessage()); 60 } 61 62 Toast.makeText(this,"当前有" + newid + "个数据",Toast.LENGTH_SHORT).show(); 63 break; 64 case R.id.delete_button: 65 //删除 66 Uri uri1 = Uri.parse("content://" + ABC + "/book/" + newid); 67 getContentResolver().delete(uri1,null,null); 68 break; 69 case R.id.query_button: 70 //查找 71 Uri uri2 = Uri.parse("content://" + ABC + "/book"); 72 Cursor cursor = getContentResolver().query(uri2,null,null,null,null); 73 if(cursor!=null){ 74 while(cursor.moveToNext()){ 75 String name = cursor.getString(cursor.getColumnIndex("name")); 76 String author = cursor.getString(cursor.getColumnIndex("author")); 77 int pages = cursor.getInt(cursor.getColumnIndex("pages")); 78 int price = cursor.getInt(cursor.getColumnIndex("price")); 79 Log.d("输出->name ",name); 80 Log.d("输出->author ",author); 81 Log.d("输出->pages"," " + pages); 82 Log.d("输出->price"," " + price); 83 } 84 cursor.close(); 85 } 86 break; 87 case R.id.updata_button: 88 //修改 89 break; 90 default: 91 break; 92 } 93 } 94 }
因为是做测试 ,所以updata()的onClick事件的方法体没来及写。
好了,现在开始填坑,之前笔者在MyDatabases文件中的onCreat()方法中没有注释掉
Toast.makeText(mcontext,"创建 成功",Toast.LENGTH_SHORT).show();
此行代码,导致当我们点击 调用方的添加按钮时 程序执行到
Uri newuri = getContentResolver().insert(uri,values);
这步,然后会去调用我们 DatabasesProvider文件中的onCreat()方法,在这个onCreat()方法中 程序又会去创建一个 MyDatabases的实例。(如图)
紧接着程序流会转到DatabasesProvider的insert()方法中,在这个方法中 我们通过MyDatabases的实例对象的getWritableDatabases()方法获取到一个具体的数据库实例,在这个过程中 其实 又调用到了MyDatabases的onCreat()方法,贴一张此方法的图。
图中的mcontext就是MainActivity
所以问题就出在这了,如果没有注释掉Toast的使用,那么Toast这条语句 就会 产生运行时异常(说说笔者的猜测,因为笔者想 当我们运行调用方的时候,提供程序所在的app运行在后台程序,那么对应的activity就是在停止状态onstop,那么此时的Toast 去使用 mcontext ,造成了运行时异常)。 勿喷😂
注释掉这行代码之后,程序得以成功运行。
之前是因为 以为 Android Studio 只能运行一个project所以不能同时调试两个app,无意中看到一篇博客,可以 以New Window的形式打开多个project,然后 同时在两个程序中 使用日志输出,逐步定位到Bug所在行。
最后,教一下大家,怎么样 让Android Studio可以同时打开多个project(笔者只尝试过打开两个)。
找到Preferences->Appearance&Behavior->System Settings点进去,在旁边的参数设置窗口找到Project Opening选项
选择Open project in new window。
最后的最后,记录一篇 关于外部存储和内部存储的好文章:http://www.jianshu.com/p/ad844547a43b