Android开发之内容解析者ContentResolver
在Android开发中有所谓四大组件之说即Activity界面 BroadcastRsolver广播 Service服务 ContentProvider内容提供者
/****
* ContentProvider内容提供者
* ContentObserver内容观察者
* ContentResolver就是来取ContentProvider提供的数据的。
* ContentProvider使你数据库中数据能够被其他程序访问,但能访问不能任意方式都能访问,
* 只能通过规定的方式,这种方式就是通过ContentResolver来实现
*内容观察者,观察内容提供者数据的变化。如果内容提供者数据变化了,那么发送信息给观察者。
*原理:在resolver身上注册一个观察者observer,当数据改变时,调用观察者的onChange方法
*在provider的数据会发生改变的方法中调用resolver的notifyChange方法。说白了ContentObserver就是一个能够动态监听数据的监听器
*/
以下是为什么要使用ContentObserver而不是自己写监听去监测数据变化
我们知道,在db 做insert、delete等操作的时候,db会改变,这个时候UI 可能是需要更新的,那怎么才能知道db 是有了变化呢?不能做个监听一直查询db是否变化吧?这样就太废精力了,Android 中提供了ContentObserver来作为db 数据变化后的callback。
部分内容取材于私房菜的博客,下边是私房菜的博客地址
http://blog.csdn.net/shift_wwx/article/details/48782367
这哥们讲的很专业,我也没必要在这里装逼了,直接看他的,,哈哈哈哈
在android 中的 ContentObserver (一) 中,提到如果一个db 发生了变化,用户需要最快的知晓。可以做个监听器一直查询db的变化状态,这里需要更正一下,这个不是个好方法,也最好不要想,对于数据表小一点的话,还是可以考虑,可是对于大型的数据表或者字段很多的情况下,这个监听器是没办法做的。
所以,剩下有两种选择,一是选择ContentObserver,另一个选择是广播。
对于两者的区别,大概可以列出下面几点:
一、前者是通过URI来把通知的发送者和接收者关联在一起的,而后者是通过Intent来关联的
二、前者的通知注册中心是由ContentService服务来扮演的,而后者是由ActivityManagerService服务来扮演的
三、前者负责接收数据更新通知的类必须要继承ContentObserver类,而后者要继承BroadcastReceiver类。
从上面这些区别看,两者完全可以做成一个,目前也就是实现的地方不一样而已,其实之所以会有这些区别,主要是因为第四点。
四、Content Proivder组件的数据共享功能本身就是建立在URI的基础之上的,因此专门针对URI来设计另外一套通知机制会更实用和方便,而Android系统的广播机制是一种更加通用的事件通知机制,它的适用范围会更广泛一些。
当然,如果不愿意用 ContentObserver,用广播也是可以,可以将uri 以 param 的形式传递上来。(通过intent传递参数)
本片博文主要讲述如何通过ContentResolver拿到内容提供者的数据并予以展示,访问的都是系统的数据库,拿到通话记录,短息,,联系人
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog;
import android.support.v7.app.AppCompatActivity;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
public class MainActivity extends AppCompatActivity {private ListView listView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);listView = (ListView) findViewById(R.id.listView);// 获得ContentResolver对象ContentResolver resolver = getContentResolver();// 构造访问通话记录的Uri// 方式一:直接通过字符串创建//Uri callLogUri = Uri.parse("content://call_log/calls");// 方式二:使用对应合约类中的常量Uri callLogUri = CallLog.Calls.CONTENT_URI;// 查询获得结果 参数为查询条件
// public final Cursor query(Uri uri, String[] projection,
// String selection, String[] selectionArgs, String sortOrder) {
// return query(uri, projection, selection, selectionArgs, sortOrder, null);
// }Cursor cursor = resolver.query(callLogUri, // Urinew String[] { "_id", "number", "date", "type" }, // 返回结果所要包含的列明数组null, null, // 共同指定了查询条件,即WHERE子句null); // 排序条件,即ORDER BY子句SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,R.layout.item_listview_main,cursor,new String[] { "number", "date", "type" },new int[] {R.id.text_item_number, R.id.text_item_date, R.id.text_item_type},CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);listView.setAdapter(adapter);}
}
item布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent" ><TextView
android:id="@+id/text_item_number"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_alignParentTop="true"android:textSize="26sp"android:textColor="#00f"android:text="TextView" /><TextView
android:id="@+id/text_item_type"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_alignParentTop="true"android:text="TextView" /><TextView
android:id="@+id/text_item_date"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_below="@+id/text_item_number"android:textColor="#ddd"android:text="TextView" /></RelativeLayout>
//别忘了权限
<uses-permission android:name="android.permission.READ_CALL_LOG" />
你会发现好多数据显示的格式都看不懂 ,那是系统数据库保存的格式,我们可以自定义一个BaseAdapter对数据进行一些转换
package com.longlian.contentobserverdemo;import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;public class MainActivity extends AppCompatActivity {private ListView listView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);listView = (ListView) findViewById(R.id.listView_main_smslist);// 获得ContentResolverContentResolver resolver = getContentResolver();// 构建访问消息的UriUri smsUri = Uri.parse("content://sms");// 查询获得结果Cursor cursor = resolver.query(smsUri,new String[] { "_id", "address", "date", "body", "type" },null, null,null);SmsAdapter adapter = new SmsAdapter(this, cursor);listView.setAdapter(adapter);}class SmsAdapter extends BaseAdapter {private Cursor cursor;private LayoutInflater inflater;public SmsAdapter(Context context, Cursor cursor) {this.cursor = cursor;inflater = LayoutInflater.from(context);}@Overridepublic int getCount() {return cursor.getCount();}@Overridepublic Object getItem(int position) {// TODO Auto-generated method stubreturn null;}@Overridepublic long getItemId(int position) {cursor.moveToPosition(position);return cursor.getLong(cursor.getColumnIndex("_id"));}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder h = null;if (convertView == null) {convertView = inflater.inflate(R.layout.item_listview_main, null);h = new ViewHolder();h.addrText = (TextView) convertView.findViewById(R.id.text_item_address);h.bodyText = (TextView) convertView.findViewById(R.id.text_item_body);h.dateText = (TextView) convertView.findViewById(R.id.text_item_date);h.typeText = (TextView) convertView.findViewById(R.id.text_item_type);convertView.setTag(h);} else {h = (ViewHolder) convertView.getTag();}// 绑定数据cursor.moveToPosition(position);h.addrText.setText(cursor.getString(cursor.getColumnIndex("address")));h.bodyText.setText(cursor.getString(cursor.getColumnIndex("body")));h.dateText.setText(getDateString(cursor.getLong(cursor.getColumnIndex("date"))));h.typeText.setText(getSmsType(cursor.getInt(cursor.getColumnIndex("type"))));return convertView;}/*** 将13位时间戳代表的日期转换为常见的可读格式* @param time* @return*/private String getDateString(long time) {Date date = new Date(time);SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);return format.format(date);}/*** 将整数代表的消息类型转换为可读字符串* @param type* @return*/private String getSmsType(int type) {String str;switch(type) {case 1:str = "收到";break;case 2:str = "发出";break;case 3:str = "草稿";break;default:str = "其他";break;}return str;}class ViewHolder {TextView addrText;TextView bodyText;TextView dateText;TextView typeText;}}}
item布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent" ><TextView
android:id="@+id/text_item_address"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_alignParentTop="true"android:textSize="26sp"android:textColor="#00f"android:text="TextView" /><TextView
android:id="@+id/text_item_type"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_alignParentTop="true"android:text="TextView" /><TextView
android:id="@+id/text_item_body"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_below="@+id/text_item_address"android:layout_marginTop="5dp"android:text="TextView" /><TextView
android:id="@+id/text_item_date"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignBaseline="@+id/text_item_body"android:layout_alignBottom="@+id/text_item_body"android:layout_alignParentRight="true"android:textColor="#f00"android:text="TextView" /></RelativeLayout>
//还有读取手机短信的权限
<uses-permission android:name="android.permission.READ_SMS" />
//上边说ContentObserver可以监听数据的delete insert操作,现在让我们以一个例子来说一下如何办到
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SimpleAdapter;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class MainActivity extends AppCompatActivity {private String uri_raw_contacts = "content://com.android.contacts/raw_contacts";private String uri_data_phones = "content://com.android.contacts/data/phones";private String uri_data_emails ="content://com.android.contacts/data/emails";private ListView listView;private ContentResolver resolver;private List<Map<String, Object>> list;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);listView = (ListView) findViewById(R.id.listView);resolver = getContentResolver();list = queryContactsList();SimpleAdapter adapter = new SimpleAdapter(this,list,R.layout.item_listview_main,new String[] { "_id", "display_name", "emails", "phones"},new int[] {R.id.text_item_id, R.id.text_item_username, R.id.text_item_emails, R.id.text_item_phones});listView.setAdapter(adapter);// 为ListView注册上下文菜单registerForContextMenu(listView);}/*** 查询联系人信息* @return*/private List<Map<String, Object>> queryContactsList() {List<Map<String, Object>> list = new ArrayList<>();Cursor cursor = null;try{cursor = resolver.query(Uri.parse(uri_raw_contacts),new String[] { "_id", "display_name" }, null, null, null);while(cursor.moveToNext()) {Map<String, Object> map = new HashMap<>();int id = cursor.getInt(cursor.getColumnIndex("_id"));String name = cursor.getString(cursor.getColumnIndex("display_name"));map.put("_id", id);map.put("display_name", name);Cursor phoneCursor = null;try {// 根据_id查询联系人电话phoneCursor = resolver.query(Uri.parse(uri_data_phones),new String[] { "raw_contact_id", "data1" }, "raw_contact_id=?", new String[] { id+"" }, null);StringBuilder builder1 = new StringBuilder();while(phoneCursor.moveToNext()) {builder1.append(phoneCursor.getString(phoneCursor.getColumnIndex("data1")));builder1.append("|");}map.put("phones", builder1.toString());Cursor emailCursor = null;try {// 根据_id查询联系人EmailsemailCursor = resolver.query(Uri.parse(uri_data_emails),new String[] { "raw_contact_id", "data1" }, "raw_contact_id=?", new String[] { id+"" }, null);StringBuilder builder2 = new StringBuilder();while(emailCursor.moveToNext()) {builder2.append(emailCursor.getString(emailCursor.getColumnIndex("data1")));builder2.append("|");}map.put("emails", builder2.toString());}catch (Exception e){e.printStackTrace();}finally {if(emailCursor != null){emailCursor.close();}}}catch (Exception e){e.printStackTrace();}finally {if(phoneCursor != null){phoneCursor.close();}}// 添加到listlist.add(map);}}catch (Exception e){e.printStackTrace();}finally {if(cursor != null){cursor.close();}}return list;}@Overrideprotected void onDestroy() {super.onDestroy();}@Overridepublic void onCreateContextMenu(ContextMenu menu, View v,ContextMenu.ContextMenuInfo menuInfo) {getMenuInflater().inflate(R.menu.contacts, menu);// 填充菜单AdapterView.AdapterContextMenuInfo aMenuInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;// 显示菜单Header图表和标题@SuppressWarnings("unchecked")Map<String, Object> map = (Map<String, Object>) listView.getAdapter().getItem(aMenuInfo.position);String name = (String) map.get("display_name");menu.setHeaderIcon(R.mipmap.ic_launcher);menu.setHeaderTitle(name);super.onCreateContextMenu(menu, v, menuInfo);}@Overridepublic boolean onContextItemSelected(MenuItem item) {AdapterView.AdapterContextMenuInfo aMenuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();Map<String, Object> map = (Map<String, Object>) listView.getAdapter().getItem(aMenuInfo.position);int id = (Integer) map.get("_id");String name = (String) map.get("display_name");switch(item.getItemId()) {case R.id.delete_item:// 删除数据库中数据resolver.delete(Uri.parse(uri_raw_contacts), "_id=?", new String[] { id+"" });// 删除内存中数据以更新界面list.remove(aMenuInfo.position);((SimpleAdapter) listView.getAdapter()).notifyDataSetChanged();break;case R.id.update_item:// 携带必要数据跳转到UpdateActivityIntent intent = new Intent(this, UpdateActivity.class);intent.putExtra("id", id);intent.putExtra("name", name);intent.putExtra("position", aMenuInfo.position);startActivityForResult(intent, 0);break;}return super.onContextItemSelected(item);}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {if (resultCode == RESULT_OK) {// 获得从UpdateActivity传回的数据int position = data.getIntExtra("position", 0);String newName = data.getStringExtra("newName");// 删除内存中数据以更新界面list.get(position).put("display_name", newName);((SimpleAdapter) listView.getAdapter()).notifyDataSetChanged();}super.onActivityResult(requestCode, resultCode, data);}
}
//更新数据的Activity
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;public class UpdateActivity extends AppCompatActivity {private String uri_raw_contacts = "content://com.android.contacts/raw_contacts";private EditText displayname;private int id;private int position;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_update);// 获取传入的值Intent intent = getIntent();String name = intent.getStringExtra("name");id = intent.getIntExtra("id", 0);position = intent.getIntExtra("position", 0);displayname = (EditText) findViewById(R.id.editText_update_displayname);displayname.setText(name);}/*** 响应按钮点击* @param view*/public void clickButton(View view) {// 修改数据库中的值ContentResolver resolver = getContentResolver();ContentValues values = new ContentValues();String newName = displayname.getText().toString();values.put("display_name", newName);int result = resolver.update(Uri.parse(uri_raw_contacts), values, "_id=" + id, null);if (result == 1) {Toast.makeText(this, "修改成功", Toast.LENGTH_SHORT).show();// 向MainActivity中回传数据,以使ListView更新界面Intent intent = new Intent();intent.putExtra("position", position);intent.putExtra("newName", newName);setResult(RESULT_OK, intent);finish();} else {Toast.makeText(this, "修改失败", Toast.LENGTH_SHORT).show();}}
}
//贴上布局代码
listView的item选项
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent" ><TextViewandroid:id="@+id/text_item_username"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_alignParentTop="true"android:textSize="24sp"android:textColor="#00f"android:text="TextView" /><TextViewandroid:id="@+id/text_item_id"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:layout_alignParentTop="true"android:text="TextView" /><TextViewandroid:id="@+id/text_item_phones"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_below="@+id/text_item_username"android:text="TextView" /><TextViewandroid:id="@+id/text_item_emails"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentLeft="true"android:layout_below="@+id/text_item_phones"android:textSize="20sp"android:textColor="#f00"android:text="TextView" /></RelativeLayout>
//UpdateActivity的布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent"android:orientation="vertical" ><EditText
android:id="@+id/editText_update_displayname"android:layout_width="match_parent"android:layout_height="wrap_content"android:ems="10" ><requestFocus /></EditText><Button
android:id="@+id/button_update_submit"android:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="clickButton"android:layout_gravity="center"android:text="修改联系人姓名" /></LinearLayout>
在这些方法中我们反复用到一个东西Cursor游标,
Cursor是Android查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证Cusor最终会被释放掉。然而如果Cursor的数据量特表大,特别是如果里面有Blob信息时,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,也就是如果不手动关闭,系统会报错,会给用户以错误提示。
所以我们使用Cursor的方式一般如下:
Cursor cursor = null;
try{cursor = mContext.getContentResolver().query(uri,null,null,null,null);if(cursor != null){cursor.moveToFirst();//do something}
}catch(Exception e){e.printStatckTrace();
}finally{if(cursor != null){cursor.close();}
}
//有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。
@Override
protected void onDestroy() {if (mAdapter != null && mAdapter.getCurosr() != null){ mAdapter.getCursor().close(); }super.onDestroy();
}
// CursorAdapter中的changeCursor函数,会将原来的Cursor释放掉,并替换为新的Cursor,所以你不用担心原来的Cursor没有被关闭。 你可能会想到使用Activity的managedQuery来生成Cursor,这样Cursor就会与Acitivity的生命周期一致了,多么完美的解决方法!然而事实上managedQuery也有很大的局限性。managedQuery生成的Cursor必须确保不会被替换,因为可能很多程序事实上查询条件都是不确定的,因此我们经常会用新查询的Cursor来替换掉原先的Cursor。因此这种方法适用范围也是很小。
我在查询联系人时用了两层游标,如果我只关闭了外层游标,没有关闭里层的,导致内存资源没有被释放。多次运行后,系统资源被耗尽。
会抛出如下异常信息
主要抛出的异常信息为:
ERROR/JavaBinder(3438): * Uncaught remote exception! (Exceptions are not yet supported across processes.)
ERROR/JavaBinder(3438): java.lang.RuntimeException: No memory in memObj