Android四大组件

Application

四大组件

Activity

Service

多线程

当我们需要执行一些耗时操作,比如发起一条网络请求时,考虑到网速等其他原因,服务器未必能够立刻响应我们的请求,如果不将这类操作放在子线程里运行,就会导致主线程被阻塞,从而影响用户对软件的正常使用。

继承Thread

1
2
3
4
5
6
class MyThread : Thread() {
override fun run() {
// 编写具体的逻辑
}
}
MyThread().start()

实现Runnable

1
2
3
4
5
6
7
class MyThread : Runnable {
override fun run() {
// 编写具体的逻辑
}
}
val myThread = MyThread()
Thread(myThread).start()

Kotlin还给我们提供了一种更加简单的开启线程的方式,写法如下:

1
2
3
thread {
// 编写具体的逻辑
}

注意:UI界面更新操作:不允许在子线程中进行,但是有些时候,我们必须在子线程里执行一些耗时任务,然后根据任务的执行结果来更新相应的UI控件,这该如何是好呢?

(1)Android提供了一套异步消息处理机制(Handler),完美地解决了在子线程中进行U操作的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MainActivity : AppCompatActivity() {

val updateText = 1

val handler = object : Handler(Looper.getMaininLooper()) {
//handleMessage()中的代码就是在主线程当中运行的了,所以我们可以放心地在这里进行UI操作
override fun handleMessage(msg: Message) {
// 在这里可以进行UI操作
when (msg.what) {
updateText -> textView.text = "Nice to meet you"
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
changeTextBtn.setOnClickListener {
thread {
val msg = Message()
msg.what = updateText
handler.sendMessage(msg) // 将Message对象发送出去
}
}
}

}

解析异步消息处理机制

Android中的异步消息处理主要由4个部分组成:Message、Handler、MessageQueue和Looper。

  • Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间传递数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。
  • Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()post()等,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()中。
  • MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
  • Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()

首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler的构造函数中我们传入了Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。

img

(2)使用AsyncTask

借助AsyncTask,即使你对异步消息处理机制完全不了解,也可以十分简单地从子线程切换到主线程。当然,AsyncTask背后的实现原理也是基于异步消息处理机制的,只是Android帮我们做了很好的封装而已。

由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须创建一个子类去继承它。在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下:

  • Params。在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress。在后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result。当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
1
2
3
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {
...
}

我们还需要重写AsyncTask中的几个方法才能完成对任务的定制。经常需要重写的方法有以下4个。

  • onPreExecute():这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

  • doInBackground(Params...):这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成,就可以通过return语句将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Unit,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress (Progress...)方法来完成。

  • onProgressUpdate(Progress...):当在后台任务中调用了publishProgress(Progress...)方法后,onProgressUpdate (Progress...)方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

  • onPostExecute(Result):当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据进行一些UI操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class DownloadTask : AsyncTask<Unit, Int, Boolean>() {

override fun onPreExecute() {
progressDialog.show() // 显示进度对话框
}

override fun doInBackground(vararg params: Unit?) = try {
while (true) {
val downloadPercent = doDownload() // 这是一个虚构的方法
publishProgress(downloadPercent)
if (downloadPercent >= 100) {
break
}
}
true
} catch (e: Exception) {
false
}

override fun onProgressUpdate(vararg values: Int?) {
// 在这里更新下载进度
progressDialog.setMessage("Downloaded ${values[0]}%")
}

override fun onPostExecute(result: Boolean) {
progressDialog.dismiss()// 关闭进度对话框
// 在这里提示下载结果
if (result) {
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show()
}
}

}

简单来说,使用AsyncTask的诀窍就是,在doInBackground()方法中执行具体的耗时任务,在onProgressUpdate()方法中进行UI操作,在onPostExecute()方法中执行一些任务的收尾工作。

如果想要启动这个任务,只需编写以下代码即可:

1
DownloadTask().execute()

当然,你也可以给execute()方法传入任意数量的参数,这些参数将会传递到DownloadTask的doInBackground()方法当中。

++++++++++++++

下面开始正式介绍Service

https://www.cnblogs.com/lwbqqyumidi/p/4181185.html

https://juejin.cn/post/7029863085548503071

https://blog.csdn.net/javazejian/article/details/52709857

Service是Android中实现程序后台运行的解决方案,它非常适合执行那些不需要和用户交互而且还要求长期运行的任务。

  • 不依赖于用户可视化UI界面,(除了前台服务,前台Service就是与Notification界面结合使用的)
  • 具有长时间运行的特性。

一、Service分类

1、按照运行进程来分:

  • 本地服务(local service)

本地服务是运行在我们应用的主进程中,与别的组件运行在同一进程中,这样做的好处是节省资源,在同一进程中,组件之间的通信方便。弊端就是如果主进程被杀死,服务便会终止。常见案例:音乐播放器

  • 远程服务(remote service)

远程服务运行在独立的进程中,所以我们在manifest中注册该服务的时候需要android:process=”:remote”来进行指定运行的进程。这样做的好处就是该服务不受其它进程的影响,便于为多个进程提供服务。弊端就是独立的进程占用一定的系统资源,通信复杂。

2、按照运行效果来分:

  • 前台服务 用于由于内存不足系统杀死服务的时候能够给予用户一定的通知作用,所以前台服务必须具有一个Notificattion状态栏,即显示在通知栏上。常见的案例:手机管家、音乐播放器
  • 后台服务 默认的服务,当我们启动一个服务,不做处理的时候,就是一个后台服务。常见案例:新闻的自动更新、天气的获取

二、Service AndroidManifest.xml 声明

使用Service一定要在AndroidManifest.xml 中声明,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<service android:enabled=["true" | "false"] 
//是否能被实例化,默认为 true因为父标签 也有 enable 属性,所以必须两个都为默认值 true 的情况下服务才会被激活,否则不会激活。
android:exported=["true" | "false"]
//代表是否能被其他应用隐式调用,其默认值是由service中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。为false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。
android:icon="drawable resource"
android:isolatedProcess=["true" | "false"]
//设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。
android:label="string resource"
android:name="string"
//service对应的名字
android:permission="string"
//权限声明
android:process="string"
//是否需要在单独的进程中运行,当设置为android:process=”:remote”时,代表Service在单独的进程中运行。注意“:”很重要,它的意思是指要在当前进程名称前面附加上当前的包名,所以“remote”和”:remote”不是同一个意思,前者的进程名称为:remote,而后者的进程名称为:App-packageName:remote。
. . .
>

</service>

三、Service分为两种形式

1、start service

Started Service相对比较简单,通过context.startService(Intent serviceIntent)启动Servicecontext.stopService(Intent serviceIntent)停止此Service。当然,在Service内部,也可以通过stopSelf(...)方式停止其本身。

基本使用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyService : Service() {
...
override fun onCreate() {
super.onCreate()
Log.d("MyService", "onCreate executed")
}

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d("MyService", "onStartCommand executed")
return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
super.onDestroy()
Log.d("MyService", "onDestroy executed")
}
}
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
startService(intent) // 启动Service
}
stopServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
stopService(intent) // 停止Service
}
}

}

