Rex的学习空间

梦想者空间


  • 首页

  • 归档

  • 标签

ContentProvider详解

发表于 2016-05-25

一、ContentProvider简介:
(一)、引入:
数据库在Android当中是私有的,不能将数据库设为WORLD_READABLE,每个数据库都只能创建它的包访问。这意味着只有创建这个数据库的应用程序才可访问它。也就是说不能跨越进程和包的边界,直接访问别的应用程序的数据库。那么如何在应用程序间交换数据呢? 如果需要在进程间传递数据,可以使用ContentProvider来实现。

(二)、ContentProvider的功能和意义:
为了在应用程序之间交换数据,Android提供了ContentProvider,ContentProvider是不同应用程序之间进行数据交换的标准API。当一个应用程序需要把自己的数据暴露给其他应用程序使用时,该应用程序可以通过提供ContentProvider来实现;而其他应用程序需要使用这些数据时,可以通过ContentResolver来操作ContentProvider暴露的数据。
一旦某个应用程序通过ContentProvider暴露了自己的数据操作接口,那么不管该应用程序是否启动,其他应用程序都可以通过该接口来操作被暴露的内部数据,包括增加数据、删除数据、修改数据、查询数据等。
虽然大部分使用ContentProvider操作的数据都来自于数据库,但是也可以来自于文件、SharedPreferences、XML或网络等其他存储方式。

(三)、核心类:
1、ContentProvider:(A应用暴露数据)
一个程序可以通过实现一个ContentProvider的抽象接口将自己的数据暴露出去
外界根本看不到,也不用看到这个应用暴露的数据在应用当中是如何存储的,是用数据库存储还是用文件存储,还是通过网上获得,这些一切都不重要,重要的是外界可以通过这一套标准及统一的接口和程序里的数据打交道,可以读取程序的数据,也可以修改程序的数据。
2、ContentResolver:(操作A应用所暴露的数据)
外界的程序通过ContentResolver接口可以访问ContentProvider提供的数据
ContentResolver 可以理解成是HttpClient的作用。
3、 Uri:Uri是ContentResolver和ContentProvider进行数据交换的标识。
每个ContentProvider提供公共的URI来唯一标识其数据集。管理多个数据集的(多个表)的 ContentProvider 为每个数据集提供了单独的URI。
比如:content://com.example.app.provider/table1/…: (类比:http://www.sina.com.cn/sports/nba/….)
Uri 的标准前缀:以“content://”作为前缀,这个是标准的前缀,表示该数据由 ContentProvider 管理。
Uri 的authority部分:该部分是完整的类名。(使用小写形式保证他的唯一性)。
Uri 的path部分(资源部分、数据部分): 用于决定哪类数据被请求(一般写的就是表的名字)。
被请求的特定记录的id值。如果请求不仅限于某个单条数据,该部分及其前面的斜线应该删除。
为了将一个字符串转换成Uri,Android中提供了Uri的parse()静态方法来实现。

【备注】URI、URL、URN的区别:
首先,URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。
URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。
也就是说,URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI。
总结一下:URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位,所以,是绝对的。

(四)、系统内置的预定义ContentProvider:
1、Contacts:获取、修改、保存联系人的信息
2、SMS:短信
3、CallLog :查看或更新通话记录
4、MediaStore:访问声音、视频、图片等多媒体文件
5、Browser:读取或修改浏览历史、网络搜索、书签
6、Setting:查看和获取蓝牙设置、铃声设置等设备首选项

二、使用ContentResolver使用(系统提供的ContentProvider全部都定义好了)

(一)、 使用ContentResolver 操作数据的步骤:
调用Context的getContentResolver()方法获得ContentResolver 对象;
调用使用ContentResolver 的insert()、delete()、update()、query()方法操作数据。
Uri insert(Uri uri, ContentValues values)
int delete(Uri uri, String where, String[] whereArgs)
Cursor query(Uri uri, String[] projection, String where, String[] whereArgs, String sortOrder)
int update(Uri uri, ContentValues values, String where, String[] whereArgs)
参数解释:
String where:表示带有占位符的where子句组成的字符串;
String[] whereArgs:表示替换where参数中占位符(?)后的数据组成的字符串数组;
String sortOrder:表示select语句中的order by子句组成的字符串;
String[] projection:表示select语句中需要查询返回的列。
ContentValues values:是由数据库中表字段和往该字段中放置的数据所组成的键值对对象。
【备注】以上四个方法的参数分别是2、3、4、5个。
注意:使用CursorAdapter的数据源cursor必须要有_id的字段

