Android四大组件
Android四大组件分别是Activity、Service、Content provider、Broadcast receiver
Activity
概念:
android 中,Activity 相当于一个页面,可以在Activity中添加Button、CheckBox 等控件,一个android 程序有多个Activity组成。
生命周期:
四种活动状态:
运行状态:
当一个活动位于栈顶的时候,即直接与用户交互的页面,就处于运行状态,系统最不愿意回收处于这个状态的活动;
暂停状态:
当一个活动不再位于栈顶,但还可见,这时活动就进入了暂停状态,此时可见指的是调用了一个对话框形式的活动覆盖在当前页面之上,并未完全覆盖屏幕,所以还是可见的,可以认为完全活着只不过被对话框暂停;系统也不愿意回收这类活动,只有内存极其低才会考虑,一般不会发生;
停止状态:
当一个活动不位于栈顶,并且完全不可见的时候,就进入了停止状态。系统仍然会保存这种活动相应的状态和成员变量,但并不是完全可靠,当其他地方更高优先级需要内存的时候,系统可能会回收停止状态的活动;
销毁状态:
当一个活动从返回栈中移除之后就变成了销毁状态,系统最倾向于回收这种状态的活动,来保证内存的充足
7个回调方法:
onCreate():
活动在第一次创建的时候被调用,应该在这个方法中完成活动的初始化操作,比如加载布局、绑定事件等;
onStart():
这个方法在活动由不可见变为可见的时候调用;
onResume():
这个方法在活动准备好和用户交互的时候进行调用,此时的活动一定位于返回栈的栈顶,即前台,处于运行状态,直接面向用户;
onPause():
这个方法在系统准备去启动或恢复另一个活动的时候调用。通常会在这个方法里将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,否则会影响栈顶活动的使用;
onStop():
当活动完全不可见的时候调用,也就是变为停止状态的时候,如果是对话框的形式,则只调用onPause()方法,而不调用onStop()方法;
onDestroy():
活动通过此方法变为销毁状态,通常调用finish()来进行销毁活动;
onRestart():
这个方法由停止状态变为运行状态的时候调用;
四种启动模式:
Activity的启动模式有4种,分别是Standard、SingleTop、SingleTask、SingleInstance,可以在AndroidManifest.xml中通过<activity>标签指定android:launchMode属性来选择启动模式:
Standard模式:
Standard模式是android的默认启动模式,在这种模式(即默认情况)下,每当启动一个新的活动,就会创建一个新的实例压入栈中,并位于栈顶的位置,如果创建了多个实例,每次back都会退回到上一个实例,并不会直接退出应用程序,模式示意图如下:
SingleTop模式:
栈顶模式,启动活动时如果发现该活动此时正位于返回栈的栈顶,那么就直接使用,而不再创建新的实例,如果同一个活动创建多次,则只需要一次back就可以退出程序,示意图如下:
SingleTask模式:
单任务模式,如果启动的activity已经存在于任务栈中,如果在栈顶则直接使用,如果不在栈顶但存在于任务栈中,则将其移动到栈顶,并将栈顶的activity出栈,否则创建新的实例,示意图如下:
SingleInstance模式:
单实例模式,一个activity一个栈,用于多个应用程序共享一个activity实例,示意图如下:给secondActivity设置为SingleInstance模式:
Service
Service(服务)通常用作后台处理耗时的逻辑,与Activity一样,也存在自己的生命周期,也需要AndroidManifest.xml中配置相关信息;
服务的运行不依赖任何用户界面,即使程序被切换到后台,或者用户打开另外一个程序,服务仍然能够保持正常运行,但服务依赖于创建服务时所在的应用程序的进程,若该进程被杀死,则所有依赖于该进程的服务也会停止运行;
服务并不会自动开启线程,所有的代码都是默认在主线程当中的,所有我们需要在服务的内部手动创建子线程,并在子线程中执行具体的任务,否则就会阻塞主线程,关于Android中多线程的知识请看我的“Android多线程编程”,这里只对服务进行一定的阐述。
定义一个服务:
新建了一个ServiceTest项目,进入包内右键--> New --> Service --> Service 或者直接创建Class类,继承Service并重写IBinder方法
Exported表示是否允许除了当前程序之外的其他程序访问这个服务,Enabled表示是否启用这个服务,默认都选中;
创建完service之后自动生成Myservice的构造函数以及onBind()方法,需要自行重写onCreate()、onStartCommand()、onDestory()方法,这三个方法是服务中最常用到的3个方法,其中onCreate()方法在初始化创建的时候调用,onStartCommand()方法会在每次服务启动的时候调用,onDestory()方法会在服务销毁的时候调用;
通常情况下,希望服务一启动就执行某个动作,所以把逻辑写在onStartCommand()方法中,当服务销毁的时候,又在onDestory()中去回收不必要的资源;
另外,每一个服务都需要在AndroidManifest.xml中去注册才可以使用,其实这是Android四大组件共有的特点,当创建服务的时候Android Studio就已经很智能地为我们已经自动注册好了服务:
启动和停止服务:
分别自定义了按钮startService、stopService来进行控制
启动和停止服务都是Intent来进行控制,startService()和stopService()方法都是定义在Context类中的,所以可以在活动里直接调用这两个方法,但是要注意的是,这里服务完全是由活动控制的,如果活动不点击stopService,服务就会一直处于运行状态,想要让他自己停止,需要在Myservice中任意位置调用stopSelf()方法即可,
onCreate()和onStartCommand()方法的区别在于,onCreate()是在服务第一次创建的时候进行调用,而onStartCommand()是在服务启动的时候调用,我们第一次点击startService的按钮的时候,onCreate()和onStartCommand()都会调用,之后每次点击startService的按钮,就只调用onStartCommand()方法
活动和服务进行通信:
在此之前,服务一旦启动之后,活动就与服务基本没什么关系了,服务做什么动作,活动也操作不了,所以为了让活动可以控制服务做一些具体的操作就需要用到Service中的这个方法,修改MyService代码如下:
在MyService中新建一个DownloadBinder的内部类(使其继承Binder类,Binder是实现IBinder接口的实现类),作用是控制服务进行开始下载和查看下载进度的方法,这里是模拟方法,没有具体实现,用打印日志的方式代替;接着,在MyService中创建了DownloadBinder的实例,然后在onBind()方法中返回这个实例,为做之后进行服务绑定准备;
修改MyActivity的代码如下:
首先引入MyService中内部类DownloadBinder为私有成员,然后创建了一个ServiceConnection()的匿名类,在里面重写了onServiceConnected()和onServiceDisconnected()方法,这两个方法分别在活动与服务成功绑定与断开连接的时候调用;在onServiceConnected()方法中首先向下转型IBinder得到我们的DownloadBinder的实例,因为DownloadBinder继承的是Binder类,而Binder实现IBinder接口,所以这里需要向下转型一下;此时,我们的活动和服务就变得很紧密了,我们可以在活动中根据具体场景来调用DownloadBinder中所有的public方法,实现活动指挥服务的功能;注意,此时活动和服务还差一步才能绑定,如以下代码所示:
bindService()和unbindService()是绑定和解绑服务的方法,与之前startService()和stopService()方法一样都是定义在Context类中的,所以可以直接在onCreate()方法中直接调用;bindService()有三个参数,第一个就是构建出的Intent对象,第二个是创建的ServiceConnection的实例,第三个参数是int类型的标志位,是系统自己定义好的,这里BIND_AUTO_CREATE表示在活动和服务进行绑定之后自动创建服务,所以会执行onCreate()方法,并不会执行onStartCommand()方法。
另外,任何一个服务都是在整个应用程序内通用的,即MyService不仅可以和MainActivity绑定,可以和这个应用程序内任何活动进行绑定。
服务的生命周期:
这里用图分别解释了连接服务和绑定服务的生命周期,先说连接,通过startService()方法启动服务,如果是第一次创建则需要先调用onCreate()方法,只要没有销毁,之后每次启动都只调用onStartCommand()方法,服务一旦启动,就会一直保持运行状态,直到stopService()方法或stopSelf()方法被调用;
再说绑定,通过bindService()方法可以绑定活动和服务,同理,如果这个服务之前从来没有创建过,则需要先执行onCreate()方法,然后调用方再通过onBind()方法获取到IBind对象的实例,此时活动和服务之间就可以自由通信了,只要调用方和服务之间连接没有断开,就会一直处于运行状态;
注意,如果既对服务执行了startService(),又对服务执行了bindService()方法,怎么才能销毁这个服务呢?在Android系统机制中,一个服务只要被启动或者被绑定了就会一直处于运行状态中,必须要让以上两种条件同时满足才能销毁服务。所以,这种情况下要同时调用stopService()和unbindService()方法,onDestory()方法才会执行。
使用前台服务:
普通服务一般都是在后台运行的,一直以来都是在默默地工作,但它的优先级还是比较低的,当系统内存不足的时候就有可能回收掉正在后台运行的服务,而前台服务将解决这种问题。前台服务与普通服务的最大区别在于,它会一直有一个正在运行的图标在系统的状态栏中,下拉状态栏后可以看到更加详细的内容,非常类似于通知的效果。
修改MyService中onCreate()方法中的代码如下:
这里的代码用到了Android中通知的知识点,这里简单说一下。
我们想要创建一个通知Notification的对象,得需要一个bulider构造器,但是Android的每个版本都会对通知的这一部分进行修改,所以就带来了API不稳定的风险,解决办法就是使用support库中提供的兼容API。Support-v4库中提供了一个NotificationCompat类,使用这个类的构造器来创建Notification对象就可以保证在所有的Android版本中正常工作。我们使用这种方法来build的时候可以在Builder构造器的最终build之前作出连缀任意多的设置方法来创建一个丰富的Notification对象,如上代码所示一共连缀了7个方法,来进行一一阐述:
- setContentTitle()方法用于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容;
- setContentText()方法用于通知的正文内容,下拉系统状态栏就可以看到这部分内容;
- setwhen()方法用于指定通知被创建的时间,以毫秒为单位,下拉系统状态栏时,这里指定的时间会显示在相应的通知上;
- setSmallIcon()方法用于设置通知的小图标,注意只能使用纯alpha图层的图片进行设置,会显示在状态栏;
- setLargeIcon()方法用于设置的大图标,下拉系统状态栏时就可以看到设置的大图标了;
- setContentIntent()方法用于点击通知来执行pendingIntent,跳转页面,这里说一下pendingIntent,此方法可以理解为延迟执行的Intent,在这里就是当点击通知的时候执行,通常可以通过几个静态方法来获取实例,例如getActivity()、getBroadcast()、getService()方法,这几个方法参数都是相同的,第一个是Context,第二个一般用不到,通常传入0即可,第三个是Intent对象,可以构建出pendingIntent的“意图”,第四个参数是用来确定pendingIntent的行为,通常传入0即可;
- setAutoCancle()方法用于在点击之后取消这个通知,传入True即可;
这里构建通知用了startForeground()方法,调用这个方法就会让MyService变成一个前台服务,并在系统状态栏显示出来,类似于notify()方法,都有两个参数,第一个参数是id,表示通知的标识,第二个参数是创建的通知对象。
使用IntentService:
服务中的代码都是默认运行在主线程当中的,如果此时在服务中处理一些比较耗时的逻辑,则很容易出现ANR(Application Not Responding),这对于客户来说是不能容忍的,这个时候就要使用Android多线程编程的技术,我们应该在服务的每个具体的方法中开启一个子线程去处理一些耗时的逻辑,如以下代码,在MyService中:
stopSelf()作用是当子线程执行结束自动停止服务,除此之外Android提供了一个IntentService类,新建一个MyIntentService类继承自IntentService,代码如下所示:
首先要提供一个无参的构造函数,在其内部调用父类的有参构造函数,传入一个String类型的参数;然后要在子类中实现onHandleIntent()这个抽象方法,在这个方法中去处理一些具体的逻辑,而且不用担心ANR的问题,因为它自动就会创建一个子线程来运行,并且将会在运行结束之后通过onDestory()方法自动销毁,所以说IntentService还是很受欢迎的,也可以使用studio快捷方式来进行创建IntentService,但会生成一些不必要的函数,所以这里使用class继承的方式,但是别忘了要在AndroidManifest.xml中进行注册:
Content provider
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口,如果一个应用程序提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问,当然这里会涉及到一些权限的问题,关于权限部分的内容可以看我专门写的“Android权限详解”,这里不予阐述。
对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver 类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。
不同于SQLiteDatabase,ContentResolver 中的增删改查都是接收一个URl参数,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority 和 path 。authority 是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式进行命名。path则是用于对同一应用程序中不同的表做区分,通常都会添加到authority后面。比如某个应用程序包是com.example.app,则authority为com.example.app.provider,同时有两张数据表table1、table2,则path就是/table1、/table2,同时也要加上内容URI的协议标识,所以完整的URI字符串就是如下所示:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
这样就很清楚地表明了访问的是哪个应用程序,操作的是哪张表,所以使用这种URI对象作为参数。在得到了内容URI字符串之后,需要把它解析成Uri对象才能作为参数传入:
现在就可以使用这个Uri对象来查询table1表中的数据了,代码如下所示:
对应参数的解释:
之后就可以来遍历Cursor取值了:
记得cursor游标取完值之后要close掉,已经接触了查询数据的方法,让我们来看一下增删改的方法:
添加数据:
这里使用values.put()的方法将数据都存到ContentValues中,put的参数如下
第一个参数是列名,第二个参数是列的值。
更新数据:
这里update是把column1中的值改为了adb,使用了selection和selectionArgs,用于对修改行进行确定,防止对所有行进行误操作;
删除数据:
读取联系人的实例:
先创建一个简单的ListView
首先创建了一个list的全局变量和adapter的适配器,
这里使用了ContentResolver的query()方法来查询系统的联系人数据,Uri参数使用ContactsContract.CommonDataKinds.Phone类封装好的数据,然后继续使用封装的数据取出联系人的姓名和手机号码,之间要进行换行,之后把数据添加到ListView的数据源中,再通知刷新一下ListView,最后一定记得关掉cursor游标,
最后一步在AndroidManifest.xml中加上权限:
大功告成。
创建自己的内容提供器:
需要继承ContentProvider,其提供了6个抽象方法,使用子类继承的时候,需要将这6个方法全部重写
可以看到,几乎每一个方法都有Uri对象,标准的URI字符串是这样写的:
content://com.example.app.provider/table1
除此之外,还可以在其后添加一个id:
content://com.example.app.provider/table1/1
表示想要访问的是table1表中id为1的那一行数据,内容URI的格式就是以上两种,我们也可以使用通配符的方式来分别匹配这两种格式的URI,规则如下:
- * : 表示匹配任意长度的任意字符
- # : 表示匹配任意长度的数字
所以,一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*
一个能够匹配表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#
接着,我们再借助UriMatcher这个类就可以轻松实匹配内容URI的功能,代码如下:
首先提供了四个整型常量,TABLE1_DIR表示访问表1中的所有数据,TABLE1_ITEM表示访问表1中的单条数据,TABLE2_DIR表示访问表2中的所有数据,TABLE2_ITEM表示访问表2中的单条数据。接着在静态代码块里使用uri.addURI()方法,将期望的格式传递进去,然后再使用uriMatcher.match(uri)方法进行对传入query方法的uri对象进行匹配获取到准确的意图,insert()、update()、delete()方法也类似。
还有一个方法是getType(),它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象的MIME类型。MIME主要由3部分组成,Android对此作出以下规定:
- 必须以vnd开头
- 如果内容URI以路径结尾,则在其后加android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/
- 最后接上vnd.<authority>.<path>
例如:对于content://com.example.app.provider/table1这个内容URI,它对应的MIME类型为:vnd.android.cursor.dri/vnd.com.example.app.provider.table1
对于content://com.example.app.provider/table1/1这个内容URI,它对应的MIME类型为:vnd.android.cursor.item/vnd.com.example.app.provider.table1
所以getType()的代码如下所示:
以上就是一个完整的内容提供器就创建完成了,这里也保证了隐私数据不会泄露,因为所有的CRUD操作都是要匹配到对应的URI格式才能执行,而不可能向UriMacher中去添加隐私数据的URI,所以这部分数据根本无法被外程序访问到,也就保护了隐私数据。
Broadcast receiver
Android广播分为两个角色:广播发送者和广播接收者。
Android广播的作用:
- 用于在不同组件之间的通信(应用内或不同应用之间);
- 用于多线程通信;
- 与android系统进行通信;
广播用于通知所有关注其的应用程序,所以Android中的每个应用程序都可以对其关注的广播进行注册,获取到关注的信息。Android中的广播分为两种类型:
标准广播:一种完全异步执行的广播,一个程序发出广播,所有广播接收器几乎都会在同一时刻收到广播消息,没有先后顺序,所以效率比较高,但同时也意味着他们无法被截断。标准广播的示意图如下:
有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,按先后顺序传递,优先级高的广播接收器可以截断广播,那么之后的广播接收器就收不到这个广播了。有序广播示意图如下:
接收系统广播:
广播接收器可以对自己感兴趣的广播进行注册,注册广播有两种方式,一种是直接在AndroidManifest.xml中进行注册,叫做静态注册,一种是在代码中进行注册,叫做动态注册,接下来在这两个方面进行阐述:
动态注册监听网络变化:
首先创建了一个内部类NetworkChangeReceiver,使其继承BroadcastReceiver类,并重写父类的onReceive()方法,然后在onCreate()方法中创建了IntentFilter的实例和NetworkChangeReceiver的实例。
IntentFilter的实例调用了一个addAction()的方法,意图是告诉我们的广播接收器想要监听的是什么类型的广播,也就是关注什么。之后又调用了Context的registerReceiver()方法,把IntentFilter的实例和NetworkChangeReceiver的实例传入其中,这样NetworkChangeReceiver广播接收器就会接收到所有值为android.net.conn.CONNECTIVITY_CHANGE的广播。最后要记得,动态注册的广播一定都要取消注册,否则就会导致内存泄漏,有一种说法是在onResume()中注册,onPause()中进行注销,可是当页面不可见但还antive的时候再次回到页面又要重新注册一次,导致资源重复占用,不过这种资源浪费很少而且这种方式也很好地解决了内存泄漏的风险。这里是在onDestory()中通过调用unregisterReceiver()方法实现的。
需要注意的是,Android系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限才可以,否则程序将直接崩溃。这里是监听网络变化,所以应该在AndroidManifest.xml中进行权限声明:
静态注册实现开机启动:
动态注册的优势在于可以灵活自由地控制注册与注销,但是也只能在程序启动了才能接收到广播,静态注册就可以实现在程序未启动的时候就接收广播。
可以使用Android Studio的快捷方式来创建一个广播接收器,然后就直接在onReceive方法中写上代码逻辑:
接着,在AndroidManifest.xml中进行静态注册,不过当使用studio的快捷方式进行创建广播接收器的时候,已经自动在AndroidManifest.xml中注册完成了:
可以看到在application的标签中出现了<receiver>标签,所有的静态广播接收器都是在这里进行注册,具体用法和<activity>标签类似,不过其中exported的意思是允许此接收器接收其他应用程序的广播,enabled的意思是启用这个广播接收器。不过目前只是进行了注册并没有实现我们的开机自启动的目的,因此对文件进行修改为:
需要注意的是,onReceive()方法不会自己开启线程去处理逻辑,所以在这个方法中不要写太多的复杂逻辑,否则会占用太多的资源,长时间运行没有结束程序也会报错。因此,广播接收器通常用来监听到某个需要时来打开程序其他组件,比如创建一条状态栏通知notification,或者启动一个服务等。
发送自定义广播:
广播分为标准广播和有序广播,这里进行分别阐述:
发送标准广播:
首先新建一个MyBroadcastReceiver的广播接收器,
在MainActivity中写一个点击按钮发送广播的事件,这里需要注意一下,Android8.0之后对接收广播有了更高的要求,所以要加setComponent的方法进行指定,如果不加将收不到发送的广播,指定的时候有两个参数,第一个写当前所在的包名,第二个写包名+广播接收器,然后在AndroidManifest.xml进行注册:
之后点击按钮就可以发送广播,并接收到广播了。
发送有序广播:
有序广播只需要把sendBroadcast()改为sendOrderBroadcast()就可以了,第一个参数是intent,第二个参数是与权限有关的字符串,这里传入null就可以了。
我们之前说有序广播是有顺序的,首先需要好几个不同的广播接收器,这里就不写了,那么怎么来确定顺序呢,直接上代码:
在AndroidManifest.xml中给注册信息中加上优先级,这里把优先级设为100,那么就自然根据优先级有了接收顺序,之前还说了可以截断广播,所以在优先级高的广播接收器中添加一行如下代码就可以截断广播,之后的广播接收器就接收不到广播了:
使用本地广播:
在Android8.0之前的发送和接收的广播全部都属于系统全局广播,即发出的广播可以被其他任何应用程序接收到,并且也可以接收到来自其他任何应用程序的广播,这样就带了安全的问题,而Android8.0对此部分进行了修改,在发送自定义的广播的时候要指定广播接收器才行,除此之外,Android还引入了一套本地广播机制,使用这个发出的广播只能够在应用程序内部进行传递,并且广播接收器也只能接受来自当前应用程序内的广播,这样安全问题就解决了。
另外,我们在写代码的时候,最好是在发送广播的时候要指定接收者的权限,否则就会具有敏感信息泄露的问题,而在接收广播的时候要指定发送者的权限,否则就会具有冒充广播的问题。
本地广播的使用并不复杂,主要就是使用了一个LocalBroadcastManager来对广播进行管理,并提供了发送广播和注册广播接收器的方法。修改MainActivity代码如下:
这个代码和前面的动态注册广播接收器以及发送广播的代码时一样的,只不过现在是通过LocalBroadcastManager的getInstance()方法来得到实例,然后注册广播的时候调用的是localBroadcastManager的registerReceiver()方法,在发送广播的时候使用的是localBroadcastManager的sendBroadcast()方法。
另外,本地广播是无法通过静态注册的方式来接收的。而且本地广播具有以下几个优势:
- 可以明确地知道正在发送的广播不会离开我们的程序,因此不必担心机密数据泄露;
- 其他的程序无法将广播发送到我们的内部,因此不必担心会有安全漏洞的隐患;
- 发送本地广播比发送系统全局广播将会更加高效;