重点关注下onStartCommand(Intent intent, int flags, int startId)方法:

其中参数flags默认情况下是0,对应的常量名为START_STICKY_COMPATIBILITY。startId是一个唯一的整型,用于表示此次Client执行startService(...)的请求标识,在多次startService(...)的情况下,呈现0,1,2….递增。另外,此函数具有一个int型的返回值,具体的可选值及含义如下:

  • START_NOT_STICKY:当Service因为内存不足而被系统kill后,接下来未来的某个时间内,即使系统内存足够可用,系统也不会尝试重新创建此Service。除非程序中Client明确再次调用startService(...)启动此Service。
  • START_STICKY:当Service因为内存不足而被系统kill后,接下来未来的某个时间内,当系统内存足够可用的情况下,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand(...)方法,但其中的Intent将是null,pendingintent除外。
  • START_REDELIVER_INTENT:与START_STICKY唯一不同的是,回调onStartCommand(...)方法时,其中的Intent将是非空,将是最后一次调用startService(...)中的intent。

2、bind service

Bound Service的主要特性在于Service的生命周期是依附于Client的生命周期的,当Client不存在时,Bound Service将执行onDestroy,同时通过Service中的Binder对象可以较为方便进行Client-Service通信。Bound Service一般使用过程如下:

  • 1.自定义Service继承基类Service,并重写onBind(Intent intent)方法,此方法中需要返回具体的Binder对象;
  • 2.Client通过实现ServiceConnection接口来自定义ServiceConnection,并通过bindService (Intent service, ServiceConnection sc, int flags)方法将Service绑定到此Client上;
  • 3.自定义的ServiceConnection中实现onServiceConnected(ComponentName name, IBinder binder)方法,获取Service端Binder实例;
  • 4.通过获取的Binder实例进行Service端其他公共方法的调用,以完成Client-Service通信;
  • 5.当Client在恰当的生命周期(如onDestroy等)时,此时需要解绑之前已经绑定的Service,通过调用函数unbindService(ServiceConnection sc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class MyService : Service() {

private val mBinder = DownloadBinder()

class DownloadBinder : Binder() {

fun startDownload() {
Log.d("MyService", "startDownload executed")
}

fun getProgress(): Int {
Log.d("MyService", "getProgress executed")
return 0
}

}

override fun onBind(intent: Intent): IBinder {
return mBinder
}
...
}
class MainActivity : AppCompatActivity() {

lateinit var downloadBinder: MyService.DownloadBinder

private val connection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName, service: IBinder) {
downloadBinder = service as MyService.DownloadBinder
downloadBinder.startDownload()
downloadBinder.getProgress()
}

override fun onServiceDisconnected(name: ComponentName) {
}

}