(二)、关于通话记录的数据操作
使用的Uri:content://call_log/calls
表中对应常用的字段的列名:
_id 主键
number 电话号码
name 联系人(如果没有这样的联系人信息,返回就是null)
type:通话类型 (1、来电记录;2、呼出记录;3、未接电话)
date 通话时间,接/未接电话的时间(年月日)(毫秒)
duration 通话时长(秒)
【备注】权限申请:android.permission.READ_CALL_LOG|android.permission.WRITE_CALL_LOG

(三)、关于短信的数据操作 (数据库路径: /data/data/com.android.providers.telephony/databases/mmssms.db)
1、常用使用的Uri:
1) content://sms/ (操作所有短信)
2) content://sms/inbox (收件箱)
3) content://sms/sent (已发送)
4) content://sms/draft (草稿)
5) content://sms/outbox (发件箱)
6) content://sms/failed (发送失败)
7) content://sms/queued (待发送列表)
2、表中对应常用的字段:
_id 主键
thread_id 序号,如果是同一个发件人,这里记录的thread_id一定以是一样的
address 发件人手机号码
person 联系人列表里的序号,null代表陌生人
date 日期(毫秒)
protocol 协议–(0.SMS_RPOTO, 1.MMS_PROTO)
read 是否阅读 (0未读, 1已读 )
status 接受状态 (-1接收,0完成, 64等待, 128失败)
type (0所有;1收件箱 ;2已发送;3草稿;4发件箱;5发送失败;6待发送列表)
body 短信内容
service_center 短信服务中心号码编号
3、sms对应的视图表threads常用字段意思 通过 (* from threads –)进行连接||扩展内容
_id 等于sms中的thread_id
message_count 短信的数量
snippet 最新一条短信的内容
date 最新一条短信对应的时间
【备注】权限申请

(四)、关于联系人的数据操作(数据库路径:/data/data/com.android.providers.contacts/databases/contacts2.db)

  从上图可知:总共有三张数据表(contact, row_contact , data)
  官方文档对三张表的解释:
1)ContactsContract.Contacts table
    Rows representing different people, based on aggregations of raw contact rows.
2)ContactsContract.RawContacts table
    Rows containing a summary of a person's data, specific to a user account and type.
3)ContactsContract.Data table
    Rows containing the details for raw contact, such as email addresses or phone numbers.

1、常用使用的Uri:
a) 联系人的Uri==> content://com.android.contacts/contacts 和 content://com.android.contacts/raw_contacts 和content://com.android.contacts/data
b )电话号码的Uri==> content://com.android.contacts/data/phones
c) EMAIL的URI==> content://com.android.contacts/data/emails

不过为了方便记忆,系统中提供了以下常量来替代以上的Uri字符串。(系统直接提供的Uri)

Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI; –> content://com.android.contacts/contacts
Uri PHONE_URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; –> content://com.android.contacts/data/phones
Uri EMAIL_URI = ContactsContract.CommonDataKinds.Email.CONTENT_URI; –> content://com.android.contacts/data/emails

2、 data表中对应常用的字段:
    a) raw_contact_id :关联raw_contacts表的主键
    b) data1: 存放数据,包括(联系人、电话号码、Email)
    c) mimetype:配合data1使用(通过不同的类型区分是联系人,电话号码,Email)

【备注】权限申请

(五)、示例代码1——查看通讯录中的联系人姓名、id、电话、Email等信息:

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
    // 查询联系人
