Service
有两种启动方式:startService
和bindService
startService
通过 startService 启动后,service 会一直无限期运行下去,只有外部调用了 stopService()或 stopSelf()方法时,该 Service 才会停止运行并销毁。开启者不能调用服务里面的方法。
生命周期为:onCreate - > onStartCommand - > onDestroy
多次调用 startService,会多次执行 onStartCommand,并不会多次执行 onCreate
bindService
使用 context 的bindService(Intent service, ServiceConnection conn,int flags)
方法启动 service,不再使用时,调用 unbindService(ServiceConnection)方法手动停止该服务,或者等调用者销毁后,Service 会自动销毁。这种启动方式,调用者可以调用 Service 里的方法。
生命周期为:onCreate() - > onBind() -> onUnbind() - > onDestory()
调用者如果需要调用 Service 中的方法,可以通过如下几步:
- Service 中定义一个 Binder 的子类。这个子类中定义一个返回 Service 的方法或属性。
- 在 Service 中实例化一个 Binder 子类的对象。
- onBind 方法中返回这个这个 Binder 子类的对象。
- 调用者在 ServiceConnection 的 onServiceConnected 中将 iBinder 转型为对应的 Binder 子类,并调用返回 Service 的方法或属性获取到 Service 实例,此时就可以调用返回 Service 里的方法。也可以通过给 Service 设置接口或者设置 kotlin 高阶函数,来实现 Service 调用它的调用者(比如 Activity)中的方法。
示例代码如下:
class TestService : Service() {
//2.在Service中实例化一个Binder子类的对象
private val testBinder: TestBinder = TestBinder()
var update: ((Int) -> Unit)? = null
private var job: Job? = null
override fun onBind(intent: Intent): IBinder {
Log.i("zx", "TestService-onBind")
job = CoroutineScope(Dispatchers.IO).launch {
//模拟进度
var index = 0
while (true) {
delay(1000)
withContext(Dispatchers.Main) {
update?.let { it(++index) }
}
}
}
//3.onBind方法中返回这个这个Binder子类的对象
return testBinder
}
override fun onDestroy() {
super.onDestroy()
job?.cancel()
}
//1.定义Binder的子类,并且是内部类,类中定义一个方法返回Service
inner class TestBinder : Binder() {
val service: TestService
get() = this@TestService
}
}
class ServiceActivity : AppCompatActivity() {
lateinit var testService: TestService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_service)
val intent = Intent(this, TestService::class.java)
bindService(intent, serviceConnection, BIND_AUTO_CREATE)
}
private var serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, iBinder: IBinder) {
//4.获取到Service实例
testService = (iBinder as TestService.TestBinder).service
//通过设置高阶函数,来实现Service调用它的调用者Activity中的方法
testService.update = {
Log.i("zx", "当前模拟的进度为$it")
}
}
override fun onServiceDisconnected(name: ComponentName) {}
}
override fun onDestroy() {
super.onDestroy()
unbindService(serviceConnection)//也可以不调用,Activity销毁时,Service会自动销毁
}
}
复制代码
IntentService
IntentService 为 Service 的子类,IntentService会创建一个线程,onHandleIntent中的代码就是运行在这个线程之中
,可以在 onHandleIntent 中执行耗时操作,执行完成后会自动销毁。使用示例如下:
class MyIntentService : IntentService("MyIntentService") {
override fun onHandleIntent(intent: Intent?) {
when (intent?.action) {
ACTION_FOO -> {
val param1 = intent.getStringExtra(EXTRA_PARAM1)
val param2 = intent.getStringExtra(EXTRA_PARAM2)
handleActionFoo(param1, param2)
}
ACTION_BAZ -> {
val param1 = intent.getStringExtra(EXTRA_PARAM1)
val param2 = intent.getStringExtra(EXTRA_PARAM2)
handleActionBaz(param1, param2)
}
}
}
private fun handleActionFoo(param1: String?, param2: String?) {
TODO("Handle action Foo")
}
private fun handleActionBaz(param1: String?, param2: String?) {
Log.i("zx", "param1=$param1,param2=$param2," + Thread.currentThread().name)
}
companion object {
@JvmStatic
fun startActionFoo(context: Context, param1: String, param2: String) {
val intent = Intent(context, MyIntentService::class.java).apply {
action = ACTION_FOO
putExtra(EXTRA_PARAM1, param1)
putExtra(EXTRA_PARAM2, param2)
}
context.startService(intent)
}
@JvmStatic
fun startActionBaz(context: Context, param1: String, param2: String) {
val intent = Intent(context, MyIntentService::class.java).apply {
action = ACTION_BAZ
putExtra(EXTRA_PARAM1, param1)
putExtra(EXTRA_PARAM2, param2)
}
context.startService(intent)
}
}
}
//使用时
MyIntentService.startActionBaz(this,"123","456")
复制代码
在 Android8.0 以上版本中,当应用本身未在前台运行时,系统会对运行后台服务施加限制,所以 IntentService 在 Android8.0 上被废弃了,可以用 WorkManager 或者 JobIntentService 代替。
BroadcastReceiver
Android 系统会在发生各种系统事件时发送广播,例如系统启动或设备开始充电时,应用也可以发送自定义广播来通知其他应用它们可能感兴趣的事件(例如,一些新数据已下载)。应用可以注册接收特定的广播。广播发出后,系统会自动将广播传送给同意接收这种广播的应用。
发送广播
有三种方式发送广播
sendOrderedBroadcast(Intent, String)
发送一个有序广播。接收器按照优先级逐个顺序执行,接收器可以向下传递结果,向下传递时可以往广播里存入数据(setResultExtras(Bundle)
),下一个接受者就可以接收到存入的数据(val bundle = getResultExtras(true)
)。一个接受者也可以完全中止广播(BroadcastReceiver.abortBroadcast()
),使其不再传递给后续其他接收器。接收器的运行顺序可以通过匹配的 intent-filter 的 android:priority 属性来控制,数值越大优先级越高;具有相同优先级的接收器将按注册先后顺序运行。sendBroadcast(Intent)
按随机的顺序向所有接收器发送广播。这称为普通广播。这种方法效率更高,但也意味着接收器无法从其他接收器读取结果,无法传递从广播中收到的数据,也无法中止广播。LocalBroadcastManager.sendBroadcast
将广播发送给同一应用中的接收器。这种实现方法的效率更高(无需进行进程间通信),而且无需担心其他应用在收发您的广播时带来的任何安全问题。
发送时可以指定接受者包名,也可以设置Action和Extra,示例代码如下:
val intent: Intent = Intent();
intent.action = "com.example.broadcast.MY_Action"
intent.putExtra("key", "value")
intent.setPackage("com.zx.test")
sendBroadcast(intent)
复制代码
接收广播
静态注册,接收广播步骤如下:
-
自定义一个类继承 BroadcastReceiver
-
重写 onReceive 方法
-
在 manifest.xml 中注册,并通过 intent-filter 指定需要接收的 action,例如:android.intent.action.BOOT_COMPLETED 表示系统启动完成的广播。
示例如下:
<receiver
android:name=".broadcast.MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
<action android:name="android.intent.action.LOCALE_CHANGED"/>
</intent-filter>
</receiver>
复制代码
动态注册,接收广播步骤如下:
-
自定义一个类继承 BroadcastReceiver
-
重写 onReceive 方法
-
创建 IntentFilter 并调用 registerReceiver(BroadcastReceiver, IntentFilter) 来注册接收。
示例如下:
val networkStateReceiver = NetworkStateReceiver() //自定义的广播接收器
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) //网络连接发生了变化 的系统广播
registerReceiver(networkStateReceiver, intentFilter)
复制代码
静态注册和动态注册区别
- 动态注册广播不是常驻型广播,也就是说广播跟随activity的生命周期。注意: 在activity结束前,移除广播接收器。
静态注册是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。 - 当广播为有序广播时:
1 优先级高的先接收
2 同优先级的广播接收器,动态优先于静态
3 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。 - 当广播为普通广播时:
1 无视优先级,动态广播接收器优先于静态广播接收器
2 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。
对进程的影响
当接收器的onReceive()
中的代码正在运行时,它被认为是前台进程,系统不会杀死它。但是当 onReceive()
返回了(执行完成后),BroadcastReceiver 就不再活跃,如果它的进程中只有BroadcastReceiver(APP最近未启动,整个进程中没有活跃的Activity或者前台任务),系统会将其进程视为低优先级进程,并随时可能会将其终止,那么进程中的线程也就会被终止,因此不应从广播接收器启动长时间运行的后台线程,也不应该启动一个后台Service(8.0以上,启动后台Service会被限制)。当然了,如果你能确保当前BroadcastReceiver所在进程不会被杀死(比如动态注册的广播,此时APP有很多Activity正在运行,是活跃进程),你也可以在onReceive()
中开一个线程,用于处理耗时任务。由于onReceive()
运行在主线程,所以主线程不能做的事情,onReceive()
中都不能做,比如网路请求、耗时任务等等。那如果想要在onReceive()
中执行耗时任务怎么办呢?官方给出的方案是使用goAsync(),告诉系统即使onReceive()返回了,也不要回收广播,我会在广播处理完成之后调用pendingResult.finish()通知你,你再回收
,这种方式代码依旧运行在主线程,所以需要开启一个线程,在线程中执行耗时操作,耗时操作执行完成后调用pendingResult.finish()
,这样广播就可以被回收了。即使新开了线程,但是耗时操作依旧被限制在10s内(也就是说10s内必须调用pendingResult.finish()
),超过10s,依然会触发ANR。示例代码如下:
override fun onReceive(context: Context, intent: Intent) {
//告诉系统,即使onReceive返回了,在pendingResult.finish()之前,不要回收BroadcastReceiver
val pendingResult: PendingResult = goAsync()
//用协程开一个线程
CoroutineScope(Dispatchers.IO).launch {
delay(9000)//模拟耗时操作。即使不在主线程,但是依旧不能超过10秒,否则仍然会ANR
withContext(Dispatchers.Main)
{
//切换到主线程,并告诉系统,广播执行完了。
pendingResult.finish()
}
}
}
复制代码
如果是动态注册的接收器,
onReceive
执行时肯定是在APP运行期间,此时进程不会被回收,如果要执行耗时任务,创建的线程也不会被回收,此时不需要使用goAsync()
,因为用了goAsync(),即使不在主线程中,也必须要在10秒内调用pendingResult.finish()
,否则就会ANR,这个规则反而限制了线程的运行时间。这种情况只需要创建一个线程,并将耗时操作放在线程里就行了。
ContentProvider
内容提供者,提供了与其他应用共享数据的方法,通过ContentProvider,可以使其他应用安全地访问和修改我们的应用数据。与ContentProvider一起使用的是ContentResolver,ContentResolver用于从其他应用或者系统获取数据。ContentProvider分为系统ContentProvider和自定义ContentProvider,我们可以通过系统的ContentProvider获取系统中的数据,比如联系人、短信、图库等等,此时我们只需要关注获取数据的ContentResolver即可。
ContentResolver
使用ContentResolver有如下几步:
- 申请相关权限。
- 确定URI
- 获取ContentResolver,并调用ContentResolver相关的方法,比如查询。
示例代码如下:
//查询系统图库中的图片
val uri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor = contentResolver.query(uri, null, null, null, MediaStore.Images.Media.DATE_ADDED + " desc LIMIT 5")
if (cursor != null) {
while (cursor.moveToNext()) {
val title = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.TITLE));
//图片的地址
val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
val height = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.HEIGHT));
val width = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.WIDTH));
val size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE));
Log.i("zx", "图片文件名:$title,路径:$path,尺寸:$height x $width,文件大小:$size");
}
cursor.close()
}
复制代码
通过ContentResolver的query(uri, projection, selection, selectionArgs, sortOrder)
即可查询,示例中按照图片添加时间降序排列并且限制了只查询5条,输出结果如下:
文件名:IMG_20210523_160524,路径:/storage/emulated/0/DCIM/Camera/IMG_20210523_160524.jpg,尺寸:3016 x 4032,大小:4765375
文件名:wx_camera_1620566260144,路径:/storage/emulated/0/Pictures/WeiXin/wx_camera_1620566260144.jpg,尺寸:1920 x 1080,大小:995780
文件名:IMG_20210507_230154,路径:/storage/emulated/0/DCIM/Camera/IMG_20210507_230154.jpg,尺寸:3016 x 4032,大小:5354687
文件名:IMG_20210507_190849,路径:/storage/emulated/0/DCIM/Camera/IMG_20210507_190849.jpg,尺寸:3016 x 4032,大小:4360429
文件名:IMG_20210505_154931,路径:/storage/emulated/0/DCIM/Camera/IMG_20210505_154931.jpg,尺寸:3016 x 4032,大小:9205474
复制代码
query方法的参数类似于SQL查询参数,如下表:
query() 参数 | SELECT 关键字/参数 | 备注 |
---|---|---|
Uri |
FROM *table_name* |
类似于SQL中的表名,上边的例子制定了从”系统图库这个表” |
projection |
*col,col,col,...* |
指定需要查询哪些列 |
selection |
`WHERE col = value | 指定查询行的条件 |
selectionArgs |
没有SQL等效项 | selection中的 ? 占位符会被这里的参数所替换 |
sortOrder |
ORDER BY *col,col,...* |
指定在返回的 Cursor 中各行的显示顺序 |
除了query,ContentResolver提供了其他几个方法insert、delete、update方法,同样是类似于SQL的增删改查操作。
ContentProvider
下面来讲讲如何使用ContentProvider将数据提供给其他应用。
-
定义如何存储数据。ContentProvider只是一个介于数据与外部应用之间的桥梁,ContentProvider本身并不能存储数据,一般使用SQLite进行存储数据。
-
继承ContentProvider,制定唯一标识authorities,实现增删改查等方法,并在manifest注册。
-
外部应用获取ContentResolver,调用ContentResolver相关的方法,比如查询。
示例代码如下:
数据库存储
class MyDBHelper(context: Context?) : SQLiteOpenHelper(context, "zx.db", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
// 创建两个表:用户表 和职业表
db.execSQL("CREATE TABLE IF NOT EXISTS $USER_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
db.execSQL("CREATE TABLE IF NOT EXISTS $JOB_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, job TEXT)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
companion object {
// 表名
const val USER_TABLE_NAME = "user"
const val JOB_TABLE_NAME = "job"
}
}
复制代码
自定义的ContentProvider
class MyContentProvider : ContentProvider() {
//外部应用使用ContentProvider时拼接URI时需要,在manifest也指定了,推荐为ContentProvider包名+类名
//外部应用使用时URI就必须为 content://zx.com.myContentProvider/表名
//之所以在manifest中指定了,这里又写一遍是为了在UriMatcher中使用
private val AUTOHORITY = "zx.com.myContentProvider"
private val mMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
private val User_Code = 1
private val Job_Code = 2
private lateinit var myDbHelper: MyDBHelper
private lateinit var db: SQLiteDatabase
private lateinit var scope: CoroutineScope
init {
// 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
// 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
mMatcher.addURI(AUTOHORITY, "user", User_Code);
mMatcher.addURI(AUTOHORITY, "job", Job_Code);
}
override fun onCreate(): Boolean {
myDbHelper = MyDBHelper(context);
db = myDbHelper.writableDatabase;
return true
}
override fun insert(uri: Uri, values: ContentValues?): Uri {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
val table = getTableName(uri)
// 向该表添加数据
db.insert(table, null, values);
// 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
context!!.contentResolver?.notifyChange(uri, null)
return uri;
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
// 该方法在最下面
val table = getTableName(uri)
// 查询数据
return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null)
}
/**
* 根据URI匹配 URI_CODE,从而匹配数据库中相应的表名
*/
private fun getTableName(uri: Uri): String? {
var tableName: String? = null
when (mMatcher.match(uri)) {
User_Code -> tableName = MyDBHelper.USER_TABLE_NAME
Job_Code -> tableName = MyDBHelper.JOB_TABLE_NAME
}
return tableName
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0;
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
}
复制代码
外部应用使用时
findViewById<Button>(R.id.button).setOnClickListener {
val uri = Uri.parse("content://zx.com.myContentProvider/user");
val cursor = contentResolver.query(uri, arrayOf("_id", "name"), null, null, null)
if (cursor != null) {
Log.i("zx", "插入数据前")
while (cursor.moveToNext()) {
Log.i("zx", "_id=" + cursor.getInt(0) + ",name=" + cursor.getString(1))
}
cursor.close()
}
// 插入表中数据
val values = ContentValues()
values.put("name", "name" + SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()))
contentResolver.insert(uri, values)
//耗时查询应该使用AsyncQueryHandler,AsyncQueryHandler会开启子线程异步查询,不会阻塞主线程,实际项目中应该使用AsyncQueryHandler,或者自己新开一个线程。
val afterInsertCursor =
contentResolver.query(uri, arrayOf("_id", "name"), null, null, null)
if (afterInsertCursor != null) {
Log.i("zx", "插入数据后")
while (afterInsertCursor.moveToNext()) {
Log.i(
"zx",
"_id=" + afterInsertCursor.getInt(0) + ",name=" + afterInsertCursor.getString(
1
)
)
}
afterInsertCursor.close()
}
}
复制代码
点击按钮后输出
插入数据前
插入数据后
_id=1,name=name2021-05-30 21:21:16
复制代码
再次点击按钮输出
插入数据前
_id=1,name=name2021-05-30 21:21:16
插入数据后
_id=1,name=name2021-05-30 21:21:16
_id=2,name=name2021-05-30 21:21:22
复制代码
外部应用中通过ContentProvider实现了查询和插入,更新和删除操作类似,不再赘述。
3个工具类
另外,Android 提供了3个用于辅助ContentProvider的工具类。
ContentUris
主要通过withAppendedId()
和parseId()
操作URI
// withAppendedId()作用:向URI追加一个id
val uri = Uri.parse("content://zx.com.myContentProvider/user")
val resultUri = ContentUris.withAppendedId(uri, 7);
// 最终生成后的Uri为:content://zx.com.myContentProvider/user/7
// parseId()作用:从URL中获取ID
val uri1 = Uri.parse("content://zx.com.myContentProvider/user/7")
val personid = ContentUris.parseId(uri1);
//获取的结果为:7
复制代码
UriMatcher
根据Uri匹配对应的数据表,步骤如下:
//1.初始化UriMatcher,未匹配上时会返回-1
private val mMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
//2.指定uri的返回规则
mMatcher.addURI(AUTOHORITY, "user", User_Code)
mMatcher.addURI(AUTOHORITY, "job", Job_Code)
// 若URI资源路径 = content://zx.com.myContentProvider/user ,则返回User_Code
// 若URI资源路径 = content://zx.com.myContentProvider/job ,则返回Job_Code
//若URI中的authorities与指定的不相同,则返回NO_MATCH,如果authorities后的path没有匹配上也返回NO_MATCH
//3.使用UriMatcher获取规则中定义的code,根据code获取表名
when (mMatcher.match(uri)) {
User_Code -> tableName = MyDBHelper.USER_TABLE_NAME
Job_Code -> tableName = MyDBHelper.JOB_TABLE_NAME
}
复制代码
ContentObserver
根据Uri观察ContentProvider 中的数据变化,数据变化时自动回调ContentObserver的onChange()
,使用示例如下:
//继承ContentObserver并实现onChange
class MyContentObserver(handler: Handler?) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
Log.i("zx", "数据改变了")
}
}
//注册观察者
myContentObserver = MyContentObserver(null)
contentResolver.registerContentObserver(uri, true, myContentObserver)
// 在ContentProvider的增删改方法中,当该Uri的ContentProvider数据发生变化时,通知ContentObserver
context!!.contentResolver?.notifyChange(uri, null)
//解除观察者
contentResolver.unregisterContentObserver(myContentObserver)
复制代码
Activity
没什么好讲的,参照mp.weixin.qq.com/s/2O2dGQQpC…