override fun onCreate(savedInstanceState: Bundle?) {
...
bindServiceBtn.setOnClickListener {
val intent = Intent(this, MyService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE) // 绑定Service
}
unbindServiceBtn.setOnClickListener {
unbindService(connection) // 解绑Service
}
}

}

三、Service生命周期

  • 1.onCreate(Client首次startService(..)) >> onStartCommand >> onStartCommand - optional … >> onDestroy(Client调用stopService(..))

注:onStartCommand(..)可以多次被调用,onDestroy()与onCreate()想匹配,当用户强制kill掉进程时,onDestroy()是不会执行的。

  • 2.对于同一类型的Service,Service实例一次永远只存在一个,而不管Client是否是相同的组件,也不管Client是否处于相同的进程中。
  • 3.Service通过startService(..)启动Service后,此时Service的生命周期与Client本身的什么周期是没有任何关系的,只有Client调用stopService(..)或Service本身调用stopSelf(..)才能停止此Service。当然,当用户强制kill掉Service进程或系统因内存不足也可能kill掉此Service。
  • 4.Client A 通过startService(..)启动Service后,可以在其他Client(如Client B、Client C)通过调用stopService(..)结束此Service。
  • 5.Client调用stopService(..)时,如果当前Service没有启动,也不会出现任何报错或问题,也就是说,stopService(..)无需做当前Service是否有效的判断。
  • 6.startService(Intent serviceIntent),其中的intent既可以是显式Intent,也可以是隐式Intent,当Client与Service同处于一个App时,一般推荐使用显示Intent。当处于不同App时,只能使用隐式Intent。

当Service需要运行在单独的进程中,AndroidManifest.xml声明时需要通过android:process指明此进程名称,当此Service需要对其他App开放时,android:exported属性值需要设置为true(当然,在有intent-filter时默认值就是true)。

四、前台Service