public static List<Map<String,Object>> queryContacts(ContentResolver resolver){
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
Cursor cursorRawContacts;
Cursor cursorData;
//查找出raw_contacts的所有主键
cursorRawContacts = resolver.query(
Uri.parse(URL_RAW_CONTACTS),
new String[]{"_id"}, null, null, null);
// 默认在开始位置
while (cursorRawContacts.moveToNext()) {
//将_id加入到Map中
Map<String, Object> map = new HashMap<String, Object>();
int _id = cursorRawContacts.getInt(cursorRawContacts
.getColumnIndex("_id"));
map.put("_id", _id);
//根据raw_contacts找到指定的data
cursorData = resolver.query(
Uri.parse(URL_DATA),
new String[]{"data1","mimetype"},
"raw_contact_id = ?",
new String[]{_id +""}, null);
//从cursorData中获取联系人,电话号码,email
while (cursorData.moveToNext()) {
String data1 = cursorData.getString(cursorData.getColumnIndex("data1"));
String mimetype = cursorData.getString(cursorData.getColumnIndex("mimetype"));
if(mimetype.equals("vnd.android.cursor.item/name")){
map.put("name", data1);
}else if(mimetype.equals("vnd.android.cursor.item/phone_v2")){
// 可能有多个电话号码
if (!map.containsKey("phone")) {
map.put("phone", data1);
} else {
String phone = (String) map.get("phone");
// 定义拼接的规则
map.put("phone", phone + "&&" + data1);
}
}else if(mimetype.equals("vnd.android.cursor.item/email_v2")){
// 可能有多个Email
if (!map.containsKey("email")) {
map.put("email", data1);
} else {
String email = (String) map.get("email");
// 定义拼接的规则
map.put("email", email + "&&" + data1);
}
}
}
list.add(map);
}
return list;
}

(六)、示例代码2——添加联系人信息:

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
 //插入联系人信息
public static void insertContacts(ContentResolver resolver , Map<String,Object> data){
//先插入一条空的数据
ContentValues values = new ContentValues();
//获取插入返回的uri
Uri uriInsert = resolver.insert(Uri.parse(URL_RAW_CONTACTS), values);
//截取uri的最后位的数字,该数字表示raw_contact_id
long raw_contact_id = ContentUris.parseId(uriInsert);
//往data表中插入名字
values.clear();
values.put("raw_contact_id", raw_contact_id);
values.put("mimetype", "vnd.android.cursor.item/name");
values.put("data1", (String) data.get("name"));
values.put("data2", (String) data.get("name"));
resolver.insert(Uri.parse(URL_DATA), values);
// 往data表中插入电话号码
values.clear();
values.put("raw_contact_id", raw_contact_id);
values.put("mimetype", "vnd.android.cursor.item/phone_v2");
values.put("data1", (String) data.get("phone"));
values.put("data2", Phone.TYPE_MOBILE);
resolver.insert(Uri.parse(URL_DATA), values);
// 往data表中插入电话号码
values.clear();
values.put("raw_contact_id", raw_contact_id);
values.put("mimetype", "vnd.android.cursor.item/email_v2");
values.put("data1", (String) data.get("email"));
values.put("data2", Email.TYPE_HOME);
resolver.insert(Uri.parse(URL_DATA), values);
}

(七)、示例代码3——删除,修改联系人信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
       // 删除联系人(根据raw_contact的_id)
public static void deleteContacts(ContentResolver resolver, int _id) {
resolver.delete(Uri.parse(URL_RAW_CONTACTS), "_id = ?",
new String[] { _id + "" });
}
//更新联系人(根据row_contact的_id名字的替换)
public static void updateContacts(ContentResolver resolver, int _id ,String newName){
ContentValues values = new ContentValues();
// 更新raw_contacts表中的4列字段(名字和排序)
values.put("display_name", newName);
values.put("display_name_alt", newName);
values.put("sort_key", newName);
values.put("sort_key_alt", newName);
resolver.update(Uri.parse(URL_RAW_CONTACTS), values, "_id = ?", new String[] { _id + "" });
// 更新data表中的2列字段
values.clear();
values.put("data1", newName);
values.put("data2", newName);
resolver.update(Uri.parse(URL_DATA), values,
"raw_contact_id = ? and mimeType = ?", new String[] { _id + "", "vnd.android.cursor.item/name" });
}

(八)、Android为多媒体提供的ContentProvider的Uri如下:

MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 存储在SD卡上的音频文件
MediaStore.Video.Media.EXTERNAL_CONTENT_URI 存储在 SD卡上的视频
MediaStore.Images.Media.EXTERNAL_CONTENT_URI 存储在 SD卡上的图片文件内容
MediaStore.Audio.Media.INTERNAL_CONTENT_URI 手机内部存储器上的音频文件
MediaStore.Video.Media.INTERNAL_CONTENT_URI 手机内部存储器上的视频
MediaStore.Images.Media.INTERNAL_CONTENT_URI 手机内部存储器上的图片
【数据库中主要字段的列名】

多媒体文件名称字段:Media.DISPLAY_NAME
详细描述字段:Media.DESCRIPTION
多媒体文件保存路径的字段:Media.DATA (/storage/mnt/icon.png)

五、当日作业:
1、利用ContentResolver+AutoCompleteTextView实现自动补全联系人姓名
2、利用ContentResolver制作管理SDCard上的多媒体文件管理器(可以显示图片,音频)
要求:左侧Fragment动态改变,右侧显示的内容动态变化(图片要求真实图片,音频视频不要求这是图片),文件名字显示必须要正确

1
2
3
4
5
6
7
8
9
10
11
12
// 手动更新媒体库?
private void updateGallery(String filename)//filename是我们的文件全名,包括后缀哦
{
MediaScannerConnection.scanFile(this,
new String[] { filename }, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.i("ExternalStorage", "Scanned " + path + ":");
Log.i("ExternalStorage", "-> uri=" + uri);
}
});
}

Activity详解

发表于 2016-05-16

一、Activity(活动):
1、定义:实现应用程序的可视化用户界面。所有Activity都必须在manifest文件(清单文件)中用activity标签生命。任何未经声明的Activity系统都会视而不见,且不会运行。
2、作用:它上面可以显示一些控件也可以监听并处理用户的事件做出响应

二、Activity的生命周期:
Activity类中定义了七个回调方法,覆盖了活动(activity)生命周期的每一个环节(依次是:)
1、onCreate():每个活动中都重写了这个方法,它会在activity第一次被创建的时候调用,因此,应该在这个方法里完成活动的初始化,比如:加载布局,绑定事件。
2、onRestart():在活动由停止状态变为运行状态之前被调用,也就是说活动被重新启动。(注:活动本身就存在栈中,没有调用onCreate()方法)
3、onStart():在活动由不可见变为可见的时候调用。
4、onResume():在活动准备好和用户进行交互的时候调用,此时的活动一定位于返回栈的栈顶,并处于运行状态。调用该方法后,界面可以和用户进行交互。
5、onPause():在系统准备去启动或恢复另一个activity的时候调用,通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些相关数据,但是这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
6、onStop:在活动完全不可视的时候调用,它和onPause()方法的主要区别在于:onPause失去了焦点(不可点击),但仍处于可见的状态,而onStop()是失去焦点并处于不可见的状态,例如:启动的新活动是一个对话框的形式,就会执行onPuase(),而不会执行onStop()
7、onDestroy():在活动被销毁之前调用,之后活动的状态将变为销毁状态。在该方法中释放资源。
由上面的几个方法,我们对以下几种例子,来得出主界面调用了哪些方法:
1)、打开主界面,后退键退出
onCreate()->onStart()->onResume()->onPause()->onStop()->onDestroy()
2)、打开主界面,进入第二个页面,再按返回键返回
onCreate()->onStart()->onResume()->onPause()->onStop()->onRestart()->onStart()->onResume()
官方给出的一张Activity的生命周期图:
这里写图片描述
七个生命周期的组合:
onCreate、onStart、onResume:启动应用程序
onPause、onStop:失去焦点
onRestart、onStart、onResume:重新获得焦点
onPause、onStop、onDestroy:退出应用程序

定义生命周期的作用:
①当用户接一个电话或切换到另一个程序不会崩溃
②当用户后台运行程序时不会销毁有价值的系统资源
③当用户离开再返回你的应用时不会丢失用户的进程
④当手机屏幕进行横竖屏切换的时候不会崩溃或者丢掉用户的进程

