Android 之四大组件、六大布局、五大存储:https://blog.csdn.net/shenggaofei/article/details/52450668
Android 四大组件、五大存储、六大布局:https://blog.csdn.net/xiankog/article/details/81702119
Android四大基本组件介绍与生命周期:https://www.cnblogs.com/bravestarrhu/archive/2012/05/02/2479461.html
转自:Android开发入门与实践
一、Android 四大组件详解
Android 四大核心组件指的是 Activity、Service、Content Provider、BroadCast Receiver,核心组件都是由 Android 系统进行管理和维护的,一般都要在清单文件中进行注册或者在代码中动态注册。
- 活动(activity):用于表现功能;
- 服务(service):后台运行服务,不提供界面呈现;
- 广播接受者(Broadcast Receive):用于接收广播;
- 内容提供者(Content Provider):支持多个应用中存储和读取数据。
1.1 activity
1、概念:
在 android 中,Activity 相当于一个页面,可以在 Activity 中添加 Button、CheckBox 等控件,一个 android 程序有多个 Activity 组成。
- (1)一个 Activity 通常就是一个单独的屏幕(窗口)。
- (2)Activity 之间通过 Intent 进行通信。
- (3)Android 应用中每一个 Activity 都必须要在 AndroidManifest.xml 配置文件中声明,否则系统将不识别也不执行该Activity。在 android stdio会自动生成,但 eclipse 需要自己手动添加
定义与作用: Activity 的中文意思是 活动,代表手机屏幕的一屏,或是平板电脑中的一个窗口,提供了和用户交互的可视化界面。一个活动开始,代表 Activity 组件启动,活动 结束,代表一个 Activity 的生命周期结束。一个 Android 应用必须通过 Activity 来 运行 和 启动,Activity 的生命周期交给系统统一管理。Activity 是用于处理 UI 相关业务的,比如加载界面、监听用户操作事件。
Activity 之间通过 Intent 进行通信;直接通过 Bundle 对象来传递
在Intent 的描述结构中,有两个最重要的部分:动作和动作对应的数据。
典型的动作类型有:MAIN(activity 的门户)、VIEW、PICK、EDIT 等。而动作对应的数据则以 URI 的形式进行表示。
例如:要查看一个人的联系方式,你需要创建一个动作类型为VIEW 的intent,以及一个表示这个人的URI。
与之有关系的一个类叫 IntentFilter。相对于 intent 是一个有效的做某事的请求,一个 intentfilter 则用于描述一个 activity(或者IntentReceiver)能够操作哪些 intent。一个 activity 如果要显示一个人的联系方式时,需要声明一个 IntentFilter,这个IntentFilter 要知道怎么去处理 VIEW 动作和表示一个人的 URI。IntentFilter 需要在 AndroidManifest.xml 中定义。通过解析各种intent,从一个屏幕导航到另一个屏幕是很简单的。当向前导航时,activity 将会调用 startActivity(Intent myIntent) 方法。然后,系统会在所有安装的应用程序中定义的 IntentFilter 中查找,找到最匹配 myIntent 的 Intent 对应的 activity。新的 activity 接收到myIntent 的通知后,开始运行。当 startActivity 方法被调用将触发解析 myIntent 的动作,这个机制提供了两个关键好处:
- Activities 能够重复利用从其它组件中以 Intent 的形式产生的一个请求;
- Activities 可以在任何时候被一个具有相同 IntentFilter 的新的 Activity 取代。
AndroidManifest 文件中含有如下过滤器的 Activity 组件为默认启动类,当程序启动时系统自动调用它
<intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
2、生命周期:
生命周期: 生命周期指的是 Activity 从 创建 到 销毁 所执行的一系列方法,主要包括7个生命周期方法。
生命周期:onCreate()
-> onStart()
- > onResume()
-> onPause()
-> onStop()
-> onDestroy()
详细流程如下图
4 个重要状态
- Resumed:一个新 Activity 启动入栈后,它在屏幕最前端,处于栈的最顶端,此时它处于可见并可和用户交互的激活状态。
- Paused:一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态,比如对话框形式的活动只会占用屏幕中间的部分区域,你还可以看到后边的界面,这时,后面的活动就处于暂停状态。( 意思就是当 Activity 被另一个透明 或者 Dialog 样式的 Activity 覆盖时的状态。此时它依旧与窗口管理器保持连接,系统继续维护其内部状态,所以它依然可见,但它己经失去了焦点故不可与用户交互。 )
- Stopped:当一个活动不在处于栈顶位置,并且完全处于不可见的时候,就进入了停止状态,就是进入了一个完全不透明的活动,上个活动会处于停止状态,这时系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。即当 Activity 被另一个 Activity 覆盖、失去焦点并不可见时处于 Stopped 状态
- Destroy :销毁状态,当一个活动从返回栈中移除后就变成了销毁状态,系统倾向于回收处于这种状态的活动,从而保证手机内存充足
摘自《Android从入门到精通》
七大方法详解
- onCreate:Activity 创建时第一个被调用的方法,通常在该方法中加载布局文件,初始化UI组件,事件注册等等
- onStart:在 onCreate 方法之后调用,用于显示界面,但用户还不能进行交互( 这个方法在活动由不可见变为可见的时候调用 )
- onRestart:当一个 stoped 状态的 Activity 被返回时调用,之后再调用 onStart 进入运行状态。( 这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。 )
- onResume:在 onStart 之后调用,该方法执行完成后,用户可以进行交互,当前 Activity 进入 resumed 状态。当一个 paused 状态的 activity 被重新返回时,会再次调用该方法,让 Activity 进入运行状态。( 活动准备好和用户进行交互时调用,此时的活动一定位于返回栈的栈顶,并且处于运行状态 )
- onPause:当其它 Activity (透明或窗口模式)进入时,该方法会被调用,让当前 Activity 进入 paused 状态(暂停状态)。当前 Activity 还可见但不可交互,如果其它更高优先级的 APP 需要内存时,当前 Activity 可能会被销毁(kill)。当前 Activity 被返回时会调用 onResume 方法。( 在系统准备去启动或者恢复另一个活动的时候调用)
- onStop:当其它 Activity 覆盖该 Activity 时,会被调用,当前 Activity 进入 stoped 状态(停止状态)。不可见,如果其它更高优先级的 APP 需要内存时,当前 Activity 可能会被销毁(kill)。 当前 Activity 被返回时会调用 onRestart 方法。( 这个方法在活动完全不可见的时候调用 )
- onDestroy:当前 Activity 被销毁时调用,通常在该方法中用来释放资源,当前 Activity killed。( 这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态 )
以上七个方法除了onRestart()方法,其他都是两两相对的,从而又可以将活动分为三种生存期
- 一、完整生存期:活动在onCreate()方法和onDestroy()方法之间所经历的,就是完整生存期
- 二、可见生存期:活动在onStart()方法和onStop()之间所经历的,在可见生命期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。
- 三、前台生存期:活动在onResume()方法和onPause()方法之间所经历的,这个生存期内,活动总是处于运行状态,此时的活动是可以和用户进行相互的,我们平时看到和接触最多的也是这个状态下的活动。
菜鸟Android开发系列之:Active的生命周期:https://www.iteye.com/blog/limdengrock-1023926
android Activity生命周期详解(图文):https://www.iteye.com/blog/104zz-1685753
安卓活动的生命周期:https://blog.csdn.net/QingKing520/article/details/73496618
安卓活动生命周期:https://blog.csdn.net/techdesign/article/details/80456706
活动生命周期示意图:
注:当 AActivity 切换 BActivity 的所执行的方法:
AActivity: onCreate()->onStart()->onResume()->onPouse()
BActivity: onCreate()->onStart()->onResume()
AActivity: onStop()->onDestory()
当 AActivity 切换 BActivity(此 activity 是以 dialog 形式存在的)所执行的方法:
AActivity: onCreate()->onStart()->onResume()->onPouse()
BActivity: onCreate()->onStart()->onResume()
另一个图:
-
启动
activity
:系统先调用onCreate()
,然后调用onStart()
,最后调用onResume()
方法,activity
进入运行状态。 -
activity
被其他activity覆盖其上(DialogActivity)或者锁屏:系统会调用onPause()
方法,暂停当前activity
的执行。 -
当前
activity
由被覆盖
状态回到前台或者解锁屏
:系统会调用onResume()
方法,再次进入运行状态。 -
当前
Activity
转到新的Activity
界面或按Home
键回到主屏,自身退居后台:系统会先调用onPause
方法,然后调用onStop
方法,进入停滞状态。 -
用户后退回到此
Activity
:系统会先调用onRestart
方法,然后调用onStart
方法,最后调用onResume
方法,再次进入运行状态。 -
当前
Activity
处于被覆盖状态或者后台不可见状态,即第2步和第4步,系统内存不足,杀死当前Activity
,而后用户退回当前Activity
:再次调用onCreate
方法、onStart
方法、onResume
方法,进入运行状态。 -
用户退出当前
Activity
:系统先调用onPause
方法,然后调用onStop
方法,最后调用onDestory
方法,结束当前Activity
。 -
onRestart()
:表示activity
正在重新启动 ,一般情况下,当前activity
从不可见
重新变成可见
状态时,onRestart()
就会被调用,这种情形一般是用户行为所导致的,比如用户按HOME
键切换到桌面然后重新打开APP
或者按back
键。 -
onStart()
:activity
可见了,但是还没有出现在前台,还无法和用户交互。 -
onPause()
:表示activity
正在停止,此时可以做一些存储数据,停止动画等工作,注意不能太耗时,因为这会影响到新activity
的显示,onPause
必须先执行完,新的activity
的onResume
才会执行。 -
从
activity
是否可见来说,onstart()
和onStop()
是配对的,从activity
是否在前台来说,onResume()
和onPause()
是配对的。 -
旧
activity
先onPause
,然后 新activity
在启动
注意:当 activity
中弹出 dialog
对话框的时候,activity不会回调onPause
。然而当activity
启动dialog风格的activity
的时候,此activity会回调onPause函数
。
异常情况下的生命周期:比如当系统资源配置发生改变以及系统内存不足时,activity
就可能被杀死。
- 情况1:资源相关的系统配置发生改变导致
activity
被杀死并重新创建。比如说当前activity
处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,在默认情况下,activity
就会被销毁并且重新创建,当然我们也可以组织系统重新创建我们的activity
。
系统配置发生改变以后,activity
会销毁,其 onPause
,onStop
,onDestory
均会被调用,由于 activity
是在异常情况下终止的,系统会调用 onSaveInstance 来保存当前 activity
状态,这个方法的调用时机是在 onStop之前。与 onPause
没有既定的时序关系,当 activity
重新创建后,系统会调用 onRestoreInstanceState
,并且把 activity
销毁时onSaveInstanceState
方法保存的 Bundle
对象作为参数同时传递给 onRestoreInstanceState 和 onCreate 方法。onRestoreInstanceState() onStart() 方法后回调。同时,在onSaveInstanceState
和onRestoreInstanceState
方法中,系统自动为我们做了一些恢复工作,如:文本框(EditeText
)中用户输入的数据,ListView
滚动的位置等,这些 view相关的状态系统都能够默认为我们恢复。可以查看view
源码,和activity
一样,每个view都有onSaveInstanceState方法和onRestoreInstanceState方法
。
生命周期日志打印:
04-11 09:44:57.350 11757-11757/cn.hotwoo.play:remote I/MainActivity: onCreate
04-11 09:44:57.354 11757-11757/cn.hotwoo.play:remote I/MainActivity: onStart
04-11 09:44:57.356 11757-11757/cn.hotwoo.play:remote I/MainActivity: onResume
04-11 09:44:57.425 11757-11757/cn.hotwoo.play:remote I/MainActivity: onCreateOptionsMenu
04-11 09:44:59.149 11757-11757/cn.hotwoo.play:remote I/MainActivity: onPause
04-11 09:44:59.151 11757-11757/cn.hotwoo.play:remote I/MainActivity: onSaveInstanceState
04-11 09:44:59.151 11757-11757/cn.hotwoo.play:remote I/MainActivity: onStop
04-11 09:44:59.151 11757-11757/cn.hotwoo.play:remote I/MainActivity: onDestroy
04-11 09:44:59.234 11757-11757/cn.hotwoo.play:remote I/MainActivity: onCreate
04-11 09:44:59.235 11757-11757/cn.hotwoo.play:remote I/MainActivity: onStart
04-11 09:44:59.236 11757-11757/cn.hotwoo.play:remote I/MainActivity: onRestoreInstanceState
04-11 09:44:59.237 11757-11757/cn.hotwoo.play:remote I/MainActivity: onResume
04-11 09:44:59.270 11757-11757/cn.hotwoo.play:remote I/MainActivity: onCreateOptionsMenu
04-11 10:02:32.320 11757-11757/cn.hotwoo.play:remote I/MainActivity: onPause
04-11 10:02:32.516 11757-11757/cn.hotwoo.play:remote I/MainActivity: onStop
04-11 10:02:32.516 11757-11757/cn.hotwoo.play:remote I/MainActivity: onDestroy
- 情况2:资源内存不足导致低优先级的
activity
被杀死
这里的情况和前面的情况1数据存储和恢复是完全一致的,activity
按照优先级从高到低可以分为如下三种:
(1)前台activity
---正在和用户交互的activity
,优先级最高
(2)可见但非前台activity
---比如activity
中弹出了一个对话框,导致activity
可见但是位于后台无法和用户直接交互。
(3)后台activity
---已经被暂停的activity
,比如执行了onStop
,优先级最低。
防止重新创建activity:activity
指定 configChange
属性来不让系统重新创建 activity
。android : configChanges = "orientation"
Activity( 活动 ) 与 Fragment ( 碎片 ) 生命周期关系
创建过程:
销毁过程:
Activity 与 menu 创建先后顺序
在 activity
创建完回调 onResume
后创建 menu
,回调 onCreateOptionsMenu
04-05 00:35:03.452 2292-2292/cn.hotwoo.play:remote I/MainActivity: onCreate
04-05 00:35:03.453 2292-2292/cn.hotwoo.play:remote I/MainActivity: onStart
04-05 00:35:03.454 2292-2292/cn.hotwoo.play:remote I/MainActivity: onResume
04-05 00:35:03.482 2292-2292/cn.hotwoo.play:remote I/MainActivity: onCreateOptionsMenu
横竖屏切换时 Activity 的生命周期
此时的生命周期跟清单文件里的配置有关系。
- ① 不设置 Activity 的 android:configChanges 时,横竖屏切换会重新调用各个生命周期,销毁当前 activity,然后重新加载,跟系统配置有关。
- ② onSaveInstanceState()方法会在当前页面销毁前被调用存储数据,onRestoreInstanceState()方法会被执行去取出保存的Bundle对象中的内容,进行一次横竖屏切换时Activity所执行的生命周期方法以及在onSaveInstanceState与onRestoreInstanceState打印相应日志
创建 与 配置
- 创建 一个 Activity 需继承自 android.app.Activity 这个类,然后重写 onCreate(),在 onCreate() 里面调用 setContentView(参数) 来加载布局,参数就是布局文件。
- 配置 则需要在清单文件的 Application 节点下面注册 Actvitiy,如果要首先启动该Activity则添加带有category节点且值为LAUNCHER 的 intent-filter 节点,下面就是清单文件的配置。
<applicationandroid:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activityandroid:name=".MainActivity"android:label="@string/app_name" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application>
3、 四种启动模式
Activity 的启动模式决定了激活 Activity 时,是否创建新的对象,进而将影响到 任务栈 也叫 回退栈。
在 AndroidManifest.xml 文件中,可以为每个 activity 节点配置 android:launchMode 属性,以决定该 Activity 的启动模式,该属性的值有:
- Standard 模式 : ( 默认 ) 标准模式:每次激活 Activity 时,都会创建新的 Activity 对象。standard 模式是 android 的默认启动模式,在这种模式下,activity 可以有多个实例,每次启动 Activity,无论任务栈中是否已经存在这个 activity 的实例,系统都会创建一个新的 activity 实例。即 在这种模式下,activity默认会进入启动它的activity所属的任务栈中。 注意:在非activity类型的context(如ApplicationContext)并没有所谓的任务栈,所以不能通过ApplicationContext去启动standard模式的activity。
- SingleTop 模式: 栈顶模式,也叫栈顶复用模式。当一个 singleTop 模式的 activity 已经位于栈顶位置时,再去启动它时,不会再创建实例,即每次只是激活但并不会创建新的 Activity 对象,如果不在栈顶,就会创建实例。( 如果新activity位于任务栈的栈顶的时候,activity不会被重新创建,同时它的onNewIntent方法会被回调。 注意:这个activity的onCreate,onStart,onResume不会被回调,因为他们并没有发生改变。 )
- SingleTask 模式 : 单任务模式,也叫栈内复用模式。如果启动的这个 activity 已经存在于 任务栈 中,则会将该 activity 移动到栈顶,并将该 activity 上面的所有 activity 出栈,否则创建新的实例。( 只要activity在一个栈中存在,那么多次启动此activity不会被重新创建单例,系统会回调onNewIntent。比如activityA,系统首先会寻找是否存在A想要的任务栈,如果没有则创建一个新的任务栈,然后把activityA压入栈,如果存在任务栈,然后再看看有没有activityA的实例,如果实例存在,那么就会把A调到栈顶并调用它的onNewIntent方法,如果不存在则把它压入栈。 )
- SingleInstance 模式 :单实例模式,一个 activity 一个栈,即 activity只能单独地位于一个任务栈 中。( 实例(对象)唯一,确保该 Activity 的对象一定只有1个,被设置为 singleInstance 的 Activity 将被置于一个专门的任务栈中,且该任务栈中有且仅有一个 Activity。 )
注意:默认情况下,所有 activity 所需的 任务栈 的名字为应用的包名,可以通过给 activity 指定 TaskAffinity 属性来指定任务栈,***** 这个属性值不能和包名相同,否则就没有意义 *****。
什么 是 任务栈(回退栈):
任务栈 是 用来存放所有激活了的 Activity 对象,激活的 Acitvity 将会按照后进先出的栈结构显示出来。因为屏幕只能显示一个Activity,当有新的 Activity 被激活时,原来正在显示的 Activity 就会进行压栈操作被压到新 Activity 对象下方的位置。当按下”Back” 键时栈顶 Activity 会执行弹栈操作,而在第 2 位的 Activity 将获得栈顶位置,显示在前台。
4、三种跳转方式
显示启动 :Intrent 内部直接声明要启动的activity所对应的的class
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intnet);
隐式启动:进行三个匹配,一个是activity,一个是category,一个是data,全部或者部分匹配,应用于广播原理
- 清单文件中 里配置 activity 属性,activity 的名字要和跳转内容一样
<activity android:name="com.exanple.android.tst.secondActivity"android:label = @string/title><intent=filter><action android:name="com.exanple.android.tst.secondActivity/><category android:name="android.intent.category.DEFAULT"/><intent-filter/>
</activity>
- 在需要跳转的地方
Intent intent = new Intent("com.example.android.tst.secondActivity");
startActivity(intnet);
跳转后再返回,能获取返回值
Intent in = new Intent(MainActivity.this,OtehrActivity.class);
in.putExtra("a",a);
startActivityForResult(in,1000);
在OTherActivity中设置返回值
Intent int = new Intent();
int.putExtra("c",c);
setResult(1001,int);
finish();
在MainActivity中获取返回值
@Override
protected void onActivityResult(int requestCode, int resultCode ,Intent data) {super.onActivityResult(requestCode,resultCode,data);if(requestCode == 1000){if(resultCode == 1001){int c = data.getExtra("c",0);}}
}
Intent 与 IntentFilter
Intent —— 意图
用于 android 个组件的启动和组件间传递数据
属性:
- component 目标组件描述
- action 对Intent执行动作的描述
- data 对此次Intent操作相关数据的描述
- type 对Intent所关联的数据类型的描述
- category 对Intent执行动作的附加信息描述
- extra 对其他一切附加信息的描述,他是对其他所有附加信息的集合
Intent-Filter —— 意图 过滤器
对 Intent 的描述进行过滤操作,对 Intent 的各个属性进行匹配,从而选择出相应的组件来执行 Intent 想要进行的操作
定义
<Intent-Filter><action name=“xxxxxxxxx”/><action name=“yyyyyyyy”/><category name=“ttttttttttt”/><category name=“uuuuuuu”/><data><data>
</Intent-Filter>
在 IntentFilter 中 action、category、data 都可以存在多个
匹配原则
action:Intent 中的 action 只要和 IntentFilter 中的任意一个 action 一样即可category:Intent 中添加的 category 必须全部在 IntentFileter 定义的 category 中data:Intent 中的 data 要和 IntentFilter 中的 data 的描述匹配type:Intent 中的 type 要和 IntentFilter 中 data 要求的 mimeType 一直
前台进程:
- 1.当前进程activity正在与用户进行交互。
- 2.当前进程service正在与activity进行交互或者当前service调用了startForground()属于前台进程或者当前service正在执行生命周期(onCreate(),onStart(),onDestory())
- 3.进程持有一个BroadcostReceiver,这个BroadcostReceiver正在执行onReceive()方法
可见进程:
- 1. 进程持有一个activity,这个activity不再前台,处于onPouse()状态下,当前覆盖的activity是以dialog形式存在的。
- 2. 进程有一个service,这个service和一个可见的Activity进行绑定。
service进程:
- 当前开启startSerice()启动一个service服务就可以认为进程是一个服务进程。
后台进程:
- activity的onStop()被调用,但是onDestroy()没有调用的状态。该进程属于后台进程。
空进程:
- 改进程没有任何运行的数据了,且保留在内存空间,并没有被系统killed,属于空进程。该进程很容易被杀死。
1.2 service
service(服务)是安卓中的四大组件之一,它通常用作在后台处理耗时的逻辑,与 Activity 一样,它存在自己的生命周期,也需要在 AndroidManifest.xml 配置相关信息。
开发人员需要在应用程序配置文件中声明全部的 service,使用 <service></service> 标签。
Service 通常位于后台运行,它一般不需要与用户交互,因此 Service 组件没有图形用户界面。Service 组件需要继承Service 基类。Service 组件通常用于为其他组件提供后台服务或监控其他组件的运行状态。
定义 与 作用
- Service(服务)是一个没有用户界面的专门在后台处理耗时任务的 Android 组件,它没有UI。它有两种启动方式,startService和bindService。其他应用组件能够启动 Service,并且当用户切换到另外的应用场景,Service将持续在后台运行。另外,一个组件能够绑定到一个service与之交互(IPC机制),例如,一个service可能会处理网络操作,播放音乐,操作文件I/O或者与内容提供者(content provider)交互,所有这些活动都是在后台进行,以上是Google文档的解释,资料来源于大神博客( https://blog.csdn.net/ryantang03/article/details/7770939 )
- Service 还有一个作用就是提升进程(每个应用都是一个进程)的优先级,进程的优先级指的是在 Android 系统中,会把正在运行的应用确定一个优先级,当内存空间不足时,系统会根据进程的优先级清理掉一部分进程占用的内存空间,以获得足够的内存空间以供新启用的应用运行。详细的进程优先级划分如下:
1) 前台进程:应用程序存在Activity正位于前台,可见并可控
2) 可见进程:应用程序存在Activity处于局部可见状态,即局部可见却不可控
3) 服务进程:应用程序存在正在运行的Service
4) 后台进程:应用程序的所有Activity均被置于后台,没有任何Activity可见
5) 空进程:已经退出的应用程序。
service 的进程优先级详细介绍请参考这篇博文:https://blog.csdn.net/fhy_2008/article/details/7328967
service 特性
- 【1】Service 的粘性:
当 Service 被意外终止(非正常停止,即不是通过 stopService() 或 stopSelf() 停止)后,会在未来的某一刻自动重启。
Service 的粘性是通过 onStartCommand() 方法的返回值确定的,可用的值有:
—– Service.START_REDELIVER_INTENT -----> 粘性的,且在自动重启时,会重新给Service发送Intent对象。
—– Service.START_STICKY -----> 粘性的
—– Service.START_NOT_STICKY -----> 非粘性的
—– Service.START_STICKY_COMPATIBILITY -----> 粘性的,并且兼容的
当需要 Service 是非粘性的,取值 Service.START_NOT_STICKY;当需要 Service 是粘性的,并且还需要获取 Intent 对象时,取值 Service.START_REDELIVER_INTENT;否则,只是需要粘性的,不需要 Intent 时,取值super.onStartCommand() 默认值。 - 【2】Service 是单例的,在程序中一个 Service 类只会存在一个对象
- 【3】Service 是没有界面的,适合于在后台进行耗时操作,但要注意 Service 仍然是运行在主线程中的,故耗时的操作还是需要开启子线程来进行。
service 用于在 后台 完成用户指定的操作,service 有 2 种启动方式
- 第一种启动方式:通过 start 方式开启服务,即 startService(启动)。
started(启动):当应用程序组件(如 activity)调用 startService() 方法启动服务时,服务处于 started 状态。
使用 service 的步骤:
1,定义一个类继承 service
2,manifest.xml 文件中配置 service
3,使用 context 的 startService(Intent) 方法启动 service
4,当不在使用时,调用 stopService(Intent) 方法停止服务。
使用 start 方式启动的生命周期:onCreate() -- > onStartCommand() -- > onDestory()注意:如果服务已经开启,不会重复回调 onCreate() 方法,如果再次调用 context.startService() 方法,service 而是会调用onStart() 或者 onStartCommand() 方法。停止服务需要调用 context.stopService() 方法,服务停止的时候回调 onDestory 被销毁。
特点:一旦服务开启就跟调用者(开启者)没有任何关系了。开启者退出了,开启者挂了,服务还在后台长期的运行,开启者不能调用服务里面的方法。
- 第二种启动方式:采用 bind 的方式开启服务,即 bindService(绑定服务)。
bound(绑定):当应用程序组件调用 bindService() 方法绑定到服务时,服务处于 bound 状态。
使用 service 的步骤:
1,定义一个类继承 Service
2,在 manifest.xml 文件中注册 service
3,使用 context 的 bindService(Intent, ServiceConnection, int) 方法启动 service
4,不再使用时,调用 unbindService(ServiceConnection) 方法停止该服务。
使用这种 bind 方式启动的 service 的生命周期如下:onCreate() -- > onBind() --> onUnbind() -- > onDestory()注意:绑定服务不会调用 onStart() 或者 onStartCommand() 方法
特点:bind 的方式开启服务,绑定服务,调用者挂了,服务也会跟着挂掉。绑定者可以调用服务里面的方法。
下图形象地说明了 Service 两种状态的生命周期
通过这个图可以看到,两种启动 service 的方式以及他们的生命周期,bind service 的不同之处在于当绑定的组件销毁后,对应的service 也就被 kill 了。service 的声明周期相比与 activity 的简单了许多,只要好好理解两种启动 service 方式的异同就行。
startService() 与 bindService() 区别:
- startService 是由其他组件调用 startService() 方法启动的,只是启动 Service,这导致服务的 onStartCommand() 方法被调用。当服务是 started 状态时,其生命周期与启动它的组件无关,并且可以在后台无限期运行,即使启动服务的组件已经被销毁。因此,服务需要在完成任务后调用 stopSelf() 方法停止,或者由其他组件调用 stopService() 方法停止。即启动它的组件(如Activity)和 Service 并没有关联,只有当 Service 调用 stopSelf 或者其他组件调用 stopService 服务才会终止。
- bindService 方法启动 Service,其他组件可以通过回调获取 Service 的代理对象和 Service 交互,而这两方也进行了绑定,当启动方销毁时,Service 也会自动进行 unBind 操作,当发现所有绑定都进行了unBind 时才会销毁 Service。
- 总结:started service(启动服务)是由其他组件调用 startService() 方法启动的。使用 bindService() 方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。
关于 Service 生命周期还有一张比较易懂的图(来源于网络)
总结:
- 启动 service:用 Context 类 定义的 startService(Intent) 即可启动 Service 组件,其中 intent 定义方法与跳转 Activity 类似,只需把 Actvity类 换成 Service类 即可。其生命周期为启动时 onCreate() –> onStartCommand() –> 销毁时的 onDestroy(),反复调用 startService() 只会导致 Service 反复执行 onStartCommand()
- 停止 service:调用 Context类 定义的 stopService(Intent)即可停止 Service 组件,反复调用并没有任何效果,亦不会报告错误,即:即使停止没有启动的 Service 也不会出错。也可以在 Service类 的内部,调用 Service 定义的 stopSelf() 方法,停止当前 Service。
- 绑定:主要作用是实现组件间的通信,实质的表现是 Activity 可以调用 Service 中的方法,使 Service 执行特定的业务,并且这些方法可以是带返回值的方法,进而 Activity 可以通过获取这些返回值,这样就实现与 Service 的通信。
【生命周期】
– onCreate() ---> 当第1次绑定时执行
– onBind() ---> 当第1次绑定时执行
– onDestroy() ---> 当解除绑定时执行
【绑定与解绑】
调用 bindService() 方法可以实现 Activity 与 Service 的绑定,调用 unbindService() 可以解除绑定。在 Activity 被销毁之前,必须解除与 Service 的绑定。
具体实现代码如下:
//在Activity中调用bindService()来实现服务绑定
Intent intent =new Intent(this,MyService.class);
//用于连接的对象,相当于组件间的一个连接纽带
ServiceConnection conn=new ServiceConnection(){@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// 当Service已经连接//参数IBinder service就是与service进行通信的对象,通过这个对象可以调用Service里的方法}@Overridepublic void onServiceDisconnected(ComponentName name) {// 当Service断开连接}
};
//绑定标志
int FLAGS=BIND_AUTO_CREATE;
bindService(intent,conn,FLAGS);
//然后在Activity销毁时解绑,这里需要一个连接对象,所以需要在上面把conn设为全局变量
@Overrideprotected void onDestroy() {unbindService(conn);super.onDestroy();}//MyService类需要继承自Service,还需要在清单文件中注册
public class MyService extends Service {@Overridepublic void onCreate() {}@Overridepublic IBinder onBind(Intent intent) {//这里需要返回一个IBinder对象,我们可以创建一个内部类来获得这个对象MyBinder binder = new MyBinder(); return binder;}/**这个内部类就是我们返回的IBinder类Activity的conn里连接上后就是得到这个对象才实现了组件间的通信*/public class MyBinder extends Binder {//这个内部类可以写很多方法,来让Activity调用,还可以是带有返回值的方法}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {}}
service 生命周期也涉及一些回调方法,这些方法都不用调用父类方法,具体如下:
public class ExampleService extends Service {int mStartMode; // indicates how to behave if the service is killedIBinder mBinder; // interface for clients that bindboolean mAllowRebind; // indicates whether onRebind should be used@Overridepublic void onCreate() {// The service is being created}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {// The service is starting, due to a call to startService()return mStartMode;}@Overridepublic IBinder onBind(Intent intent) {// A client is binding to the service with bindService()return mBinder;}@Overridepublic boolean onUnbind(Intent intent) {// All clients have unbound with unbindService()return mAllowRebind;}@Overridepublic void onRebind(Intent intent) {// A client is binding to the service with bindService(),// after onUnbind() has already been called}@Overridepublic void onDestroy() {// The service is no longer used and is being destroyed}
}
关于 Service 还有很多知识,这里就不再一一列举,可以参考:http://developer.android.com/guide/components/services.html
通过 start 方法开启服务
- 创建一个类继承Service
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;public class MyService extends Service {public MyService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}
}
- 在清单文件中注册这个服务
<serviceandroid:name=".MyService"android:enabled="true"android:exported="true"></service>
- 通过startService方法启动服务
Intent intent = new Intent(this, MyService.class);startService(intent);
- 当不用服务的时候通过stopService()方法停止该服务
stopService(intent);
特点: 通过start方法启动的service一旦服务开启就跟调用者(开启者)没有任何关系了。开启者退出了,开启者挂了,服务还在后台长期的运行,开启者不能调用服务里面的方法。
通过 bind 的方式启动服务
- 创建一个类继承Service
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;public class MyService extends Service {public MyService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}
}
- 在清单文件中注册这个服务
<serviceandroid:name=".MyService"android:enabled="true"android:exported="true"></service>
- 通过startService方法启动服务
Intent intent = new Intent(this, MyService.class);bindService(Intent,ServiceConnection,int);
- 当不用服务的时候通过stopService()方法停止该服务
unbindService(ServiceConnection);
特点:使用bind方法启动的服务,则调用者挂了,服务也挂了,调用者可以调用服务中的方法
定义一个 Server
项目内 Server 包 右键 --> New --> Service --> Service 或者直接创建 Class 类,继承 Service 并重写 IBinder 方法
public class MyService extends Service{public MyService(){}@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {// TODO Auto-generated method stubsuper.onDestroy();}
}
重写 Service 的 onCreate()、onStartCommand() 和 onDestory() 方法。其中 onCreate() 方法在服务创建的时候调用、onStartCommand() 方法会在每次服务启动的时候调用、onDestory() 方法会在服务销毁的时候调用。
通常情况下,如果我们希望服务一旦启动就立刻去执行任务,就可以将逻辑卸载 onStartCommand() 方法里。
另外需要注意的是,每个服务都需要在 Androidmanifest.xml 中进行注册才能生效:
<application....>...<serviceandroid:name=".MyService"android:enabled="true"android:exported="true"></service>
</application>
示例:定义一个类继承 service
//本地service不涉及进程间通信
public class MyService extends Service {private String TAG = "MyService";@Overridepublic void onCreate() {super.onCreate();Log.i(TAG,"onCreate");}@Overridepublic void onStart(Intent intent, int startId) {super.onStart(intent, startId);Log.i(TAG,"onStart");}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {Log.i(TAG,"onStartCommand");return super.onStartCommand(intent, flags, startId);}//绑定服务时调用这个方法,返回一个IBinder对象@Nullable@Overridepublic IBinder onBind(Intent intent) {Log.i(TAG,"onBind");return new MyBinder();}@Overridepublic boolean onUnbind(Intent intent) {Log.i(TAG,"onUnbind");return super.onUnbind(intent);}// 停止服务,通过调用Context.unbindService(),别忘了service也继承了Context类
// @Override
// public void unbindService(ServiceConnection conn) {
// super.unbindService(conn);
// Log.i(TAG,"unbindService");
// }//服务挂了@Overridepublic void onDestroy() {super.onDestroy();Log.i(TAG,"onDestroy");}public interface MyIBinder{void invokeMethodInMyService();}public class MyBinder extends Binder implements MyIBinder{public void stopService(ServiceConnection serviceConnection){unbindService(serviceConnection);}@Overridepublic void invokeMethodInMyService() {for(int i =0; i < 20; i ++){System.out.println("service is opening");}}}
在 manifest.xml 文件中注册 service
//Service 必须要注册<service android:name=".server.MyService"android:exported="true"><intent-filter><action android:name="cn.hotwoo.play.server.MyService"/><category android:name="android.intent.category.default" /></intent-filter></service>
绑定自定义的 service
public class CustomActivity extends AppCompatActivity {private Button startService, unbindService;private MyService.MyBinder myBinder;private ServiceConnection serviceConnection;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_custom);startService = (Button) findViewById(R.id.service_start);unbindService = (Button) findViewById(R.id.unbind_service);startService.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {
// startService(new Intent(CustomActivity.this, MyService.class));serviceConnection = new MyServiceConnection();bindService(new Intent(CustomActivity.this, MyService.class), serviceConnection, Context.BIND_AUTO_CREATE);}});unbindService.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {unbindService(serviceConnection);}});}class MyServiceConnection implements ServiceConnection {//这里的第二个参数IBinder就是Service中的onBind方法返回的@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {Log.i("MyService", "onServiceConnected");myBinder = (MyService.MyBinder) service;}@Overridepublic void onServiceDisconnected(ComponentName name) {Log.i("MyService", "onServiceDisconnected");}}
}
startService输出日志:
04-01 19:56:09.846 22845-22845/cn.hotwoo.play I/MyService: onCreate
04-01 19:56:09.854 22845-22845/cn.hotwoo.play I/MyService: onStartCommand
04-01 19:56:09.854 22845-22845/cn.hotwoo.play I/MyService: onStart
bindService 输出日志:
04-01 19:53:21.459 14704-14704/cn.hotwoo.play I/MyService: onCreate
04-01 19:53:21.460 14704-14704/cn.hotwoo.play I/MyService: onBind
04-01 19:53:21.461 14704-14704/cn.hotwoo.play I/MyService: onServiceConnected
点击back键关闭activity或者调用Context.unbindService()方法后:
04-05 01:16:27.508 11427-11427/cn.hotwoo.play I/MyService: onUnbind
04-05 01:16:27.508 11427-11427/cn.hotwoo.play I/MyService: onDestroy
远程服务
调用者 和 service 不在同一个进程中,service 在单独的进程中的 main 线程,是一种垮进程通信方式。学习地址
绑定远程服务的步骤:
- 在服务的内部创建一个内部类,提供一个方法,可以间接调用服务的方法
- 把暴露的接口文件的扩展名改为.aidl文件 去掉访问修饰符
- 实现服务的onbind方法,继承Bander和实现aidl定义的接口,提供给外界可调用的方法
- 在 activity 中绑定服务。bindService()
- 在服务成功绑定的时候会回调 onServiceConnected方法 传递一个 IBinder对象
- aidl 定义的接口.Stub.asInterface(binder) 调用接口里面的方法
IntentService
IntentService 是 Service 的子类,比普通的 Service 增加了额外的功能。
Service 分为两种:
- 一种是 Service(这一种是运行在主线程中的,如果要执行耗时操作,可在service中创建一个异步来执行)。
- 一种是 IntentService(这是一种异步服务,是继承于Service 的子类),所以推荐当要执行耗时操作时使用 IntentService,如果不耗时,我们可以使用 Service
Service 本身存在两个问题:
- Service不会专门启动一条单独的进程,Service与它所在应用位于同一个进程中;
- Service也不是专门一条新线程,因此不应该在Service中直接处理耗时的任务;
IntentService 特征:
- 会创建独立的 worker 线程来处理所有的 Intent 请求;
- 会创建独立的 worker 线程来处理 onHandleIntent() 方法实现的代码,无需处理多线程问题;
- 所有请求处理完成后,IntentService 会自动停止,无需调用 stopSelf() 方法停止 Service;
- 为 Service 的 onBind() 提供默认实现,返回 null;
- 为 Service 的 onStartCommand 提供默认实现,将请求 Intent 添加到队列中;
IntentService 相比 父类Service 而言,最大特点是其回调函数 onHandleIntent 中可以直接进行耗时操作,不必再开线程。其原理是IntentService 的成员变量 Handler在初始化时已属于工作线程,之后 handleMessage,包括 onHandleIntent 等函数都运行在工作线程中。
IntentService 还有一个特点,就是多次调用 onHandleIntent函数(也就是有多个耗时任务要执行),多个耗时任务会按顺序依次执行。原理是其内置的 Handler 关联了任务队列,Handler 通过 looper 取任务执行是顺序执行的。这个特点就能解决多个耗时任务需要顺序依次执行的问题。而如果仅用 service,开多个线程去执行耗时操作,就很难管理。
官方文档的说明:
IntentService
This is a subclass of Service
that uses a worker thread to handle all start requests, one at a time. This is the best option if you don't require that your service handle multiple requests simultaneously. All you need to do is implement onHandleIntent()
, which receives the intent for each start request so you can do the background work.
IntentService 使用队列的方式将请求的 Intent 加入队列,然后开启一个 worker thread(线程) 来处理队列中的 Intent,对于异步的startService 请求,IntentService 会处理完成一个之后再处理第二个,每一个请求都会在一个单独的 worker thread 中处理,不会阻塞应用程序的主线程,这里就给我们提供了一个思路,如果有耗时的操作与其在Service里面开启新线程还不如使用IntentService 来处理耗时操作。而在一般的继承 Service 里面如果要进行耗时操作就必须另开线程,但是使用 IntentService 就可以直接在里面进行耗时操作,因为默认实现了一个 worker thread。对于异步的 startService 请求,IntentService 会处理完成一个之后再处理第二个。
IntentService
- 内部有一个工作线程来完成耗时的操作,只需实现 onHandleIntent 方法即可
- 完成工作后会自动终止服务
- 如果同时执行多个任务时,会以工作队列的方式,一次执行
- 通过该类来完成本 APP 中耗时的工作
看下 IntentService 的具体实现:
public class HelloIntentService extends IntentService {/** * A constructor is required, and must call the super IntentService(String)* constructor with a name for the worker thread.*/public HelloIntentService() {super("HelloIntentService");}/*** The IntentService calls this method from the default worker thread with* the intent that started the service. When this method returns, IntentService* stops the service, as appropriate.*/@Overrideprotected void onHandleIntent(Intent intent) {// Normally we would do some work here, like download a file.// For our sample, we just sleep for 5 seconds.long endTime = System.currentTimeMillis() + 5*1000;while (System.currentTimeMillis() < endTime) {synchronized (this) {try {wait(endTime - System.currentTimeMillis());} catch (Exception e) {}}}}
}
关于停止 Service,如果 service 是非绑定的,最终当任务完成时,为了节省系统资源,一定要停止 service,可以通过 stopSelf()来停止,也可以在其他组件中通过 stopService() 来停止,绑定的 service 可以通过 onUnBind() 来停止 service。
启动 和 停止 服务
启动服务:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); //启动服务
停止服务:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); //停止服务
使用前台服务
前台服务与普通服务的最大区别在于,它会一直有一个正在运行的图标在系统的状态栏中,下拉状态栏后可以看到更加详细的内容,非常类似于通知的效果。
public class MyService extends Service{Intent intent = new Intent(this, MainActivity.class);PendingIntent pi = PendingIntent.getActivity(this, 0 , intent, 0);Notification notification = new NotificationCompat.Builder(this).setContentTitle(" this is content titile").setContentText("this is content text").setWhen(System.currentTimeMillis()).setSmallIcon(R.mipmap.ic_launcher);.setLargeIcon(BitmapFactory.decodeResource(getResource(),R.mipmap.ic_launcher)).setContentIntent(pi).build();startForeground(1,notification);
}
构造一个Notification对象后并没有使用NotificationManager 来讲通知显示出来,而是调用了startForeground()方法,该方法会将MyService变成一个前台服务,并在系统状态栏中显示出来。
使用 IntentService
服务中的代码都默认运行在主线程中,如果直接在服务中执行耗时操作很容易出现ANR(Application not Responding)
所以这个时候需要用到Android多线程编程技术,我们应该在服务的每个具体的方法里启动一个子线程,然后在这里去处理那些耗时的操作:
public class MyService extends Service{...@Overridepublic int onStartCommand(Intent intent , int flags, int startId){new Thread(new Runnable(){public void run(){//处理具体的逻辑}}).start();return super.onStartCommand(intent, flags, startId);}
}
但是,这种服务一旦启动之后,就会一直处于运行状态,必须调用stopService()或者stopSelf()方法才能让服务停止下来,所以,如果想要实现让一个服务在执行完毕后自动停止的功能,就可以这样写:
public class MySerivce extends Servcie{...@Overridepublic int onStartCommand(Intent intent, int flats , int startId){new Thread(new Runnable(){public void run(){//处理具体的逻辑stopSelf();}});}
}
虽说这样的写法并不复杂,但是总会有一些程序员忘记开启线程或者忘记调用stopSelf() 方法。为了简单创建一个异步、会自动停止的服务。Android专门提供了一个IntentService类
public class MyIntentService extends IntentService{public MyIntentService(){super("MyIntentService"); //调用父类的有参构造方法}@Overrideprotected void onHandleIntent(Intent intent){ //打印当前的线程IDLog.e("mylog","Thread id is” + Thread.cuttentThread().getId();}@Overridepublic void onDestory(){super.onDestory();Log.e("mylog","on Destory executed");}
}
首先这里提供一个无参的构造方法,并且必须在其内部调用父类的有参构造方法。然后要在子类中去实现onHandleIntent() 这个抽象方法,在这个方法中可以去处理一些逻辑,而且不用担心ANR,因为这个方法已经是在子线程中运行了。
IntentService线程的调用:
Intent intent = new Intent(this, MyIntentService.class);
startServcie(intent);
如此,线程就会自动启动并执行逻辑,执行完毕后自动关闭。这就是IntentService 的好处,能够自动开启和关闭;
1.3 content provider
contentprovider 是 android 四大组件之一的内容提供器,它主要的作用就是将程序的内部的数据和外部进行共享,为数据提供外部访问接口,被访问的数据主要以数据库的形式存在,而且还可以选择共享哪一部分的数据。这样一来,对于程序当中的隐私数据可以不共享,从而更加安全。contentprovider 是 android中 一种跨程序共享数据的重要组件。
- (1)android 平台提供了 ContentProvider 把一个应用程序的指定数据集提供给其他应用程序。其他应用可以通过ContentResolver类 从该内容提供者中获取或存入数据。
- (2)只有需要在多个应用程序间共享数据是才需要 内容提供者。例如,通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处是统一数据访问方式。
- (3)ContentProvider 实现数据共享。ContentProvider 用于保存和获取数据,并使其对所有应用程序可见。这是不同应用程序间共享数据的唯一方式,因为 android 没有提供所有应用共同访问的公共存储区。
- (4)开发人员不会直接使用 ContentProvider 类的对象,大多数是通过 ContentResolver 对象实现对 ContentProvider 的操作。
- (5)ContentProvider 使用 URI 来唯一标识其数据集,这里的 URI 以 content:// 作为前缀,表示该数据由 ContentProvider来管理。 4 大基本组件都需要注册才能使用,每个 Activity、service、Content Provider 都需要在 AndroidManifest 文件中进行配置。AndroidManifest 文件中未进行声明的 activity、服务 以及 内容提供者 将不为系统所见,从而也就不可用。而 broadcast receiver 广播接收者的注册分静态注册(在AndroidManifest文件中进行配置)和通过代码动态创建并以调用Context.registerReceiver()的方式注册至系统。需要注意的是在AndroidManifest文件中进行配置的广播接收者会随系统的启动而一直处于活跃状态,只要接收到感兴趣的广播就会触发(即使程序未运行)。
1. 作用: content provider 中文意思是 内容提供者,ContentProvider 可以将应用程序自身的数据对外(对其它应用程序)共享,使得其它应用可以对自身的数据进行增、删、改、查操作。
使用系统的 ContentProvider:Android 系统使用了许多 ContentProvider,将系统中的绝大部分常规数据进行对外共享,系统的 ContentProvider 有很多,例如:通讯录、通话记录、短信、相册、歌曲、视频、日历等等,一般这些数据都存放于一个个的数据库中。同时这些数据一般都需要和第三方的 app 进行共享数据。既然是使用系统的,那么 contentprovider 的具体实现就不需要我们担心了,使用内容提供者的步骤如下
- 获取 ContentResolver 实例
- 确定 Uri 的内容,并解析为具体的 Uri 实例
- 通过 ContentResolver 实例来调用相应的方法,传递相应的参数,但是第一个参数总是 Uri,它制定了我们要操作的数据的具体地址
可以通过读取系统通讯录的联系人信息,显示在Listview中来实践这些知识。不要忘记在读取通讯录的时候,在清单文件中要加入相应的读取权限。
自定义 ContentProvider
系统的 contentprovider 在与我们交互的时候,只接受了一个 Uri 的参数,然后根据我们的操作返回给我们结果。系统到底是如何根据一个 Uri 就能够提供给我们准确的结果呢?只有自己亲自实现一个看看了。
和之前提到的一样,想重新自定义自己程序中的四大组件,就必须重新实现一个类,重写这个类中的抽象方法,在清单文件中注册,最后才能够正常使用。
重新实现 ContentProvider 之后,发现我们重写了 6 个重要的抽象方法
- oncreate
- query
- update
- insert
- delete
- gettype
大部分的方法在数据库那里已经见过了,他们内部的逻辑可想而知都是对数据的增删改查操作,其中这些方法的第一个参数大多都是 Uri 实例。其中有两个方法比较特殊:
- oncreate 方法应该是内容提供者创建的时候所执行的一个回调方法,负责数据库的创建和更新操作。这个方法只有我们在程序中获取ContentResolver实例之后准备访问共享数据的时候,才会被执行。
- gettype 方法是获取我们通过参数传递进去的Uri的MIME类型,这个类型是什么,后面会有实例说明。
内容提供者首先要做的一个事情就是将我们传递过来的 Uri 解析出来,确定其他程序到底想访问哪些数据。
Uri 的形式一般有两种:
- 1,以路径名为结尾,这种 Uri 请求的是整个表的数据,如: content://com.demo.androiddemo.provider/tabl1 标识我们要访问tabl1 表中所有的数据
- 2,以 id 列值结尾,这种 Uri 请求的是该表中和其提供的列值相等的单条数据。 content://com.demo.androiddemo.provider/tabl1/1 标识我们要访问 tabl1 表中_id 列值为 1 的数据。
如果是内容提供器的设计者,那么我们肯定知道这个程序的数据库是什么样的,每一张表,或者每一张表中的 _id 都应该有一个唯一的内容 Uri 。我们可以将传递进来的 Uri 和我们存好的 Uri 进行匹配,匹配到了之后,就说明数据源已经找到,便可以进行相应的增删改查操作。
ContentProvider详解
2. 访问 Content Provider
Content Provider 以一个或多个表(与在关系型数据库中找到的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。
应用从具有 ContentResolver对象的Content Provider访问数据。 此对象具有调用提供程序对象(ContentProvider 的某个具体子类的实例)中同名方法的方法。 ContentResolver 方法可提供持续存储的基本“CRUD”(创建、检索、更新和删除)功能。
客户端应用进程中的 ContentResolver 对象和拥有提供程序的应用中的 ContentProvider 对象可自动处理跨进程通信。 ContentProvider 还可充当其数据存储区和表格形式的数据外部显示之间的抽象层。
注:要访问提供程序,您的应用通常需要在其清单文件中请求特定权限。 内容提供程序权限部分详细介绍了此内容。
例如,要从用户字典提供程序中获取字词及其语言区域的列表,则需调用 ContentResolver.query()。 query() 方法会调用用户字典提供程序所定义的 ContentProvider.query() 方法。 以下代码行显示了 ContentResolver.query() 调用:
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(UserDictionary.Words.CONTENT_URI, // The content URI of the words table,URI映射的数据表名mProjection, // The columns to return for each row,我们所要我的选择查询的表的列名,字符串数组mSelectionClause // Selection criteria,查询条件相当于SQL中的where a =? and b = ?mSelectionArgs, // Selection criteria,字符串数组,替代上面条件中的?占位符mSortOrder); // The sort order for the returned rows,排列的顺序相当于SQL中的order(字段)
3. URI 资源访问格式
内容 URI 是用于在 Content Provider 中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表的名称(路径)。 当您调用客户端方法来访问提供程序中的表时,该表的内容 URI 将是其参数之一。
在前面的代码行中,常量 CONTENT_URI 包含用户字典的“字词”表的内容 URI。 ContentResolver 对象会分析出 URI 的授权,并通过将该授权与已知提供程序的系统表进行比较,来“解析”提供程序。 然后, ContentResolver 可以将查询参数分派给正确的提供程序。
ContentProvider 使用内容 URI 的路径部分来选择要访问的表。 提供程序通常会为其公开的每个表显示一条路径。
在前面的代码行中,“字词”表的完整 URI 是:
content://user_dictionary/words
其中,user_dictionary 是提供程序的授权,需要在清单文件中注册,words 是表的路径; content://(架构)始终显示,并将此标识为内容 URI 。
许多提供程序都允许您通过将 ID 值追加到 URI 末尾来访问表中的单个行。 例如,要从用户字典中检索 _ID 为 4 的行,则可使用此内容 URI :
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
在检索到一组行后想要更新或删除其中某一行时通常会用到 ID 值。
注:Uri 和 Uri.Builder 类包含根据字符串构建格式规范的 URI 对象的便利方法。 ContentUris 包含一些可以将 ID 值轻松追加到 URI 后的方法。 前面的代码段就是使用 withAppendedId() 将 ID 追加到 UserDictionary 内容 URI 后。
4. 显示数据
ContentResolver.query() 方法始终会返回符合以下条件的 Cursor:包含查询的表为匹配查询选择条件的行指定的列, Cursor 对象为其包含的行和列提供随机读取访问权限。 通过使用 Cursor 方法,您可以循环访问结果中的行、确定每个列的数据类型、从列中获取数据,并检查结果的其他属性。 某些 Cursor 实现会在提供程序的数据发生更改时自动更新对象或在 Cursor 更改时触发观察程序对象中的方法。
注:提供程序可能会根据发出查询的对象的性质来限制对列的访问。 例如,联系人提供程序会限定只有同步适配器才能访问某些列,因此不会将它们返回至 Activity 或服务。
如果没有与选择条件匹配的行,则提供程序会返回 Cursor.getCount() 为 0(空游标)的 Cursor 对象。
如果出现内部错误,查询结果将取决于具体的提供程序。它可能会选择返回 null,或引发 Exception。
由于 Cursor 是行“列表”,因此显示 Cursor 内容的较好方式是通过 SimpleCursorAdapter 将其与 ListView 关联。
以下代码段会创建一个包含由查询检索到的 Cursor 的 SimpleCursorAdapter 对象,并将此对象设置为 ListView 的适配器:
// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{UserDictionary.Words.WORD, // Contract class constant containing the word column nameUserDictionary.Words.LOCALE // Contract class constant containing the locale column name
};// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(getApplicationContext(), // The application's Context objectR.layout.wordlistrow, // A layout in XML for one row in the ListViewmCursor, // The result from the querymWordListColumns, // A string array of column names in the cursormWordListItems, // An integer array of view IDs in the row layout0); // Flags (usually none are needed)// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);
注意:要通过 Cursor 支持 ListView,游标必需包含名为 _ID 的列。 正因如此,前文显示的查询会为“字词”表检索 _ID 列,即使 ListView 未显示该列。 此限制也解释了为什么大多数提供程序的每个表都具有 _ID 列。
获取某个列的值
// 得到words表中WORD的字段标签,也即是上面查询时列名的位置mWordListColumns的下标,这里应该是第一个
int index =mCursor.getColumnIndex(UserDictionary.Words.WORD);
if (mCursor != null) {while (mCursor.moveToNext()) {// 得到该下标列名的值.newWord = mCursor.getString(index); }
} else {}
5.创建 Content Provider
实现 ContentProvider 类,实现它的抽象方法。
query()
从您的提供程序检索数据。使用参数选择要查询的表、要返回的行和列以及结果的排序顺序。 将数据作为 Cursor 对象返回。
insert()
在您的提供程序中插入一个新行。使用参数选择目标表并获取要使用的列值。 返回新插入行的内容 URI。
update()
更新您提供程序中的现有行。使用参数选择要更新的表和行,并获取更新后的列值。 返回已更新的行数。
delete()
从您的提供程序中删除行。使用参数选择要删除的表和行。 返回已删除的行数。
getType()
返回内容 URI 对应的 MIME 类型。实现内容提供程序 MIME 类型部分对此方法做了更详尽的描述。
onCreate()
初始化您的提供程序。Android 系统会在创建您的提供程序后立即调用此方法。
请注意,ContentResolver 对象尝试访问您的提供程序时,系统才会创建它。
要创建我们自己的 Content Provider 的话,我们需要遵循以下几步:
- 创建一个继承了ContentProvider父类的类
- 定义一个名为CONTENT_URI,并且是public static final的Uri类型的类变量,你必须为其指定一个唯一的字符串值,最好的方案是以类的全名称, 如:public static final Uri CONTENT_URI = Uri.parse( “content://com.google.android.MyContentProvider”);
- 创建你的数据存储系统。大多数Content Provider使用Android文件系统或SQLite数据库来保持数据,但是你也可以以任何你想要的方式来存储。
- 定义你要返回给客户端的数据列名。如果你正在使用Android数据库,则数据列的使用方式就和你以往所熟悉的其他数据库一样。但是,你必须为其定义一个叫_id的列,它用来表示每条记录的唯一性。
- 如果你要存储字节型数据,比如位图文件等,那保存该数据的数据列其实是一个表示实际保存文件的URI字符串,客户端通过它来读取对应的文件数据,处理这种数据类型的Content Provider需要实现一个名为_data的字段,_data字段列出了该文件在Android文件系统上的精确路径。这个字段不仅是供客户端使用,而且也可以供ContentResolver使用。客户端可以调用ContentResolver.openOutputStream()方法来处理该URI指向的文件资源,如果是ContentResolver本身的话,由于其持有的权限比客户端要高,所以它能直接访问该数据文件。
- 声明public static String型的变量,用于指定要从游标处返回的数据列。
- 查询返回一个Cursor类型的对象。所有执行写操作的方法如insert(), update() 以及delete()都将被监听。我们可以通过使用ContentResover().notifyChange()方法来通知监听器关于数据更新的信息。
- 在AndroidMenifest.xml中使用标签来设置Content Provider。
- 如果你要处理的数据类型是一种比较新的类型,你就必须先定义一个新的MIME类型,以供ContentProvider.geType(url)来返回。MIME类型有两种形式:一种是为指定的单个记录的,还有一种是为多条记录的。
这里给出一种常用的格式:
vnd.android.cursor.item/vnd.yourcompanyname.contenttype (单个记录的MIME类型)
比如, 一个请求列车信息的URI如content://com.example.transportationprovider/trains/122 可能就会返回typevnd.android.cursor.item/vnd.example.rail这样一个MIME类型。vnd.android.cursor.dir/vnd.yourcompanyname.contenttype (多个记录的MIME类型)
比如, 一个请求所有列车信息的URI如content://com.example.transportationprovider/trains 可能就会返回vnd.android.cursor.dir/vnd.example.rail这样一个MIME 类型。
实例:读取系统联系人
读取系统联系人需要声明权限,如果系统是 6.0 以后的,需要申请运行时权限
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {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()){//获取联系人姓名String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));//获取联系人电话号码String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));list.add(name+"\n"+number);}}}catch(Exception e){e.printStackTrace()}finally{if(cursor != null){cursor.close();}}
}@Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults){switch(requestCode){case 1:if(grantResults.length >0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){readContacts();}else {//您拒绝了权限}}
}
创建自己的内容提供器
创建自己的内容提供器,需要去继承 ContentProvider 类,ContentProvider 类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。
public class MyProvider extends ContentProvider{@Overridepublic boolean onCreate() {return false;}@Overridepublic Cursor query(Uri uri, String[] projection, Stirng selection, String[] selectionArgs, String sortOrder){return null;}@Overrridepublic Uri insert(Uri uri , ContentValues values){return null;}@Overridepublic int update(Uri uri, ContentValuse values, String selection, String[] selectionArgs){return 0;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs){return 0;}@Overridepublic String getType(Uri uri){return null;}
}
设计内容 URI
内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表或文件的名称(路径)。 可选 ID 部分指向表中的单个行。 ContentProvider 的每一个数据访问方法都将内容 URI 作为参数;您可以利用这一点确定要访问的表、行或文件。
设计授权
提供程序通常具有单一授权,该授权充当其 Android 内部名称。为避免与其他提供程序发生冲突,您应该使用互联网网域所有权(反向)作为提供程序授权的基础。 由于此建议也适用于 Android 软件包名称,因此您可以将提供程序授权定义为包含该提供程序的软件包名称的扩展名。 例如,如果您的 Android 软件包名称为
com.example.<appname>
就应使用 com.example.<appname>.provider
授权。
设计路径结构
开发者通常通过追加指向单个表的路径来根据权限创建内容 URI。 例如,如果您有两个表:table1 和 table2,则可以通过合并上一示例中的权限来生成 内容 URI com.example..provider/table1 和 com.example..provider/table2。路径并不限定于单个段,也无需为每一级路径都创建一个表。
处理内容 URI ID
按照惯例,提供程序通过接受末尾具有行所对应 ID 值的内容 URI 来提供对表中单个行的访问。 同样按照惯例,提供程序会将该 ID 值与表的 _ID 列进行匹配,并对匹配的行执行请求的访问。
这一惯例为访问提供程序的应用的常见设计模式提供了便利。应用会对提供程序执行查询,并使用 CursorAdapter 以 ListView 显示生成的 Cursor。 定义 CursorAdapter 的条件是, Cursor 中的其中一个列必须是 _ID
用户随后从 UI 上显示的行中选取其中一行,以查看或修改数据。 应用会从支持 ListView 的 Cursor 中获取对应行,获取该行的 _ID 值,将其追加到内容 URI,然后向提供程序发送访问请求。 然后,提供程序便可对用户选取的特定行执行查询或修改。
内容 URI 模式
为帮助您选择对传入的内容 URI 执行的操作,提供程序 API 加入了实用类 UriMatcher,它会将内容 URI “模式 ”映射到整型值。 您可以在一个 switch 语句中使用这些整型值,为匹配特定模式的一个或多个内容 URI 选择所需操作。
URI 的主要格式有以下两种
content://com.example.app.provider/table1
content://com.example.app.provider/table1/1*:匹配由任意长度的任何有效字符组成的字符串
#:匹配由任意长度的数字字符组成的字符串//一个能够匹配任意表的内容 URI 格式就可以写成:
content://com.example.app.provider/*//一个能够匹配表中任意一行数据的内容 URI 格式就可以写成:
content://com.example.app.provider/table1/#
以设计和编码内容 URI 处理为例,假设一个具有授权 com.example.app.provider 的提供程序能识别以下指向表的内容 URI:
content://com.example.app.provider/table1:一个名为 table1 的表
content://com.example.app.provider/table2/dataset1:一个名为 dataset1 的表
content://com.example.app.provider/table2/dataset2:一个名为 dataset2 的表
content://com.example.app.provider/table3:一个名为 table3 的表
提供程序也能识别追加了行 ID 的内容 URI,
例如,content://com.example.app.provider/table3/1 对应由 table3 中 1 标识的行的内容 URI。
可以使用以下内容 URI 模式:
content://com.example.app.provider/*
匹配提供程序中的任何内容 URI。content://com.example.app.provider/table2/*:
匹配表 dataset1 和表 dataset2 的内容 URI,但不匹配 table1 或 table3 的内容 URI。content://com.example.app.provider/table3/#:匹配 table3 中单个行的内容 URI,
如 content://com.example.app.provider/table3/6 对应由 6 标识的行的内容 URI。
在清单文件中注册实现 content provider 的类
与 Activity 和 Service 组件类似,必须使用 provider 元素在清单文件中为其应用定义 ContentProvider 的子类。 Android 系统会从该元素获取以下信息:
授权 (android:authorities)
用于在系统内标识整个提供程序的符号名称,也即是上面所说的URI包含路径名表名的字符串。
提供程序类名 ( android:name )
实现 ContentProvider 的类。实现 ContentProvider 类中对此类做了更详尽的描述。
【 实现 】
由于 ContentProvider 可提供增、删、改、查这些操作,通常结合 SQLite 使用。
ContentResolver 是读取由 ContentProvider 共享的数据的工具。通过 Context类 定义的 getContentResolver() 方法,可以获取ContentResolver 对象。如果您不打算与其他应用共享数据,则无需开发自己的提供程序。
对于每一个应用程序来说,如果想要访问 内容提供器 中共享的数据,就一定要借助 ContentResolver 类,可以通过Context中的getContentResolver() 方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中insert() 方法用于添加数据,update() 方法用于更新数据,delete() 方法用于删除数据,query() 方法用于查询数据。
不同于SQLiteDatabase,ContentResolver 中的增删改查都是接收一个URl参数,这个参数被称为内容URL。内容URL给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority 和 path 。authority 是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式进行命名。path则是用于对同一应用程序中不同的表做区分,通常都会添加到authority后面:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
在使用内容URL作为参数的时候,需要将URL转换成URL对象:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
现在我们就可以使用这个uri对象来查询talbe1表中的数据了:
Cursor cursor = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder
);
对应参数的解释:
查询完之后,就可以从游标中取值了:
if(cursor != null){while(cursor.moveToNext()) {String column1 = cursor.getString(cursor.getColumnIndex("column1"));int column2 = cursor.getInt(cursor.getColumnIndex("column2"));}cursor.close();
}
增、删、改、查
添加数据
要增加记录,可以调用 ContentResolver.insert() 方法,该方法接受一个要增加的记录的目标URI,以及一个包含了新记录值的Map对象,调用后的返回值是新记录的URI,包含记录号。
上面的例子中我们都是基于联系人信息簿这个标准的Content Provider,现在我们继续来创建一个insertRecord() 方法以对联系人信息簿中进行数据的添加:
ContentValues values = new ContentValues();
values.put(“column1”, "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
示例方法:
private void insertRecords(String name, String phoneNo) {ContentValues values = new ContentValues();values.put(People.NAME, name);Uri uri = getContentResolver().insert(People.CONTENT_URI, values);Log.d(”ANDROID”, uri.toString());Uri numberUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY);values.clear();values.put(Contacts.Phones.TYPE, People.Phones.TYPE_MOBILE);values.put(People.NUMBER, phoneNo);getContentResolver().insert(numberUri, values);
}
更新数据
可以使用 ContentResolver.update() 方法来修改数据。
ContentValues valuse = new ContentValues();
valuse.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", 1});
示例方法:
private void updateRecord(int recNo, String name) {Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, recNo);ContentValues values = new ContentValues();values.put(People.NAME, name);getContentResolver().update(uri, values, null, null);
}
删除数据
Content Provide r中的 getContextResolver.delete() 方法可以用来删除记录。
getContentResolver().delete(uri , "column2 = ?", new String[]{ "1"});
下面的记录用来删除设备上所有的联系人信息:
private void deleteRecords() {Uri uri = People.CONTENT_URI;getContentResolver().delete(uri, null, null);
}
查询记录:
在 Content Provider 中使用的查询字符串有别于标准的 SQL 查询。很多诸如 select、add、delete、modify 等操作我们都使用一种特殊的 URI 来进行,这种 URI 由 3 个部分组成, “content://” 代表数据的路径,和一个可选的标识数据的 ID。以下是一些示例URI:
content://media/internal/images 这个URI将返回设备上存储的所有图片
content://contacts/people/ 这个URI将返回设备上的所有联系人信息
content://contacts/people/45 这个URI返回单个结果(联系人信息中ID为45的联系人记录)
尽管这种查询字符串格式很常见,但是它看起来还是有点令人迷惑。为此,Android提供一系列的帮助类(在android.provider包下),里面包含了很多以类变量形式给出的查询字符串,这种方式更容易让我们理解一点,参见下例:
MediaStore.Images.Media.INTERNAL_CONTENT_URI
Contacts.People.CONTENT_URI
因此,如上面 content://contacts/people/45 这个 URI 就可以写成如下形式:
Uri person = ContentUris.withAppendedId(People.CONTENT_URI, 45);
然后执行数据查询:
Cursor cur = managedQuery(person, null, null, null);
1.4 broadcast receiver
1. 概述
broadcast receiver( 广播接收者 )顾名思义就是用来接收来自系统和应用中的广播 的 系统组件。广播是一种 1对多 的通信方式,即存在1个发送方,若干个接收方。在 Android 系统中,广播体现在方方面面,例如:当开机完成后系统会产生一条广播,接收到这条广播就能实现开机启动服务的功能;当网络状态改变时系统会产生一条广播,接收到这条广播就能及时地做出提示和保存数据等操作;当电池电量改变时,系统会产生一条广播,接收到这条广播就能在电量低时告知用户及时保存进度,等等。把这种数据的传递方式的机制称之为 “广播” 。Android 系统会在特定的情景下发出各种广播,例如开机、锁屏了、电量不足了、正在充电了、呼出电话了、被呼叫了……
android 广播分为两个角色:
- 广播 发送者
- 广播 接收者
android 广播( 广播是一种跨进程的、“全设备之内” 的通信方式 ):
- 1)用于不同组件间的通信(含:应用内/不同应用之间)
- 2)用于多线程通信
- 3)与 android 系统的通信
在 Android 中,广播是一种广泛运用的在应用程序之间传输信息的机制。而 广播接收者 是对发送出来的广播进行过滤接受,并响应的一类组件。可以使用 广播接收者 来让 应用(app) 对一个外部事件做出响应。应用(app) 可以对外部事件进行过滤,只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个 activity 或 serice 来响应它们收到的信息,或者用 NotificationManager 来通知用户。通知可以用很多种方式来吸引用户的注意力,例如:闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。
广播接收者 的类型
- (1)Normal broadcasts:默认广播。发送一个默认广播使用 Content.sendBroadcast() 方法,普通广播对于接收者来说是完全异步的,通常每个接收者都无需等待即可以接收到广播,接收者相互之间不会有影响。对于这种广播,接收者无法终止广播,即无法阻止其他接收者的接收动作。
- (2)Ordered broadcasts:有序广播。发送一个有序广播使用 Content.sendOrderedBroadcast() 方法,有序广播比较特殊,它每次只发送到优先级较高的接收者那里,然后由优先级高的接收者再传播到优先级低的接收者那里,优先级高的接收者有能力终止这个广播
- (3)Sticky Broadcast:粘性广播。当处理完之后的 Intent,依然存在,直到你把它去掉。
注册 广播接接收者 的两种方式
广播接收者的注册有两种方法,分别是 AndroidManifest 文件中进行静态注册 和 程序动态注册。
- 静态注册:静态注册是在 AndroidManifest.xml 配置文件中注册
- 动态注册:需要在代码中动态指定广播地址并注册,通常我们是在 Activity 或 Service 注册一个广播。
静态注册 和 动态注册 的区别:
- 动态注册广播接收者特点是当用来注册的 Activity 关掉后,广播也就失效了。( 动态注册广播不是常驻型广播,也就是说广播跟随 activity 的生命周期。注意:在 activity 结束前,移除广播接收器。 )
- 静态注册时无需担忧广播接收者是否被关闭,只要设备是开启状态,广播接收者也是打开着的。也就是说哪怕 app 本身未启动,该 app 订阅的广播在触发时也会对它起作用。( 静态注册是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。 )
- 当广播为有序广播时:
1 优先级高的先接收
2 同优先级的广播接收器,动态优先于静态
3 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。 - 当广播为普通广播时:
1 无视优先级,动态广播接收器优先于静态广播接收器
2 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。
广播接收者 的 创建:
- 1,构建 Intent,使用 sendBroadcast 方法发出广播。
- 2,自定义一个类,该 类 继承 BroadcastReceive 基类
- 3,重写抽象方法 onReceive() 方法
- 4,注册该广播接收者,我们可以在代码中注册,也可以在 manifest.xml 中注册。
1,广播接收器收到相应广播后,会自动调用 onReceive() 方法
2,一般情况下,onReceive方法会涉及与其他组件之间的交互,如 发送 Notiotification,启动 server 等
3,默认情况下,广播接收器运行在 UI 线程,因此,onReceive 方法不能执行耗时操作,否则将导致 ANR
public class MyReceiver1 extends BroadcastReceiver {public MyReceiver1(){}//接收@Overridepublic void onReceive(Context context, Intent intent) {String info=intent.getStringExtra("info");Toast.makeText(context,info,Toast.LENGTH_SHORT).show();}
}
创建一个广播的步骤:
- 创建一个类继承BroadcastReceiver,并且重写其onReceive方法
public class MyBroadcastReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {Log.i("fuck","intent-action : " + intent.getAction());if(intent.getAction().equals("test")){Toast.makeText(context,"fuck",Toast.LENGTH_LONG).show();}}}
- 在清单文件中注册(静态注册)
//广播接收器<receiver android:name=".broadcast.MyBroadcastReceiver"><intent-filter><action android:name="android.intent.action.ACTION_POWER_CONNECTED" /><action android:name="test"/>//这里自定义一个广播动作</intent-filter></receiver>
或者动态注册
registerReceiver(new MyBroadcastReceiver(),new IntentFilter("test"));
- 加上权限
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
- 发送广播
Intent intent = new Intent("test");sendBroadcast(intent);
生命周期:如果一个广播处理完 onReceive 那么系统将认定此对象将不再是一个活动的对象,也就会 finished 掉它。
至此,大家应该能明白 Android 的广播生命周期的原理。
注意 :BroadcastReceiver 生命周期很短。如果需要在 onReceiver 完成一些耗时操作,应该考虑在 Service 中开启一个新线程处理耗时操作,不应该在 BroadcastReceiver 中开启一个新的线程,因为 BroadcastReceiver 生命周期很短,在执行完 onReceiver 以后就结束,如果开启一个新的线程,可能出现 BroadcastRecevier 退出以后线程还在,而如果 BroadcastReceiver 所在的进程结束了,该线程就会被标记为一个空线程,根据 Android 的内存管理策略,在系统内存紧张的时候,会按照优先级,结束优先级低的线程,而空线程无异是优先级最低的,这样就可能导致 BroadcastReceiver 启动的子线程不能执行完成。
示例
public class MyBroadcastReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {Log.i("fuck","intent-action : " + intent.getAction());if(intent.getAction().equals("test")){Toast.makeText(context,"fuck",Toast.LENGTH_LONG).show();}}}
注册
//广播接收器<receiver android:name=".broadcast.MyBroadcastReceiver"><intent-filter><action android:name="android.intent.action.ACTION_POWER_CONNECTED" /><action android:name="test"/>//这里自定义一个广播动作</intent-filter></receiver>
广播还可以通过动态注册:
registerReceiver(new MyBroadcastReceiver(),new IntentFilter("test"));
一定要加上这个权限(坑)
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
注意:xml 中注册的优先级高于动态注册广播。
有序广播的创建
先创建两个接收器
public class MyReceiver3 extends BroadcastReceiver {public MyReceiver3(){}@Overridepublic void onReceive(Context context, Intent intent) {Bundle data= getResultExtras(false);String info= data.getString("info");Toast.makeText(context,"有序广播-1----"+info,Toast.LENGTH_SHORT).show();}
}public class MyReceiver4 extends BroadcastReceiver {public MyReceiver4(){}@Overridepublic void onReceive(Context context, Intent intent) {Toast.makeText(context,"有序广播-2",Toast.LENGTH_SHORT).show();Bundle data=new Bundle();data.putString("info","广播-2");this.setResultExtras(data);}
}
粘性广播的创建
创建一个广播接收器
public class MyReceiver5 extends BroadcastReceiver {
public MyReceiver5(){}@Overridepublic void onReceive(Context context, Intent intent) {Toast.makeText(context,"接收一个粘性的广播",Toast.LENGTH_SHORT).show();}
}
发送一个粘性广播
public void sendStickyClick(View v){Intent intent=new Intent("com.example.action.MY_BROADCAST3");this.sendStickyBroadcast(intent);}
粘性广播必须要设置权限,在清单文件中加入
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
2. 发送广播
调用 Context对象 的 sendBroadcast(Intent) 即可发送广播,在参数 Intent 对象中,应该调用 setAction() 方法配置广播的“频道号”,即是相当于收音机要接收某个电台的频段,只有注册了相同的 Action 的广播接收者才可以接收到该广播。
广播的发送 = 广播发送者 将此广播的 意图(intent)通过 sendBroasdcast() 方法发送出去
发送广播
Intent intent = new Intent("test");
sendBroadcast(intent);
广播的类型
- 普通广播
- 系统广播
- 有序广播
- 粘性广播
- App 应用内广播
特别注意:
对于不同注册方式的广播接收器回调 OnReceive(Context context,Intent intent)中的context返回值是不一样的:
- 对于静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext;
- 对于全局广播的动态注册,回调onReceive(context, intent)中的context返回值是:ActivityContext;
- 对于应用内广播的动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Application Context。
- 对于应用内广播的动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Activity Context;
发送广播
Context.sendBroadcast()
发送的是普通广播,所有订阅者都有机会获得并进行处理。
发送一个普通的广播
public void sendNormalClick(View v){Intent intent=new Intent("com.example.action.MY_BROADCAST");intent.putExtra("info","姑娘约否");this.sendBroadcast(intent);}
注册文件中添加对应的 action
发送一个有序广播
public void sendOrderClick(View v){Intent intent=new Intent("com.example.action.MY_BROADCAST2");//参数:intent ,接收权限this.sendOrderedBroadcast(intent,null);}
配置:
其中 priority 表示优先级,属性范围(-1000,1000),数值越大,优先级越高。
中断有序广播
this.abortBroadcast();
同级别接收先后是随机的,再到级别低的收到广播;如果先接收到的把广播截断了,同级别以外的接收者是无法收到该广播的。
在这个方法发来的广播中(代码注册方式中),收到广播先后次序为:注明优先级的、代码注册的、没有优先级的;如果都没有优先级,代码注册收到为最先。
发送有序广播:
Context.sendOrderedBroadcast()
发送的是有序广播,系统会根据接收者声明的优先级别按顺序逐个执行接收者,
前面的接收者有权终止广播(BroadcastReceiver.abortBroadcast()),
如果广播被前面的接收者终止,后面的接收者就再也无法获取到广播。
对于有序广播,前面的接收者可以将处理结果通过setResultExtras(Bundle)方法存放进结果对象,
然后传给下一个接收者,通过代码:Bundle bundle =getResultExtras(true))可以获取
上一个接收者存入在结果对象中的数据。系统收到短信,发出的广播属于有序广播。如果想阻止用户收到短信,可以通过设置优先级,
让你们自定义的接收者先获取到广播,然后终止广播,这样用户就接收不到短信了。
3. 接收广播
自定义类,继承自 android.content.BroadcastReceiver 后需要注册,注册时,使用 IntentFilter 配置与发送方相同的 Action,重写onReceiver() 方法实现对广播的处理。
public class MyBroadcastReceiver extends BroadcastReceiver {private static final String TAG = "MyBroadcastReceiver";@Overridepublic void onReceive(Context context, Intent intent) {StringBuilder sb = new StringBuilder();sb.append("Action: " + intent.getAction() + "\n");sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");String log = sb.toString();Log.d(TAG, log);Toast.makeText(context, log, Toast.LENGTH_LONG).show();}
}
4. 注册广播接收者
注册广播接收者的两种方式
- 静态注册:静态注册是在 AndroidManifest.xml 文件中配置。
- 动态注册:动态注册需要在代码中动态的指定广播地址并注册,通常是在 Activity 或 Service 注册一个广播。
例如
MyReceiver receiver=new MyReceiver();
IntentFilter filter=new IntentFilter();
filter.addAction("android.intent.action.MY_BROADCAST");
registerReceiver(receiver,filter);解除注册
unregisterReceiver(receiver);
静态注册
静态注册:在清单文件AndroidManifest.xml中,在application节点下使用receiver节点进行注册。这种方式注册的广播接收者必须是一个单独的只实现BroadcastReceiver的类,不能是内部类。且这样的广播接收者是常驻型的,即从APP安装到手机上的那一刻即开始处于接收广播状态,且直至该APP被从手机移除。
<receiver android:name=".MyBroadcastReceiver" android:exported="true"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED"/><action android:name="android.intent.action.INPUT_METHOD_CHANGED" /></intent-filter>
</receiver>
- 注册方式:在 AndroidManifest.xml 里通过<receive 标签声明
- 属性说明
<receiverandroid:enable="true"/"false"//此broadcastReceiver 是否接受其他应用发出的广播//默认值时由receiver 中d有无inter-filter决定,如果有,默认true,否则默认falseandroid:exported="true"/"false"android:icon="drawable resource"android:label="string resource"//继承BroadcastReceiver子类的类名android:name=".mBroadcastReceiver"
//具有相应权限的广播发送者发送的广播才能被此BroadcastReceiver所接收;android:permission="string"
//BroadcastReceiver运行所处的进程
//默认为app的进程,可以指定独立的进程
//注:Android四大基本组件都可以通过此属性指定自己的独立进程android:process="string" >//用于指定此广播接收器将接收的广播类型
//本示例中给出的是用于接收网络状态改变时发出的广播<intent-filter><action android:name="android.net.conn.CONNECTIVITY_CHANGE" /></intent-filter></receiver>
注册示例:
<receiver //此广播接收者类是mBroadcastReceiverandroid:name=".mBroadcastReceiver" >//用于接收网络状态改变时发出的广播<intent-filter><action android:name="android.net.conn.CONNECTIVITY_CHANGE" /></intent-filter>
</receiver>
当此 APP 首次启动时,系统会自动实例化 mBroadcastReceiver 类,并注册到系统中。
动态注册
动态注册:在程序中调用 Context对象 的 registerReceiver(BroadcastReceiver, IntentFilter) 方法进行注册。这种方式可以注册以内部类的形式存在的广播接收者,且这种方式的广播接收者仅当注册后才开始接收广播,并且在调用了Context对象的unregisterReceiver(BroadcastReceiver) 方法后就会停止接收广播。
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
this.registerReceiver(br, filter);
- 注册方式:在代码中调用 Context.registerReceiver() 方法
- 具体代码如下:
// 1. 实例化BroadcastReceiver子类 & IntentFiltermBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();IntentFilter intentFilter = new IntentFilter();// 2. 设置接收广播的类型intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);// 3. 动态注册:调用Context的registerReceiver()方法registerReceiver(mBroadcastReceiver, intentFilter);//动态注册广播后,需要在相应位置记得销毁广播unregisterReceiver(mBroadcastReceiver);
特别注意
动态广播最好在 onResume中注册, onPause 注销
原因:
- 1,对于动态广播,有注册必然得有注销,否则会导致内存泄漏
- 2,onPause在App死亡前一定会被执行,从而保证app死亡前一定会被注销,从而防止内存泄漏
两种注册方式的区别
5. 无序广播 与 有序广播
- 1. 普通广播: 即为 无序广播,谁都可以接收,并不会相互打扰。普通广播是完全异步的,可以在同一时刻(逻辑上)被所有接收者接收到,消息传递的效率比较高,但缺点是:接收者不能将处理结果传递给下一个接收者,并且无法终止广播 Intent 的传播;
- 2. 有序广播:调用 sendOrderedBroadcast(Intent, String permission)方法发送的广播,各广播接收者在接收广播时,会存在一定的先后顺序,即某接收者会先收到广播,其他接收者后收到广播,广播会在各接收者之间按照一定的先后顺序进行传递。在广播的传递过程中,先接收到广播的接收者可以对广播进行拦截或篡改。( 有序广播是按照接收者声明的优先级别(声明在 intent-filter 元素的 android:priority 属性中,数越大优先级别越高,取值范围:-1000 到 1000。也可以调用IntentFilter 对象的 setPriority() 进行设置),被接收者依次接收广播。如:A 的级别高于 B,B 的级别高于 C,那么,广播先传给A,再传给B,最后传给C。A 得到广播后,可以往广播里存入数据,当广播传给 B 时,B可以从广播中得到 A 存入的数据。 )
6. 有序广播的接收者们的优先级
有序广播的接收者们的优先级用于确定接收的先后顺序,优先级越高的接收者,将更优先接收到广播,反之,则更靠后接收到广播。
- 1. 注册广播时,在广播对应的 IntentFilter 中的 priority 属性直接决定优先级,该属性值为 int 类型的数值,取值越大,则优先级越高!
- 2. 如果存在多个广播接收者配置的 priority 属性值相同,则动态注册的广播接收者的优先级高于静态注册的广播接收者。
- 3. 如果根据以上 2 条规则都无法确定优先级,则根据注册的先后顺序确定各接收者们的优先级。
7. 有序广播的拦截或篡改
- 1. 【拦截】在广播接收者中,使用abortBroadcast()方法,可以终止有序广播向后继续传递,即后续的接收者们将无法接收到该广播。注意:该方法只能在接收有序广播时调用!
- 2. 【篡改】在广播接收者中,调用setResult()方法,可以向广播中添加数据,并在后续的接收者中,可以通过getResult()获取这些数据,同时,后续的接收者也可以再次调用setResult()方法重新向广播中写入数据,即覆盖原有的数据,以实现篡改。
小 结:
- 在Android 中如果要发送一个广播必须使用sendBroadCast 向系统发送对其感兴趣的广播接收器中。
- 使用广播必须要有一个intent 对象必设置其action动作对象
- 使用广播必须在配置文件中显式的指明该广播对象
- 每次接收广播都会重新生成一个接收广播的对象
- 在BroadCastReceiver中尽量不要处理太多逻辑问题,建议复杂的逻辑交给Activity 或者 Service 去处理
- 如果在AndroidManifest.xml中注册,当应用程序关闭的时候,也会接收到广播。在应用程序中注册就不产生这种情况了。
注意
当如果要进行的操作需要花费比较长的时间,则不适合放在BroadcastReceiver中进行处理。
引用网上找到的一段解释:
在 Android 中,程序的响应( Responsive )被活动管理器( Activity Manager )和窗口管理器( Window Manager )这两个系统服务所监视。当 BroadcastReceiver 在 10 秒内没有执行完毕,Android 会认为该程序无响应。所以在 BroadcastReceiver 里不能做一些比较耗时的操作,否侧会弹出ANR ( Application No Response )的对话框。如果需要完成一项比较耗时的工作,应该通过发送Intent 给 Service ,由 Service 来完成。而不是使用子线程的方法来解决,因为 BroadcastReceiver 的生命周期很短(在 onReceive() 执行后 BroadcastReceiver 的实例就会被销毁),子线程可能还没有结束BroadcastReceiver 就先结束了。如果 BroadcastReceiver 结束了,它的宿主进程还在运行,那么子线程还会继续执行。但宿主进程此时很容易在系统需要内存时被优先杀死,因为它属于空进程(没有任何活动组件的进程)。
1.5 Android 四大组件总结:
- Activity:是整个应用程序的门面,主要负责应用程序当中数据的展示,是各种各样控件的容器,是用户和应用程序之间交互的接口
- Service:在前台不可见,但是承担大部分数据处理工作,它和 Activity 的地位是并列的,区别在于 Activity 运行于前台,Service 运行于后台,没有图形用户界面,通常他为其他的组件提供后台服务或监控其他组件的运行状态
- BroadcastReceiver:实现消息的异步接收,他非常类似事件编程中的监听器,但他与普通事件监听器有所不同,普通的事件监听器监听的事件源是程序中的控件,而 BroadcastReceiver 监听的事件源是 Android 应用中其他的组件
- ContentProvider:为不同的应用程序之间数据访问提供统一的访问接口,通常它与ContentResolver结合使用,一个是应用程序使用 ContentProvider 来暴露自己的数据,而另外一个是应用程序通过 ContentResolver 来访问数据
(1)4大组件的注册
4大基本组件都需要注册才能使用,每个Activity、service、Content Provider都需要在AndroidManifest文件中进行配置。AndroidManifest文件中未进行声明的activity、服务以及内容提供者将不为系统所见,从而也就不可用。而broadcast receiver广播接收者的注册分静态注册(在AndroidManifest文件中进行配置)和通过代码动态创建并以调用Context.registerReceiver()的方式注册至系统。需要注意的是在AndroidManifest文件中进行配置的广播接收者会随系统的启动而一直处于活跃状态,只要接收到感兴趣的广播就会触发(即使程序未运行)。
(2)4大组件的激活
内容提供者的激活:当接收到ContentResolver发出的请求后,内容提供者被激活。而其它三种组件activity、服务和广播接收器被一种叫做intent的异步消息所激活。
(3)4大组件的关闭
内容提供者仅在响应ContentResolver提出请求的时候激活。而一个广播接收器仅在响应广播信息的时候激活。所以,没有必要去显式的关闭这些组件。Activity关闭:可以通过调用它的finish()方法来关闭一个activity。服务关闭:对于通过startService()方法启动的服务要调用Context.stopService()方法关闭服务,使用bindService()方法启动的服务要调用Contex.unbindService()方法关闭服务。
(4)android中的任务(activity栈)
- (a)任务其实就是activity的栈,它由一个或多个Activity组成,共同完成一个完整的用户体验。栈底的是启动整个任务的Activity,栈顶的是当前运行的用户可以交互的Activity,当一个activity启动另外一个的时候,新的activity就被压入栈,并成为当前运行的activity。而前一个activity仍保持在栈之中。当用户按下BACK键的时候,当前activity出栈,而前一个恢复为当前运行的activity。栈中保存的其实是对象,栈中的Activity永远不会重排,只会压入或弹出。
- (b)任务中的所有activity是作为一个整体进行移动的。整个的任务(即activity栈)可以移到前台,或退至后台。
- (c)Android系统是一个多任务(Multi-Task)的操作系统,可以在用手机听音乐的同时,也执行其他多个程序。每多执行一个应用程序,就会多耗费一些系统内存,当同时执行的程序过多,或是关闭的程序没有正确释放掉内存,系统就会觉得越来越慢,甚至不稳定。为了解决这个问题,Android引入了一个新的机制,即生命周期(Life Cycle)。
二、六大布局:
2.1 声明 Android 程序布局有两种方式:
- 1) : 使用 XML 文件描述界面布局;
- 2) : 在 Java 代码中通过调用方法进行控制。
既可以使用任何一种声明界面布局的方式,也可以同时使用两种方式。
使用XML文件声明有以下3个特点:
- 1) :将程序的表现层和控制层分离;
- 2) :在后期修改用户界面时,无须更改程序的源程序;
- 3) :可通过WYSIWYG可视化工具直接看到所设计的用户界面,有利于加快界面设计的过程。
建议尽量采用 XML 文件声明界面元素布局。在程序运行时动态添加界面布局会大大降低应用响应速度,但依然可以在必要时动态改变屏幕内容。
2.2 Android 六大界面布局方式
Android 六大界面布局方式包括:
- 线性布局 (LinearLayout)
- 框架布局 (FrameLayout)
- 表格布局 (TableLayout)
- 相对布局 (RelativeLayout)
- 绝对布局 (AbsoluteLayout)
- 网格布局 (GridLayout)
1. LinearLayout 线性布局
LinearLayout容器中的组件一个挨一个排列,通过控制android:orientation属性,可控制各组件是横向排列还是纵向排列。
线性布局,如名字所描述的那样,这个布局将它所包含的控件在线性方向上一次排列,方向分为 水平方向和数值方向。
属性 android:orientation = “vertical” | “horizontal” 竖直或水平,默认水平
属性 android:layout_gravity = “top” | “center” | “bottom” 内部的布局方式
属性 android:gravity = “top”|"center”|“bottom” 相对于父容器的对齐方式
属性 android:layout_weidht 使用比例方式执行控件的大小,在手机屏幕适配方面起到非常重要的作用
XML属性 | 相关方法 | 说明 |
android:gravity | setGravity(int) | 设置布局管理器内组件的对齐方式 |
android:orientation | setOrientation(int) | 设置布局管理器内组件的排列方式,可以设置为horizontal、vertical两个值之一 |
其中,gravity属性支持top, left, right, center_vertical, fill_vertical,center_horizontal, fill_horizontal, center, fill, clip_vertical,clip_horizontal。也可以同时指定多种对齐方式的组合。
XML属性 | 说明 |
android:layout_gravity | 指定该子元素在LinearLayout中的对齐方式 |
android:layout_weight | 指定子元素在LinearLayout中所占的权重 |
2. TableLayout 表格布局
表格布局与HTML中的table td tr标签类似
<table><tr><td></td></tr>
</table>
如何确定行与列
- 如果在TableLayout下添加组件,这个组件会占满整行
- 如果想把多个组件放在同一行,需要添加TableRow的容器,然后把组件放进去
- TableRow中的组件个数决定的该行的列数,而列的宽度由列中最宽的单元格决定
- TableRow嗯layout_width属性默认是fill-parent,修改无效。但是layout_height默认是wrapcontent,可以修改
- 整个表格的宽度取决于父容器的宽度(占满父容器)
重要的属性:
- android:collapaseColumns:设置需要被隐藏的列的序号
- android:shrinkColumns:设置允许被收缩的列的序号
- android:stretchCoumns:设置运行被拉伸嗯列的序号
这三个属性都是从0开始算的
shrinkColumns= "2" //对应第三行
shrinkColumns = '"0,2" //设置多个都生效
shrinkColumns = "" //所有列都生效
- android:layout_column=“2”: 表示跳过第二个,直接显示第三个,从1开始
- android:layout_span=“4”:表示合并*4个单元格,也就说这个组件占4个单元格
TableLayout 继承自 Linearout,本质上仍然是线性布局管理器。表格布局采用行、列的形式来管理 UI 组件,并不需要明确地声明包含多少行、多少列,而是通过添加 TableRow、其他组件来控制表格的行数和列数。
每向 TableLayout 中添加一个 TableRow 就代表一行;
每向 TableRow 中添加一个一个子组件就表示一列;
如果直接向 TableLayout 添加组件,那么该组件将直接占用一行;
在表格布局中,可以为单元格设置如下三种行为方式:
- Shrinkable:该列的所有单元格的宽度可以被收缩,以保证该表格能适应父容器的宽度;
- Strentchable:该列所有单元格的宽度可以被拉伸,以保证组件能完全填满表格空余空间;
- Collapsed:如果该列被设置为 Collapsed,那么该列的所有单元格会被隐藏;
TableLayout 的常用 XML 属性及方法
XML属性 | 相关方法 | 说明 |
android:collapseColumns | setColumns(int, boolean) | 设置需要被隐藏的列的序号,多个序号间用逗号分隔 |
android:shrinkColumns | setShrinkAllColumns(boolean) | 设置需要被收缩的列的序号 |
android:stretchColumns | setStretchAllColumns(boolean) | 设置允许被拉伸的列的序号 |
3. FrameLayout 帧布局
FrameLayout直接继承自ViewGroup组件。帧布局为每个加入其中的组件创建一个空白的区域(称为一帧),每个子组件占据一帧,这些帧会根据gravity属性执行自动对齐。
XML属性 | 相关方法 | 说明 |
android:foreground | setForeground(Drawable) | 设置该帧布局容器的前景图像 |
android:foregroundGravity | setForeGroundGraity(int) | 定义绘制前景图像的gravity属性 |
4. RelativeLayout 相对布局
RelativeLayout 的 XML 属性及相关方法说明
XML属性 | 相关方法 | 说明 |
android:gravity | setGravity(int) |
|
android:ignoreGravity | setIgnoreGravity(int) | 设置哪个组件不受gravity属性的影响 |
为了控制该布局容器的各子组件的布局分布,RelativeLayout提供了一个内部类:RelativeLayout.LayoutParams。
RelativeLayout.LayoutParams里只能设为boolean的XML属性
XML属性 | 说明 |
android:layout_centerHorizontal | 设置该子组件是否位于布局容器的水平居中 |
android:layout_centerVertical |
|
android:layout_centerParent |
|
android:layout_alignParentBottom |
|
android:layout_alignParentLeft |
|
android:layout_alignParentRight |
|
android:layout_alignParentTop |
RelativeLayout.LayoutParams里属性值为其他UI组件ID的XML属性
XML属性 | 说明 |
android:layout_toRightOf | 控制该子组件位于给出ID组件的右侧 |
android:layout_toLeftOf |
|
android:layout_above |
|
android:layout_below |
|
android:layout_alignTop |
|
android:layout_alignBottom |
|
android:layout_alignRight |
|
android:layout_alignLeft |
5. Android 4.0 新增的网格布局 GridLayout
GridLayout是Android4.0增加的网格布局控件,与之前的TableLayout有些相似,它把整个容器划分为rows × columns个网格,每个网格可以放置一个组件。性能及功能都要比tablelayout好,比如GridLayout布局中的单元格可以跨越多行,而tablelayout则不行,此外,其渲染速度也比tablelayout要快。
GridLayout提供了setRowCount(int)和setColumnCount(int)方法来控制该网格的行和列的数量。
XML属性 | 相关方法 | 说明 |
android:alignmentMode | setAlignmentMode(int) | 设置该布局管理器采用的对齐模式 |
android:columnCount | setColumnCount(int) | 设置该网格的列数量 |
android:columnOrderPreserved | setColumnOrderPreserved(boolean) | 设置该网格容器是否保留序列号 |
android:roeCount | setRowCount(int) | 设置该网格的行数量 |
android:rowOrderPreserved | setRowOrderPreserved(boolean) | 设置该网格容器是否保留行序号 |
android:useDefaultMargins | setUseDefaultMargins(boolean) | 设置该布局管理器是否使用默认的页边距 |
为了控制GridLayout布局容器中各子组件的布局分布,GridLayout提供了一个内部类:GridLayout.LayoutParams,来控制Gridlayout布局容器中子组件的布局分布。
GridLayout.LayoutParams常用的XML属性和方法说明
XML属性 | 说明 |
android:layout_column | 设置该组件在GridLayout的第几列 |
android:layout_columnSpan | 设置该子组件在GridLayout横向上跨几列 |
android:layout_gravity | 设置该子组件采用何种方式占据该网格的空间 |
android:layout_row | 设置该子组件在GridLayout的第几行 |
android:layout_rowSpan | 设置该子组件在GridLayout纵向上跨几行 |
6. AbsoluteLayout 绝对布局
即 Android 不提供任何布局控制,而是由开发人员自己通过 X 坐标、Y 坐标 来控制组件的位置。每个组件都可指定如下两个 XML 属性:
- layour_x;
- layout_y;
绝对布局已经过时,不应使用或少使用。
界面布局类型的选择和性能优化
首先得明确,界面布局类型的嵌套越多越深越复杂,会使布局实例化变慢,使 Activity 的展开时间延长。建议尽量减少布局嵌套,尽量减少创建View 对象的数量。
1 . 减少布局层次,可考虑用 RelativeLayout 来代替 LinearLayout。通过 Relative 的相对其他元素的位置来布局,可减少块状嵌套;
2 . 另一种减少布局层次的技巧是使用 <merge /> 标签来合并布局;
3 . 重用布局。Android 支持在 XML 中使用 <include /> 标签, <include /> 通过指定 android:layout 属性来指定要包含的另一个XML布局。
如:
<includeandroid:id="@+id/id1"android:layout="@layout/mylayout">
<includeandroid:id="@+id/id2"android:layout="@layout/mylayout">
<includeandroid:id="@+id/id3"android:layout="@layout/mylayout">
三、五大存储
在 Android 中,可供选择的存储方式有 SharedPreferences、文件存储、SQLite数据库方式、内容提供器(Content provider)和网络。
一. SharedPreferences 方式
Android 提供用来存储一些简单的配置信息的一种机制,例如,一些默认欢迎语、登录的用户名和密码等。其以键值对的方式存储,
使得我们可以很方便的读取和存入.
1)程序要实现的功能:
我们在Name文本框中输入wangwu,在Password文本框中输入123456,然后退出这个应用。我们在应用程序列表中找到这个应用,重新启动,可以看到其使用了前面输入的Name和Password
2)实现的代码
布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"><TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="SharedPreferences demo"/><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:text="name"></TextView><EditTextandroid:id="@+id/name"android:layout_width="fill_parent"android:layout_height="wrap_content"android:text=""></EditText><TextViewandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:text="password"></TextView><EditTextandroid:id="@+id/password"android:layout_width="fill_parent"android:layout_height="wrap_content"android:password="true"android:text=""></EditText>
</LinearLayout>
主要实现代码
package com.demo;import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.EditText;public class SharedPreferencesDemo extends Activity {public static final String SETTING_INFOS = "SETTING_Infos"; public static final String NAME = "NAME"; public static final String PASSWORD = "PASSWORD"; private EditText field_name; //接收用户名的组件private EditText filed_pass; //接收密码的组件@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);//Find VIew field_name = (EditText) findViewById(R.id.name); //首先获取用来输入用户名的组件filed_pass = (EditText) findViewById(R.id.password); //同时也需要获取输入密码的组件// Restore preferencesSharedPreferences settings = getSharedPreferences(SETTING_INFOS, 0); //获取一个SharedPreferences对象String name = settings.getString(NAME, ""); //取出保存的NAMEString password = settings.getString(PASSWORD, ""); //取出保存的PASSWORD//Set valuefield_name.setText(name); //将取出来的用户名赋予field_namefiled_pass.setText(password); //将取出来的密码赋予filed_pass}@Override protected void onStop(){ super.onStop(); SharedPreferences settings = getSharedPreferences(SETTING_INFOS, 0); //首先获取一个SharedPreferences对象settings.edit() .putString(NAME, field_name.getText().toString()) .putString(PASSWORD, filed_pass.getText().toString()) .commit(); } //将用户名和密码保存进去}
SharedPreferences 保存到哪里去了?
SharedPreferences 是以 XML 的格式以文件的方式自动保存的,在 DDMS中的File Explorer中展开到/data/data/<package
name>/shared_prefs下,以上面这个为例,可以看到一个叫做SETTING_Infos.xml的文件
注意:Preferences只能在同一个包内使用,不能在不同的包之间使用。
SharedPreferences 是使用键值对的方式进行存储数据的。
想要使用SharedPreferences 来存储数据,首先主要获取到SharedPreferences 对象。Android提供了三种方法用于获取SharedPreferences对象:
1,Context类中的getSharedPreferences()方法
//此方法接收两个参数,一个参数用于指定SharedPreferences文件的名称,
//如果指定的文件不存在则会创建一个,
//SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下
//第二个参数用于指定操作模式,目前只有MODE_PRIVATE这种模式,和直接传入0效果相同
SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age",13);
editor.putBoolean("married",false);
editor.apply();
2,Activity类中的getPreferences()方法
//这个方法和Context中的getSharedPreferences()方法很类似,不过它只接收一个操作模式,
//因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名
3,PreferencesManager类中的getDefaultSharedPreferences()方法
// 这是一个静态方法,它接收一个Context参数,
// 并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件
得到了SharedPreferences对象后, 就可以开始想SharedPreferences文件中存储数据了,主要可以分为三步:
(1)调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象
(2)向SharedPreferences.Editor 对象中添加数据,比如添加一个布尔值,可以使用putBoolean() 方法
(3)调用apply()方法的添加的数据提交,从而完成数据存储操作
SharedPreferences中读取数据
SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE );
String name = pref.getString("name","");
int age = pref.getInt("age",0);
boolean married = pref.getBoolean("married", false);
二. 文件存储方式
在 Android 中,其提供了openFileInput 和 openFileOuput 方法读取设备上的文件,下面看个例子代码,具体如下所示:
StringFILE_NAME = "tempfile.tmp"; //确定要操作文件的文件名
FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE); //初始化
FileInputStream fis = openFileInput(FILE_NAME); //创建写入流
上述代码中两个方法只支持读取该应用目录下的文件,读取非其自身目录下的文件将会抛出异常。需要提醒的是,如果调用FileOutputStream 时指定的文件不存在,Android 会自动创建它。另外,在默认情况下,写入的时候会覆盖原文件内容,如果想把新写入的内容附加到原文件内容后,则可以指定其模式为Context.MODE_APPEND。
三. SQLite数据库方式
SQLite 是 Android 所带的一个标准的数据库,它支持SQL语句,它是一个轻量级的嵌入式数据库。
1)实现的功能
在这个例子里边,我们在程序的主界面有一些按钮,通过这些按钮可以对数据库进行标准的增、删、改、查。
2)实现代码
所用到的字符文件
<?xml version="1.0" encoding="utf-8"?>
<resources><string name="app_name">SQLite数据库操作实例</string><string name="button1">建立数据库表</string><string name="button2">删除数据库表</string><string name="button3">插入两条记录</string><string name="button4">删除一条记录</string><string name="button5">查看数据库表</string>
</resources>
布局代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"><TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/app_name"/><Button android:text="@string/button1" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><Button android:text="@string/button2" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><Button android:text="@string/button3" android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><Button android:text="@string/button4" android:id="@+id/button4" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button><Button android:text="@string/button5" android:id="@+id/button5" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button></LinearLayout>
主要代码
package com.sqlite;import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;/** 什么是SQLiteDatabase?* 一个SQLiteDatabase的实例代表了一个SQLite的数据库,通过SQLiteDatabase实例的一些方法,我们可以执行SQL语句,* 对数据库进行增、删、查、改的操作。需要注意的是,数据库对于一个应用来说是私有的,并且在一个应用当中,数据库的名字也是惟一的。 *//** 什么是SQLiteOpenHelper ? * 这个类主要生成一个数据库,并对数据库的版本进行管理。* 当在程序当中调用这个类的方法getWritableDatabase()或者getReadableDatabase()方法的时候,如果当时没有数据,那么Android系统就会自动生成一个数据库。* SQLiteOpenHelper 是一个抽象类,我们通常需要继承它,并且实现里边的3个函数,* onCreate(SQLiteDatabase):在数据库第一次生成的时候会调用这个方法,一般我们在这个方法里边生成数据库表。* onUpgrade(SQLiteDatabase, int, int):当数据库需要升级的时候,Android系统会主动的调用这个方法。一般我们在这个方法里边删除数据表,并建立新的数据表,当然是否还需要做其他的操作,完全取决于应用的需求。* onOpen(SQLiteDatabase):这是当打开数据库时的回调函数,一般也不会用到。 */public class SQLiteDemo extends Activity {OnClickListener listener1 = null;OnClickListener listener2 = null;OnClickListener listener3 = null;OnClickListener listener4 = null;OnClickListener listener5 = null;Button button1;Button button2;Button button3;Button button4;Button button5;DatabaseHelper mOpenHelper;private static final String DATABASE_NAME = "dbForTest.db";private static final int DATABASE_VERSION = 1;private static final String TABLE_NAME = "diary";private static final String TITLE = "title";private static final String BODY = "body";//建立一个内部类,主要生成一个数据库private static class DatabaseHelper extends SQLiteOpenHelper {DatabaseHelper(Context context) {super(context, DATABASE_NAME, null, DATABASE_VERSION);}//在数据库第一次生成的时候会调用这个方法,一般我们在这个方法里边生成数据库表。@Overridepublic void onCreate(SQLiteDatabase db) {String sql = "CREATE TABLE " + TABLE_NAME + " (" + TITLE+ " text not null, " + BODY + " text not null " + ");";Log.i("haiyang:createDB=", sql);db.execSQL(sql);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);prepareListener();initLayout();mOpenHelper = new DatabaseHelper(this);}private void initLayout() {button1 = (Button) findViewById(R.id.button1);button1.setOnClickListener(listener1);button2 = (Button) findViewById(R.id.button2);button2.setOnClickListener(listener2);button3 = (Button) findViewById(R.id.button3);button3.setOnClickListener(listener3);button4 = (Button) findViewById(R.id.button4);button4.setOnClickListener(listener4);button5 = (Button) findViewById(R.id.button5);button5.setOnClickListener(listener5);}private void prepareListener() {listener1 = new OnClickListener() {public void onClick(View v) {CreateTable();}};listener2 = new OnClickListener() {public void onClick(View v) {dropTable();}};listener3 = new OnClickListener() {public void onClick(View v) {insertItem();}};listener4 = new OnClickListener() {public void onClick(View v) {deleteItem();}};listener5 = new OnClickListener() {public void onClick(View v) {showItems();}};}/** 重新建立数据表*/private void CreateTable() {//mOpenHelper.getWritableDatabase()语句负责得到一个可写的SQLite数据库,如果这个数据库还没有建立,//那么mOpenHelper辅助类负责建立这个数据库。如果数据库已经建立,那么直接返回一个可写的数据库。SQLiteDatabase db = mOpenHelper.getWritableDatabase();String sql = "CREATE TABLE " + TABLE_NAME + " (" + TITLE+ " text not null, " + BODY + " text not null " + ");";Log.i("haiyang:createDB=", sql);try {db.execSQL("DROP TABLE IF EXISTS diary");db.execSQL(sql);setTitle("数据表成功重建");} catch (SQLException e) {setTitle("数据表重建错误");}}/** 删除数据表*/private void dropTable() {//mOpenHelper.getWritableDatabase()语句负责得到一个可写的SQLite数据库,如果这个数据库还没有建立,//那么mOpenHelper辅助类负责建立这个数据库。如果数据库已经建立,那么直接返回一个可写的数据库。SQLiteDatabase db = mOpenHelper.getWritableDatabase();String sql = "drop table " + TABLE_NAME;try {db.execSQL(sql);setTitle("数据表成功删除:" + sql);} catch (SQLException e) {setTitle("数据表删除错误");}}/** 插入两条数据*/private void insertItem() {//mOpenHelper.getWritableDatabase()语句负责得到一个可写的SQLite数据库,如果这个数据库还没有建立,//那么mOpenHelper辅助类负责建立这个数据库。如果数据库已经建立,那么直接返回一个可写的数据库。SQLiteDatabase db = mOpenHelper.getWritableDatabase();String sql1 = "insert into " + TABLE_NAME + " (" + TITLE + ", " + BODY+ ") values('haiyang', 'android的发展真是迅速啊');";String sql2 = "insert into " + TABLE_NAME + " (" + TITLE + ", " + BODY+ ") values('icesky', 'android的发展真是迅速啊');";try {// Log.i()会将参数内容打印到日志当中,并且打印级别是Info级别// Android支持5种打印级别,分别是Verbose、Debug、Info、Warning、Error,当然我们在程序当中一般用到的是Info级别Log.i("haiyang:sql1=", sql1);Log.i("haiyang:sql2=", sql2);db.execSQL(sql1);db.execSQL(sql2);setTitle("插入两条数据成功");} catch (SQLException e) {setTitle("插入两条数据失败");}}/** 删除其中的一条数据*/private void deleteItem() {try {//mOpenHelper.getWritableDatabase()语句负责得到一个可写的SQLite数据库,如果这个数据库还没有建立,//那么mOpenHelper辅助类负责建立这个数据库。如果数据库已经建立,那么直接返回一个可写的数据库。SQLiteDatabase db = mOpenHelper.getWritableDatabase();//第一个参数是数据库表名,在这里是TABLE_NAME,也就是diary。 //第二个参数,相当于SQL语句当中的where部分,也就是描述了删除的条件。//如果在第二个参数当中有“?”符号,那么第三个参数中的字符串会依次替换在第二个参数当中出现的“?”符号。 db.delete(TABLE_NAME, " title = 'haiyang'", null);setTitle("删除title为haiyang的一条记录");} catch (SQLException e) {}}/** 在屏幕的title区域显示当前数据表当中的数据的条数。*//** Cursor cur = db.query(TABLE_NAME, col, null, null, null, null, null)语句将查询到的数据放到一个Cursor 当中。这个Cursor里边封装了这个数据表TABLE_NAME当中的所有条列。 query()方法相当的有用,在这里我们简单地讲一下。第一个参数是数据库里边表的名字,比如在我们这个例子,表的名字就是TABLE_NAME,也就是"diary"。第二个字段是我们想要返回数据包含的列的信息。在这个例子当中我们想要得到的列有title、body。我们把这两个列的名字放到字符串数组里边来。第三个参数为selection,相当于SQL语句的where部分,如果想返回所有的数据,那么就直接置为null。第四个参数为selectionArgs。在selection部分,你有可能用到“?”,那么在selectionArgs定义的字符串会代替selection中的“?”。第五个参数为groupBy。定义查询出来的数据是否分组,如果为null则说明不用分组。第六个参数为having ,相当于SQL语句当中的having部分。第七个参数为orderBy,来描述我们期望的返回值是否需要排序,如果设置为null则说明不需要排序。*/private void showItems() {SQLiteDatabase db = mOpenHelper.getReadableDatabase();String col[] = { TITLE, BODY };//查询数据Cursor cur = db.query(TABLE_NAME, col, null, null, null, null, null);Integer num = cur.getCount();setTitle(Integer.toString(num) + " 条记录");}
}
Android 为了让我们能够更加方便的管理数据库,专门提供了一个SQLiteOpenHelper 帮助类,借助这个类可以非常简单的将数据库进行创建好升级。
SQLiteOpenHelper 中有两个非常重要的实例方法,getReadableDatabase() 和 getWritableDatabase() 。这两个方法可以创建或者打开一个现有的数据库(如果数据库存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入(如磁盘空间已满),getReadableDatabase方法返回的对象将以只读的方式打开数据库,而getWeitableDatabase则出现异常
例子(在指定路径下创建数据库文件 .db )
public class MainActivity extends Activity {public static final String PATH_ONE = "KogBill";public static final String PATH_NAME = "KogBill.db";private SQLiteDatabase db; //声明SQLiteDatabase ,该对象可以操作数据库String path = Environment.getExternalStorageDirectory().getAbsolutePath();String path1 = path + File.separator + PATH_ONE; //需要创建的路径String path2 = path + File.separator + PATH_ONE + File.separator + PATH_NAME; //需要创建的文件@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);File f = new File(path1);if( !f.exists()){ //创建数据库文件路径f.mkdirs();} //实例化MySQLiteHelper ,创建指定目录下数据库文件,并创建表MySQLiteHelper mSQL = new MySQLiteHelper(MainActivity.this, path2);db = mSQL.getWritableDatabase();}class MySQLiteHelper extends SQLiteOpenHelper{private static final int DATABASE_VERSION = 1;//数据库版本号private static final String CREATE_TABLE = "create table kog_bill ("+ "_id integer primary key autoincrement,"+ "date text, "+ "breakfast text, "+ "lunch text,"+ "dinner text,"+ "happy text,"+ "other text,"+ "spare text)";//方便创建实例,简化构造方法,方法内调用4参数构造方法//参数 name 可以是 数据库名称,也可以数据库文件路径(即可以指定数据库文件路径)public MySQLiteHelper(Context context, String name) {this(context, name, null, DATABASE_VERSION);}//必须要实现的方法public MySQLiteHelper(Context context, String name, CursorFactory factory, int version) {super(context, name, factory, version);}@Overridepublic void onCreate(SQLiteDatabase db) {// 第一次创建数据库时 才会调用Log.e("mylog", "创建数据库表");db.execSQL(CREATE_TABLE);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}}
}
根据上述代码,便获得db对象,通过db(SQLiteDatabase)可进行数据库的操作,如 db.query() db.delete()
如果我们想在创建一个数据库表,参照上述代码,可以在SQLiteOpenHelper的onCreate方法中加入语句:
@Override
public void onCreate(SQLiteDatebase db) {db.execSQL(CREATE_TABLE);db.execSQL(CREATE_BOOK); //新创建一个数据库表
}
然后重新运行一下,发现并没有创建成功,因为KogBill.db数据库已经存在,所以MySQLiteHelper 中的onCreate方法都不会执行,解决这个办法的方法很简单,只需要将db文件删除,重新运行,便可成功,但是原来数据库中的数据都会被删除。所以需要用到MySQLiteHelper中的update方法。
class MySQLiteHelper extends SQLiteOpenHelper{.....@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){db.execSQL("drop table if exists book"); //如果已经存在就删除,防止重复创建onCreate(db); // 再次执行onCreate 方法}
}
但是onUpgrade方法默认是不执行的,如何让onUpgrade方法执行,需要用到MySQLiteHelper 构造参数中的版本号:
private static final int DATABASE_VERSION = 1;// 将版本号 由 1 改为2
这里将数据库版本号由 1 改为 2,表示对数据库的升级
数据库的 增、删、改、查
添加数据
ContentValues values = new ContentValues();values.put("date", str1.isEmpty()?"0.0":str1);values.put("breakfast", ""+str2);values.put("lunch", ""+str3);values.put("dinner", ""+str4);values.put("happy", ""+str5);values.put("other", ""+str6);values.put("spare", ""+str7);long ii = db.insert("kog_bill", "", values);values.clear();if(ii != -1) {Utils.showToast("保存成功!", MainActivity.this);}else {Utils.showToast("保存失败!", MainActivity.this);}
更新数据
ContentValues valus = new ContentValues();
valuse.put("other","12");
db.update("kogBill", values, "_id=?",new String[]{id});
删除数据
db.delete("kogBill", "_id=?",new String[]{id});
查询数据
db.query("kog_bill", new String[]{"_id","date","breakfast","lunch","dinner","happy","other","spare"}, null, null, null, null, "date desc");
使用SQL操作数据库
虽然Android 已经给我们提供了非常方便的API用于操作数据库,不过总会有些人不习惯去使用这些辅助行的方法,而是更加青睐于直接使用SQL来操作数据库,当然Android也是提供的。
添加数据
db.execSQL("insert into kogBill ("date","breakfest","lunch","dinner","happy","other","spare") values (?,?,?,?,?,?,?)", new String[]{"1921-1-1",“123”,“1”,“1”,“11”,“2”,“3”});
更新数据
db.execSQL("update kogBill set other = ? where _id = ? ", new String[]{"12",id});
删除数据
db.execSQL("delete from kogBill where _id = ?”, new String[]{id});
使用 LitePal 操作数据库
假设编译环境为AndroidStudio。
1,引进包
dependencies{compile fileTree(dir:'libs', include:['*.jar'])compile 'com.android.support:appcompat-v7:23.2.0'testCompile 'junt:junt:4.12'compile 'org.litepal.android:core:1.3.2' //引入litepal包
}
2,配置litepal.xml 文件
右键app/src/main 目录->New -> Directory ,创建一个assets目录,然后在 assets目录下再新建一个litepal.xml 文件,接着编辑文件中的内容
<?xml version='1.0' encoding="utf-8"?>
<litepal><dbname value = "BookStore"></dbname><version value="1"></version><list></list>
</listepal>
其中,<dbname 标签用来指定数据库名,<version 用来指定数据库版本号,<list 标签用来指定所有映射模型。
最后还需要在配置以下 LitePalApplication, 修改AndroidManifest.xml 中的代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.litepaltest" ><applicationandroid:name="org.litepal.LitePalApplication" //配置 LitePalApplicationandroid:allowBackup="true".....</application>
</manifest>
以上,LitePal的配置工作已经结束了,接下来使用LitePal。
首先将需要实现 javabean类 对应 数据库表.
然后将javabean类添加到映射模型列表中,修改litepal.xml 中的代码
<litepal><dbname value="kogBill" ></dbname><version value="1"></version><list><mapping class="com.example.litepaltest.book"></mapping> //javabean类的路径</list>
这样所有工作就已经完成,现在只要进行任意一次数据库的操作,数据库db文件就会自动创建,比如:
Connector.getDatabase();
操作数据
如果需要对某个表进行数据操作,需要让其对应的javaBean类继承DataSupport
public class Book extends DataSupport { //让对应的类继承DataSupport...
}
接下来,进行添加数据的操作:
Book book = new Book();
book.setName("...");
book.setAuthor("...");
book.setPages(234);
book.setPrice(12,21);
book.setPress("unkow");
book.save(); //执行sava 就可以插入数据了
执行更新数据:
Book book = new Book();
book.setPrice(11.11);
book.setPress("Anchor");
book.updateAll("name = ? and authro = ?","..","...");
删除数据:
DataSupport.deleteAll(Book.class, "price<?","13");
查询数据:
//查询所有
List<Book> books = DataSupport.findAll(Book.class);
// 查询第一条
List<Book> books = DataSupport.findFirst(Book.class);
//查询最后一条
List<Book> books = DataSupport.findLast(Book.class);
//查询那几列的数据
List<Book> books = DataSupport.select("name","author).find(Book.class);
//条件查询, 页面大于400
List<Book> books = DataSupport.where("pages >?","400").find(Book.class);
//将 price 降序排序
List<Book> books = DataSupport.order(price desc").find(Book.class);
//查询前3条
List<Book> books = DataSupport.limit(3).find(Book.class);
//从下表1开始,往后查询3条
List<Book> boods = DataSupport.limit(3).offset(1),find(Book.class)
当然这些方法也可以组合起来使用:
List<Book> books = DataSupport.select("name","author","pages").where("pages>?”,"400").order("pages").limit(10).offset(10).find(Book.class);
如果有些特殊查询,使用上述方法无法查询时,可以使用如下语句:
Cursor c = DataSupport.findBySQL("select * from Book where pages > ? and price < ?”, "400","20”);
四. 内容提供器(Content provider)方式
在 Android 的设计“哲学”里是鼓励开发者使用内部类的,这样不但使用方便,而且执行效率也高。
1.什么是 ContentProvider
数据在Android当中是私有的,当然这些数据包括文件数据和数据库数据以及一些其他类型的数据。难道两个程序之间就没有办法对于数据进行交换?解决这个问题主要靠ContentProvider。
一个Content Provider类实现了一组标准的方法接口,从而能够让其他的应用保存或读取此Content Provider的各种数据类型。也就是说,一个程序可以通过实现一个ContentProvider的抽象接口将自己的数据暴露出去。外界根本看不到,也不用看到这个应用暴露的数据在应用当中是如何存储的,或者是用数据库存储还是用文件存储,还是通过网上获得,这些一切都不重要,重要的是外界可以通过这一套标准及统一的接口和程序里的数据打交道,可以读取程序的数据,也可以删除程序的数据,当然,中间也会涉及一些权限的问题。
下边列举一些较常见的接口,这些接口如下所示。
// 通过Uri进行查询,返回一个Cursor。
query(Uriuri, String[] projection, String selection, String[] selectionArgs,StringsortOrder)// 将一组数据插入到Uri 指定的地方。
insert(Uriurl, ContentValues values)// 更新Uri指定位置的数据。
update(Uriuri, ContentValues values, String where, String[] selectionArgs)// 删除指定Uri并且符合一定条件的数据。
delete(Uriurl, String where, String[] selectionArgs)
2.什么是 ContentResolver
外界的程序通过ContentResolver接口可以访问ContentProvider提供的数据,在Activity当中通过getContentResolver()可以得到当前应用的ContentResolver实例。
ContentResolver提供的接口和ContentProvider中需要实现的接口对应,主要有以下几个。
// 通过Uri进行查询,返回一个Cursor。
query(Uriuri, String[] projection, String selection, String[] selectionArgs,StringsortOrder)// 将一组数据插入到Uri 指定的地方。
insert(Uriurl, ContentValues values)// 更新Uri指定位置的数据。
update(Uriuri, ContentValues values, String where, String[] selectionArgs)// 删除指定Uri并且符合一定条件的数据。
delete(Uriurl, String where, String[] selectionArgs)
3. ContentProvider和ContentResolver中用到的Uri
在 ContentProvider 和 ContentResolver 当中用到了 Uri 的形式通常有两种,
- 一种是指定全部数据,
- 另一种是指定某个ID的数据。
我们看下面的例子。
- content://contacts/people/ 这个Uri指定的就是全部的联系人数据。
- content://contacts/people/1 这个Uri指定的是ID为1的联系人的数据。
在上边两个类中用到的Uri一般由3部分组成。
- 第一部分是:"content://"。
- 第二部分是要获得数据的一个字符串片段。
- 最后就是ID(如果没有指定ID,那么表示返回全部)。
由于URI通常比较长,而且有时候容易出错,且难以理解。所以,在Android当中定义了一些辅助类,并且定义了一些常量来代替这些长字符串的使用,例如下边的代码:
Contacts.People.CONTENT_URI (联系人的URI)。
1)实现的功能
在这个例子里边,首先在系统的联系人应用当中插入一些联系人信息,然后把这些联系人的名字和电话再显示出来
2)实现方法
package com.contentProvider;import android.app.ListActivity;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.Contacts.Phones;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;public class ContentProviderDemo extends ListActivity {protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//getContentResolver()方法得到应用的ContentResolver实例。// query(Phones.CONTENT_URI, null, null, null, null)。它是ContentResolver里的方法,负责查询所有联系人,并返回一个Cursor。这个方法参数比较多,每个参数的具体含义如下。//· 第一个参数为Uri,在这个例子里边这个Uri是联系人的Uri。//· 第二个参数是一个字符串的数组,数组里边的每一个字符串都是数据表中某一列的名字,它指定返回数据表中那些列的值。//· 第三个参数相当于SQL语句的where部分,描述哪些值是我们需要的。//· 第四个参数是一个字符串数组,它里边的值依次代替在第三个参数中出现的“?”符号。//· 第五个参数指定了排序的方式。Cursor c = getContentResolver().query(Phones.CONTENT_URI, null, null, null, null);startManagingCursor(c); //让系统来管理生成的Cursor。ListAdapter adapter = new SimpleCursorAdapter(this,android.R.layout.simple_list_item_2, c, new String[] { Phones.NAME, Phones.NUMBER }, new int[] { android.R.id.text1, android.R.id.text2 });setListAdapter(adapter); //将ListView和SimpleCursorAdapter进行绑定。}}
五. 网络存储方式
1.例子介绍
通过邮政编码查询该地区的天气预报,以POST发送的方式发送请求到webservicex.net站点,访问WebService.webservicex.net站点上提供查询天气预报的服务,具体信息请参考其WSDL文档,网址为:
http://www.webservicex.net/WeatherForecast.asmx?WSDL。
输入:美国某个城市的邮政编码。
输出:该邮政编码对应城市的天气预报。
2.实现步骤如下
(1)如果需要访问外部网络,则需要在AndroidManifest.xml文件中加入如下代码申请权限许可:
<!-- Permissions -->
<uses-permission Android:name="Android.permission.INTERNET"/>
(2)以HTTP POST的方式发送(注意:SERVER_URL并不是指WSDL的URL,而是服务本身的URL)。实现的代码如下所示:
//定义需要获取的内容来源地址private static final String SERVER_URL = "http://www.webservicex.net/WeatherForecast. asmx/GetWeatherByZipCode";//根据内容来源地址创建一个Http请求HttpPost request = new HttpPost(SERVER_URL); // 添加一个变量List<NameValuePair> params = new ArrayList<NameValuePair>();// 设置一个华盛顿区号params.add(new BasicNameValuePair("ZipCode","200120")); //添加必须的参数request.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); //设置参数的编码try{//发送请求并获取反馈HttpResponse httpResponse = new DefaultHttpClient().execute(request); // 解析返回的内容if (httpResponse.getStatusLine().getStatusCode() != 404) {String result = EntityUtils.toString(httpResponse.getEntity());Log.d(LOG_TAG, result);}} catch(Exception e){Log.e(LOG_TAG, e.getMessage());}
代码解释:
如上代码使用Http从webservicex获取ZipCode为“200120”(美国WASHINGTON D.C)的内容,其返回的内容如下:
<WeatherForecasts xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http: //www.w3.org/2001/XMLSchema-instance" xmlns="http://www.webservicex.net"><Latitude>38.97571</Latitude><Longitude>77.02825</Longitude><AllocationFactor>0.024849</AllocationFactor><FipsCode>11</FipsCode><PlaceName>WASHINGTON</PlaceName><StateCode>DC</StateCode><Details><WeatherData><Day>Saturday, April 25, 2009</Day><WeatherImage>http://forecast.weather.gov/images/wtf/sct.jpg</WeatherImage><MaxTemperatureF>88</MaxTemperatureF><MinTemperatureF>57</MinTemperatureF><MaxTemperatureC>31</MaxTemperatureC><MinTemperatureC>14</MinTemperatureC></WeatherData><WeatherData><Day>Sunday, April 26, 2009</Day><WeatherImage>http://forecast.weather.gov/images/wtf/few.jpg</WeatherImage><MaxTemperatureF>89</MaxTemperatureF><MinTemperatureF>60</MinTemperatureF><MaxTemperatureC>32</MaxTemperatureC><MinTemperatureC>16</MinTemperatureC></WeatherData>
…</Details>
</WeatherForecasts>