从Android 8.0系统开始,==只有当应用保持在前台可见状态的情况下,Service才能保证稳定运行,一旦应用进入后台之后,Service随时都有可能被系统回收。而如果你希望Service能够一直保持运行状态,就可以考虑使用前台Service或WorkManager==。前台Service和普通Service最大的区别就在于,它一直会有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyService : Service() {
...
override fun onCreate() {
super.onCreate()
Log.d("MyService", "onCreate executed")
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("my_service", "前台Service通知",
NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
val intent = Intent(this, MainActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "my_service")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.setContentIntent(pi)
.build()
//调用startForeground()方法后就会让MyService变成一个前台Service,并在系统状态栏显示出来
startForeground(1, notification)
}
...
}

==使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行==

1
2
3
4
5
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicetest">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
...
</manifest>

现在即使你退出应用程序,MyService也会一直处于运行状态,而且不用担心会被系统回收。当然,MyService所对应的通知也会一直显示在状态栏上面。如果用户不希望我们的程序一直运行,也可以选择手动杀掉应用,这样MyService就会跟着一起停止运行了。

五、IntentService

https://www.cnblogs.com/DMingO/p/13391435.html

Service中的代码都是默认运行在主线程当中的,如果直接在Service里处理一些耗时的逻辑,就很容易出现ANR(Application Not Responding)的情况。需要用到Android多线程编程的技术了,我们应该在Service的每个具体的方法里开启一个子线程,然后在这里处理那些耗时的逻辑。

1
2
3
4
5
6
7
8
9
10
class MyService : Service() {
...
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
thread {
// 处理具体的逻辑
stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
}

但是,这种Service一旦启动,就会一直处于运行状态,必须调用stopService()或stopSelf()方法,或者被系统回收,Service才会停止。虽说这种写法并不复杂,但是总会有一些程序员忘记开启线程,或者忘记调用stopSelf()方法。

为了可以简单地创建一个异步的、会自动停止的Service,Android专门提供了一个IntentService类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//这个字符串可以随意指定,只在调试的时候有用
class MyIntentService : IntentService("MyIntentService") {

override fun onHandleIntent(intent: Intent?) {
// 打印当前线程的id
Log.d("MyIntentService", "Thread id is ${Thread.currentThread().name}")
}

override fun onDestroy() {
super.onDestroy()
Log.d("MyIntentService", "onDestroy executed")
}

}


class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
startIntentServiceBtn.setOnClickListener {
// 打印主线程的id
Log.d("MainActivity", "Thread id is ${Thread.currentThread().name}")
val intent = Intent(this, MyIntentService::class.java)
startService(intent)
}
}

}

img

可以看到,不仅MyIntentService和MainActivity所在的线程名不一样,而且onDestroy()方法也得到了执行,说明MyIntentService在运行完毕后确实自动停止了。

==但是IntentService已经弃用,官方建议使用JetPack组件中的WorkManager或者JobIntentService类代替它。==

Content Provider

https://www.cnblogs.com/huansky/p/13785634.html

https://www.cnblogs.com/lihuawei/p/16636956.html

不同应用程序之间的数据共享,数据提供方提供数据,使用方使用 content://authorities/path 类似的 URI 来访问数据。

运行时权限

在学习content Provider之前先学习一下运行是权限,就是在程序运行过程中,申请权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCall.setOnClickListener {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.CALL_PHONE), 1)
} else {
call()
}
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
} else {
Toast.makeText(this, "You denied the permission",
Toast.LENGTH_SHORT).show()
}
}
}
}

private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}

}

所有的危险权限都要进行运行时权限申请

img

下面进行学习内容提供者相关内容:

(1)访问其他程序中的数据

如果想访问内容提供者中的共享数据,需要使用ContentResolver类来进行访问,ContentResolve可以调用contentprovider中的增删改查。

不同于SQLiteDatabase的是,ContentResolver是用uri来访问,被称为内容uri。

内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。

URI 的格式如下:

1
2
3
4
5
content://com.wang.provider.myprovider/tablename/id
content//:用来说明content provider控制这些数据
com.wang.provider.myprovider:外部应用通过这个标识来找到它
tablename:要找的表名
id:找到该id对应的数据

内容URI最标准的格式如下:

1
2
content://com.example.app.provider/table1
content://com.example.app.provider/table2

在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析方法很简单:

1
val uri = Uri.parse("content://com.example.app.provider/table1")

之后就可以使用uri对象来查询,修改,添加,删除provider提供的数据库表中的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//查询
val cursor = contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder)
while (cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
//添加
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values)

//更新
val values = contentValuesOf("column1" to "")
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))

//删除
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))

(2)创建自定义的ContentProvider

新建一个类去继承ContentProvider,并重写6个抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyProvider : ContentProvider() {

override fun onCreate(): Boolean {
return false
}

override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
return null
}

override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}

override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
return 0
}

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}

override fun getType(uri: Uri): String? {
return null
}

}

(1) onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。

(2) query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。

(3) insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。

(4) update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。

(5) delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。

(6) getType()。根据传入的内容URI返回相应的MIME类型。