三、任务和回退栈:
(一)任务Task:
1、概念:一个任务(task)就是在执行某项工作时与用户进行交互的Activity集合。这些Activity按照被打开的顺序依次被安排在一个堆栈中(回退栈)。
2、主屏页面:
设备的主屏是大多数任务的启动位置,当用户触摸一个应用程序启动器图标(或者app快捷图标),应用程序的任务就会在前台显示。如果相关应用程序的任务不存在,那么就会有一个新的任务被创建,并且应用程序打开的”主”Activity会作为任务中的根Activity。
(二)、回退栈:
1、概念:在当前的Activity启动了另一个Activity时,这个新的Activity被放到了堆栈的顶部,并且带有焦点。前一个Activity并没有消失,而是保存在
回退栈中,此时它处于停止状态。
当用户按下回退按钮时,当前的Activity会被从回退栈的顶部弹出(这个Activity被销毁),而前一个Activity被恢复。堆栈中的Activity不会被重新排列。因此,回退栈的操作跟后进先出的对象结构是一样的
在用户按下回退按钮的时,当前的Activity被销毁,并且前一个Activity被恢复。如果用户继续按回退键,那么回退栈中的每个Activity会被一次弹出,前一个Activity会被显示,直到用户返回主屏(或者返回到任务开始时运行的那个Activity)。当所有的Activity从回退栈中被删除时,这个任务就不再存在。
图:
这里写图片描述
注意:后台中可以拥有多个任务,但是如果用户同时运行了很多后台任务,系统为了回收内存可能销毁一些后台的Activity,从而导致Activity的状态丢失。

四、启动模式
在Android中Activity的启动模式决定了Activity的启动运行方式。
1、Activity的启动模式分为四种:
(1)standard(系统默认的启动模式)
标准启动模式,每次激活Activity时都会创建Activity,并放入任务栈中。每个窗体的getTastId()保持不变,但this.hashCode()会发生改变。
(2)singleTop:
如果在任务的栈顶正好存在该Activity的实例,就重用该实例,而不会创建新的Activity对象,不过它会调用onNewIntent()方法。如果栈顶不存在就会创建新的实例并放入栈顶(即使栈中已经存在该Activity实例,只要不再栈顶,都会创建实例),会回调onNewIntent()
(3)singleTask
如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的onNewIntent())。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移除栈。如果栈中不存在该实例,将会创建新的实例放入栈中。
和singleTop在名字上即可看出区别,即singleTop每次只检测当前栈顶的Activity是否是我们需要请求创建的,而singleTask则会检测栈中全部的Activity对象,从上向下,如果检测到是我们所请求的则会消灭此Activity对象上面的对象,直接把检测到的我们需要的Activity置为栈顶。
(4)singleInstance
与singleTask模式的区别是存放singleInstance模式窗口对象的回退栈不能有其他任何窗口对象。因此如果该窗口不存在,则要新建任务来存放该singleInstance模式窗口。也就是说getTaskId()会发现任务id发生了变化。
此启动模式和我们使用的浏览器工作原理类似,在多个程序中访问浏览器时,如果当前浏览器没有打开,则打开浏览器,否则会在当前打开的浏览器中访问。此模式会节省大量的系统资源,因为他能保证要请求的Activity对象在当前的栈中只存在一个。
总之,在开发Android项目时,巧妙设置Activity的启动模式会节省系统开销和提高程序运行效率。

五、Activity常见问题:
1、Activity返回时,如何返回数据?
(1)、在Activity1中用startActivityForResult()来启动另一个Activity2,
在Activity1中复写onActivityResult()方法,回调拿到返回的数据
(2)、在Activity2中利用setResult()方法返回数据给Activity1

2、异常情况下保存临时数据的方法:
在onSaveInstanceState()方法中保存数据
//读取数据的方法
方法1:onRestoreInstanceState(),该方法执行在onStart()方法之后
方法2:在onCreate方法中的入参,就是保存的数据,直接从中取出数据
注意:取出数据之前,一定要判断是否为空。