内容URI的格式主要有两种,以路径结尾表示期望访问该表中所有的数据,以id结尾表示期望访问该表中拥有相应id的数据。可以使用通配符分别匹配这两种格式的内容URI,规则如下。

  • *表示匹配任意长度的任意字符。
  • #表示匹配任意长度的数字。
1
2
content://com.example.app.provider/ *
content://com.example.app.provider/table1/#

再借助UriMatcher可以轻松实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyProvider : ContentProvider() {

private val table1Dir = 0
private val table1Item = 1
private val table2Dir = 2
private val table2Item = 3

private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

init {
uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
uriMatcher.addURI("com.example.app.provider ", "table1/#", table1Item)
uriMatcher.addURI("com.example.app.provider ", "table2", table2Dir)
uriMatcher.addURI("com.example.app.provider ", "table2/#", table2Item)
}
...
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
//UriMatcher的match()对传入的Uri对象进行匹配,
//如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码
when (uriMatcher.match(uri)) {
table1Dir -> {
// 查询table1表中的所有数据
}
table1Item -> {
// 查询table1表中的单条数据
}
table2Dir -> {
// 查询table2表中的所有数据
}
table2Item -> {
// 查询table2表中的单条数据
}
}
...
}
...
}

对于getType()方法,它是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。

一个内容URI所对应的MIME字符串主要由3部分组成:

● 必须以vnd开头。

● 如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。

● 最后接上vnd..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
content://com.example.app.provider/table1
vnd.android.cursor.dir/vnd.com.example.app.provider.table1

content://com.example.app.provider/table1/1
vnd.android.cursor.item/vnd.com.example.app.provider.table1
//完善内容
class MyProvider : ContentProvider() {
...
override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
else -> null
}
}

完整的provider的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class DatabaseProvider : ContentProvider() {

private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.databasetest.provider"
private var dbHelper: MyDatabaseHelper? = null

//by lazy代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,
//只有当uriMatcher变量首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋给uriMatcher
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority, "book", bookDir)
matcher.addURI(authority, "book/#", bookItem)
matcher.addURI(authority, "category", categoryDir)
matcher.addURI(authority, "category/#", categoryItem)
matcher
}

//借助?.操作符和let函数判断它的返回值是否为空:
//如果为空就使用?:操作符返回false,表示ContentProvider初始化失败;
//如果不为空就执行let函数中的代码
override fun onCreate() = context?.let {
dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
true
} ?: false

override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?) = dbHelper?.let {
// 查询数据
val db = it.readableDatabase
val cursor = when (uriMatcher.match(uri)) {
bookDir -> db.query("Book", projection, selection, selectionArgs,
null, null, sortOrder)
bookItem -> {
val bookId = uri.pathSegments[1]
db.query("Book", projection, "id = ?", arrayOf(bookId), null, null,
sortOrder)
}
categoryDir -> db.query("Category", projection, selection, selectionArgs,
null, null, sortOrder)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.query("Category", projection, "id = ?", arrayOf(categoryId),
null, null, sortOrder)
}
else -> null
}
cursor
}

override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
// 添加数据
val db = it.writableDatabase
val uriReturn = when (uriMatcher.match(uri)) {
bookDir, bookItem -> {
val newBookId = db.insert("Book", null, values)
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir, categoryItem -> {
val newCategoryId = db.insert("Category", null, values)
Uri.parse("content://$authority/category/$newCategoryId")
}
else -> null
}
uriReturn
}

override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?) = dbHelper?.let {
// 更新数据
val db = it.writableDatabase
val updatedRows = when (uriMatcher.match(uri)) {
bookDir -> db.update("Book", values, selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.update("Book", values, "id = ?", arrayOf(bookId))
}
categoryDir -> db.update("Category", values, selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.update("Category", values, "id = ?", arrayOf(categoryId))
}
else -> 0
}
updatedRows
} ?: 0

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?)
= dbHelper?.let {
// 删除数据
val db = it.writableDatabase
val deletedRows = when (uriMatcher.match(uri)) {
bookDir -> db.delete("Book", selection, selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.delete("Book", "id = ?", arrayOf(bookId))
}
categoryDir -> db.delete("Category", selection, selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category", "id = ?", arrayOf(categoryId))
}
else -> 0
}
deletedRows
} ?: 0