3、横竖屏切换的问题:
竖屏—>横屏:先杀死一个Activity,再创建一个Activity
横屏—>竖屏:先杀死一个Activity,再创建一个Activity
再杀死一个Activity,再创建一个Activity
原因:android的键盘导致的
可以设置清单文件中:
android:configChanges=”keyboardHidden|orientation|screenSize”
来保证横竖屏切换的时候,不改变Activity的生命周期
当横竖屏切换的时候,会触发:
public void onConfigurationChanged(Configuration newConfig);
横竖屏切换的时候可以在这个方法中做一些任务

BroadCastReceiver详解

发表于 2016-05-16

一、BroadCastReceiver(广播接收者):
(一)、广播传播机制:
广播接收器,也被称为全局事件,或系统事件。
当Android系统中任何程序有动作时,如果想通知其他程序,采用广播的方式进行传播是非常有效的。广播从理论上说,可以将一个动作传播给任意多个程序(当然,广播接收器的数量会收到系统限制)。
在Android中,有一些操作完成以后,会发送广播,比如说发出一条短信,或打出一个电话,如果某个程序接收了这个广播,就会做相应的处理。这个广播跟我们传统意义中的电台广播有些相似之处。之所以叫做广播,就是因为它只负责“说”而不管你“听不听”,也就是不管你接收方如何处理。另外,广播可以被不只一个应用程序所接收,当然也可能不被任何应用程序所接收。
广播机制最大的特点就是发送方并不关心接收方是否接到数据,也不关心接收方是如何处理数据的。
Android中广播的是操作系统中产生的各种各样的事件。例如,收到一条短信就会产生一个收到短信息的事件。而Android操作系统一旦内部产生了这些事件,就会向所有的广播接收器对象来广播这些事件。

(二)、广播机制的三要素:
Android广播机制包含三个基本要素:

1. 广播(Broadcast) - 用于发送广播;
2. 广播接收器(BroadcastReceiver) - 用于接收广播;
3. 意图内容(Intent)-用于保存广播相关信息的媒介。
Broadcast是一种广泛运用的在应用程序之间传输信息的机制。
而BroadcastReceiver是对发送出来的Broadcast进行过滤接受并响应的一类组件。

Context详解

发表于 2016-05-16

一、什么是Context?
源码注释:

1
2
3
4
5
6
7
8
9
/**
* Interface to global information about an application environment. This is
* an abstract class whose implementation is provided by
* the Android system. It
* allows access to application-specific resources and classes, as well as
* up-calls for application-level operations such as launching activities,
* broadcasting and receiving intents, etc.
*/

public abstract class Context {

从注释中可以看出:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。既然上面Context是一个抽象类,那么肯定有他的实现类咯,我们在Context的源码中通过IDE可以查看到他的子类最终可以得到如下关系图:

Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

二、一个应用程序有几个Context?
其实这个问题本身并没有什么意义,关键还是在于对Context的理解,从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。上面的关系图也从另外一个侧面告诉我们Context类在整个Android系统中的地位是多么的崇高,因为很显然Activity,Service,Application都是其子类,其地位和作用不言而喻。

三、Context能干什么?
Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等都需要用到Context。

1
2
3
4
5
6
7
8
TextView tv = new TextView(getContext());
ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);
getApplicationContext().getContentResolver().query(uri, ...);
getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;
getContext().startActivity(intent);
getContext().startService(intent);
getContext().sendBroadcast(intent);

四、Context引起的内存泄露
但Context并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面就示例两种错误的引用方式。
错误的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton(Context context) {
this.mContext = context;
}
public static Singleton getInstance(Context context) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}

这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

View持有Activity引用

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {
private static Drawable mDrawable;
@Override
protected void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = new ImageView(this);
mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
iv.setImageDrawable(mDrawable);
}
}

有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

正确使用Context
一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:
1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2:不要让生命周期长于Activity的对象持有到Activity的引用。
3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