override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.
provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.
provider.category"
else -> null
}
}

另外,还有一点需要注意,ContentProvider一定要在AndroidManifest.xml文件中注册才可以使用。 访问上面程序的共享数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class MainActivity : AppCompatActivity() {

var bookId: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addData.setOnClickListener {
// 添加数据
val uri = Uri.parse("content://com.example.databasetest.provider/book")
val values = contentValuesOf("name" to "A Clash of Kings",
"author" to "George Martin", "pages" to 1040, "price" to 22.85)
val newUri = contentResolver.insert(uri, values)
bookId = newUri?.pathSegments?.get(1)
}
queryData.setOnClickListener {
// 查询数据
val uri = Uri.parse("content://com.example.databasetest.provider/book")
contentResolver.query(uri, null, null, null, null)?.apply {
while (moveToNext()) {
val name = getString(getColumnIndex("name"))
val author = getString(getColumnIndex("author"))
val pages = getInt(getColumnIndex("pages"))
val price = getDouble(getColumnIndex("price"))
Log.d("MainActivity", "book name is $name")
Log.d("MainActivity", "book author is $author")
Log.d("MainActivity", "book pages is $pages")
Log.d("MainActivity", "book price is $price")
}
close()
}
}
updateData.setOnClickListener {
// 更新数据
bookId?.let {
val uri = Uri.parse("content://com.example.databasetest.provider/
book/$it")
val values = contentValuesOf("name" to "A Storm of Swords",
"pages" to 1216, "price" to 24.05)
contentResolver.update(uri, values, null, null)
}
}
deleteData.setOnClickListener {
// 删除数据
bookId?.let {
val uri = Uri.parse("content://com.example.databasetest.provider/
book/$it")
contentResolver.delete(uri, null, null)
}
}
}

}

Broadcast receiver

通过广播的方式进行消息传递。

(1)广播接收者

1
2
3
4
5
class MyReceivre: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Toast.makeText(context,"接受到消息",Toast.LENGTH_SHORT).show()
}
}

(2)注册广播接收者(静态 / 动态)

动态注册是在代码中注册,静态注册是在AndroidManifest.xml中的注册。

  • 静态注册

在AndroidManifest.xml里通过标签声明 (本例用于接收网路状态发生改变的注册)

1
2
3
4
5
6
<receiver android:name=".MyReceivre"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.CONFIGURATION_CHANGED"/>
</intent-filter>
</receiver>

另外,这里有非常重要的一点需要说明。Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,必须在AndroidManifest.xml文件中进行权限声明,否则程序将会直接崩溃。

  • 动态注册,动态注册必须程序启动才能收到广播,静态注册不用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : AppCompatActivity() {
private lateinit var myreceiver: MyReceivre
override fun onResume() {
super.onResume()
myreceiver = MyReceivre()
val intentFilter = IntentFilter()
//设置接收广播的类型
intentFilter.addAction("android.intent.action.CONFIGURATION_CHANGED")
//动态注册
registerReceiver(myreceiver, intentFilter)
}

override fun onPause() {
super.onPause()
//销毁
unregisterReceiver(myreceiver)
}
}

(3)发送广播

发送广播的类型分为:普通广播,有序广播,本地广播。

  • 普通广播
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendBroadcast(intent)
}
...
}
...
}

intent.setPackage(packageName)说明:在Android 8.0系统之后,静态注册的BroadcastReceiver是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver将无法接收到这条广播。

  • 有序广播

发送有序广播的方法:sendOrderedBroadcast(intent,null),(第一个参数仍然是Intent;第二个参数是一个与权限相关的字符串,这里传入null就行了)。可以接收到广播的程序按照优先级依次接收,优先级的设置:

1
2
3
<intent-filter android:priority="100">
<action android:name="android.intent.action.CONFIGURATION_CHANGED"/>
</intent-filter>

还可以阻塞传递,在onReceive方法最后:abortBroadcast()

  • 本地广播
1
2
private lateinit var  localBroadcastManager: LocalBroadcastManager
localBroadcastManager.sendBroadcast(intent)

Context

网络技术


Android四大组件
http://example.com/2024/01/06/Android基础知识/
作者
zlw
发布于
2024年1月6日
许可协议