参考文章:
[http://www.jianshu.com/p/94e0f9ab3f1d]
[http://blog.csdn.net/guolin_blog/article/details/47028975]
[http://blog.csdn.net/yanbober/article/details/45967639]

Service详解

发表于 2016-05-16

一、Service 简介:
“Service” 意思即“服务”的意思, Service是Android中实现程序后台运行的解决方案,它适合用于执行不需要和用户交互而还要求长期运行的任务。Service运行在后台,它是不可见的、无界面的程序。Service的运行不依赖于任何用户界面,承担着静悄悄的不为人所注意的工作,即使当程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。
但是,Service并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。另外,不要被Service的后台概念所迷惑,Service不会自动开启线程,所有的代码都默认运行在主线程当中。使用Service时,如果担心主线程被阻塞,就需要在Service的内部手动创建一个子线程来执行任务。(服务不开启线程执行耗时操作也会产生ANR)
Service可以在很多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity,这个时候程序要在后台继续播放;比如检测SD卡上文件的变化;再或者在后台记录用户的地理信息位置的改变;或者启动一个服务来运行并一直监听某种动作等等。

【备注】
 三大智能手机操作系统中,IOS是不支持后台的,当应用程序不在前台运行时就会进入到挂起状态。Android则沿用了Symbian的老习惯,加入了后台功能,这样应用程序即使在关闭的情况下仍然可在后台继续运行。Windows Phone则经历了由不支持后台到支持后台的过程。目前Windows Phone8系统也具备后台功能。

二、Service概念的总结:

  • Service在后台运行,不可以与用户直接交互;
  • 一个服务不是一个单独的进程。服务对象本身并不意味着它是在自己的进程中运行,除非另有规定,否则它与运行程序是同在一个进程中;
  • 一个服务不是一个单独的线程。Service和其他组件一样,默认情况下,Service中的所有代码都是运行在主线程中;
  • Service存在的价值虽然不如Activity那么清晰。但是一般都让Service执行耗时较长的操作。例如:播放音乐、下载文件、上传文件等等。但是因为Service默认运行在主线程中,因此不能直接用它来做耗时的请求或者动作,最好在Service中启动新线程来运行耗时的任务;
  • 需要通过某一个Activity/BroadCastReceiver或其他Context对象来启动Service。context.startService() 或 context.bindService();
  • Service很大程度上充当了应用程序后台线程管理器的角色。(如果Activity中新开启一个线程,当该Acitivyt关闭后,该线程依然在工作,但是与开启它的Activity失去联系。也就是说此时的这个线程处于失去管理的状态。但是使用Service,则可以对后台运行的线程有效地管理)
  • 对于Activity来说,Service管理线程更为容易

三、为什么不使用后台线程而使用Service?
1、Service可以放在独立的进程中,所以更安全;(不是默认的行为,要单独设置)
2、使用Service可以依赖现有的binder机制,不需要在应用层面上处理线程同步的繁杂工作;(比如远程服务数据传递)
3、系统可以重新启动异常死去的Service。(是不会影响用户的UI操作的)

四、Service 与 Activity 的相同点与不同点

  • 不同点:Activity是与用户交互的组件,即可以看到UI界面,而Service是在后台运行、无需界面;
  • 相同点:使用Activity 时我们需要在配置文件中声明标签,同样的使用Service 也需要在配置文件中声明标签。都具有一定的生命周期。

五、服务的分类:
1、本地服务Local Service:(开启的服务给本应用程序使用的)
Local Service 用于应用程序内部。用于实现应用程序自己的一些耗时任务,比如查询升级信息,并不占用应用程序比如Activity所属线程,而是单开线程后台执行,这样用户体验比较好。
启动service有两种方法:

1. Context.startService()
                调用者与服务之间没有关联,即使调用者退出,服务仍可运行
2. Context.bindService()
                调用者与服务绑定在一起,调用者一旦退出,服务也就终止

A、根据启动方式将本地服务分为:启动服务Started Service和绑定服务Bound Service。

  • Started Service:被启动的服务
    被启动的服务是由其它组件调用startService()方法而启动的,该方法会导致被启动服务的生命周期方法onStartCommand()被回调。当服务是被启动状态后,其生命周期与启动它的组件无关,即使启动服务的组件(Activity,BroadcastReceiver)已经被销毁,该服务还可以在后台无限期运行。除非调用stopSelf()或stopService()来停止该服务。
  • Bound Service:被绑定的服务
    绑定服务是允许其它应用程序绑定并且与之交互的Service的实现类。为了提供绑定,必须实现onBind()回调方法。该方法返回IBinder对象(IBinder是接口),它定义了服务类与Activity交互的程序接口。
    Activity通过bindService()方法绑定到服务类,同时Activity必须提供ServiceConnection接口的实现类,它监视Activity与服务类之间的连接。在重写ServiceConnection接口的onServiceConnected()方法时,实现了将服务类顺利赋值到了Activity中,实现了在Activity中使用该服务类并执行其中的方法。

B、根据onStartCommand()回调方法的返回值,将Service分为粘性Service和非粘性Service:
onStartCommand()方法有三种返回值:(服务被异常kill掉是系统的行为,当内存缺乏的时候会kill掉服务)

  • START_STICKY(常量值:1):sticky的意思是“粘性的”。使用这个返回值时,我们启动的服务跟应用程序”粘”在一起,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务。当再次启动服务时,传入的第一个参数将为null;
  • START_NOT_STICKY(常量值:2):“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。
  • START_REDELIVER_INTENT(常量值:3):具有“粘性的”,重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。
    【备注】
    以上三种情况,可以理解为发生车祸后的人:
    START_STICKY:(常量值:1)车祸后自己苏醒,但是失忆;
    START_NOT_STICKY:(常量值:2)车祸后再也没有苏醒;
    START_REDELIVER_INTENT:(常量值:3)车祸后自己苏醒,依然保持记忆。

2、远程服务Remote Service:
Remote Service 用于android系统内部的应用程序之间。可以定义接口并把接口暴露出来,以便其他应用进行操作。可被其他应用程序复用,比如天气预报服务,其他应用程序不需要再写这样的服务,调用已有的即可。

六、Service的生命周期:


1、Started Service的生命周期:

  • onCreate():创建服务
  • onStartCommand():服务开始运行(在2.0以前版本中,使用onStart()回调方法)
  • onDestroy() :服务被停止
    【详细说明:】
  • 在程序中调用:context.startService() 会触发执行Service生命周期中的onCreate()、onStartCommand()回调方法,此时服务就开始正式运行;
  • 如果Service还没有运行,则android先调用onCreate()然后调用onStartCommand();如果Service已经运行,则只调用onStartCommand(),所以一个Service的onStartCommand方法可能会重复调用多次;
  • 如果在程序中调用:context.stopService()会触发执行Service生命周期中的onDestroy()回调方法,会让服务停止;
  • stopService()的时候直接onDestroy,如果是调用者自己直接退出而没有调用stopService()的话,Service会一直在后台运行。该Service的调用者再启动该Service后可以通过stopService关闭Service;stopSelf()
  • 所以StartService的生命周期为:onCreate –> onStartCommand(可多次调用) –> onDestroy。

2、Bound Service的生命周期:

  • onCreate():创建服务
  • onBind():绑定服务,服务开始运行
  • onUnbind():取消绑定
  • onDestroy() :服务被停止

【详细说明:】

  • 在程序中调用:context.bindService()会触发执行Service生命周期中的onCreate()、onBind()回调方法,此时服务开始运行;
  • onBind将返回给客户端一个IBind接口实例,IBind允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。此后调用者(Context,例如Activity)会和Service绑定在一起;
  • 如果调用Service的调用者Context退出了,那么会依次调用Service生命周期中的onUnbind()、onDestroy()回调方法,会让服务停止;
  • 所以BindService的生命周期为:onCreate –> onBind(只一次,不可多次绑定) –> onUnbind –> onDestory。

【备注:】

  • Service是不能自己启动的,只有通过 Context 对象调用startService() 和bindService() 方法来启动。
  • 在Service每一次的开启关闭过程中,只有onStartCommand()可被多次调用(通过多次startService调用),其他onCreate()、onBind()、onUnbind()、onDestory()在一个生命周期中只能被调用一次。
  • Service可以在和多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务总是藏在后头的。

First Night

发表于 2016-04-27

我有一头小毛驴,可是我从来都不骑。 ——Rex

刚搬到新家,还没适应怎么写,多折腾折腾几天再开始记录。

Rex

Rex

不忘初心

6 日志
© 2016 Rex
由 Hexo 强力驱动
主题 - NexT.Muse