抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

什么是 Service

服务(Service)是 Android 中实现程序后台运行的解决方案,去执行那些不需要和用户交互(不需要 UI 界面)而且还要求长期运行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。

需要在 AndroidManifest.xml 中进行注册,否则系统无法识别该 Service。通过 <service> 标签,我们可以设置 Service 的各种属性,如是否可被其他应用调用、运行进程等。

1
2
3
4
5
6
7
8
9
10
11
<!-- 启动状态的Service 声明 -->
<service android:name=".MyStartService" //指定Service的类名,例如.MyStartService指的是com.example.myapp.MyStartService
android:enabled="true" //指定Service是否可以被系统实例化,默认为true
android:exported="false" //指定Service是否可以被其他应用隐式调用。如果包含intent-filter,默认值为true,否则为false
android:process=":remote" //指定Service是否需要在单独的进程中运行。
android:isolatedProcess="false"> //置为true意味着服务会在一个特殊的进程下运行,与系统其他进程分开,并且没有自己的权限。
<!-- 可以添加 intent-filter 来允许隐式启动 -->
<!-- <intent-filter> -->
<!-- <action android:name="com.example.myapp.ACTION_START_SERVICE" /> -->
<!-- </intent-filter> -->
</service>

但是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。

服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。也就是说,我们需要在服务的内部手动创建子线程,并在这里执行具体的任务,否则就有可能出现主线程被阻塞住的情况。

Service 的类型

  1. 启动的 Service(Started Service)
    • 通过 startService() 启动。
    • 一旦启动,Service 会在后台独立运行,直到自己调用 stopSelf() 或外部调用 stopService()
    • 适合执行一次性任务,如下载或文件上传等。
  2. 绑定的 Service(Bound Service)
    • 通过 bindService() 与一个组件(如 Activity)绑定。
    • 当所有绑定的组件解除绑定后,Service 会自动停止。
    • 适合用于长时间运行的服务,并且需要与其他组件交互(如 Activity 获取数据等)。
  3. 前台 Service(Foreground Service)
    • 显示一个通知以提升优先级,避免因内存不足而被系统回收。
    • 适合需要用户持续关注的任务,如音乐播放或位置跟踪等。

多线程编程

线程的基本用法

定义一个线程只需要新建一个类继承自 Thread,然后重写父类的 run() 方法,并在里面编写我们想要处理的事务的逻辑即可,

new 出 MyThread 的实例,然后调用它的 start() 方法,这样 run() 方法中的代码就会在子线程当中运行了

1
2
3
4
5
6
7
class MyThread extends Thread{
@Override
public void run(){
//处理事务的具体逻辑
}
}
new MyThread().start();

使用实现 Runnable 接口的方式来定义一个线程

1
2
3
4
5
6
7
8
class MyThread implements Runnable{
@Override
public void run() {
//处理事务的具体逻辑
}
}
val myThread = MyThread()
new Thread(new MyThread()).start();//启动

Thread 的构造函数接收一个 Runnable 参数,而我们 new 出的 MyThread 正是一个实现了 Runnable 接口的对象,所以可以直接将它传入到 Thread 的构造函数里。接着调用 Threadstart() 方法,run() 方法中的代码就会在子线程当中运行了。

使用匿名类的方式

1
2
3
4
5
6
7
8
new Thread(new Runnable() {

@Override
public void run() {
// 处理具体的逻辑
}

}).start();

在子线程中更新 UI

想要更新应用程序里的 UI 元素,必须在主线程中进行,否则就会出现异常。

新建一个 AndroidThreadTest 项目,然后修改 activity_main.xml 中的代码,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/change_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Change Text" />

<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello world"
android:textSize="20sp" />

</RelativeLayout>

定义了两个控件:TextView 用于在屏幕的正中央显示一个 “Hello world” 字符串;

Button 用于改变 TextView 中显示的内容,我们希望在点击“Button”后可以把 TextView 中显示的字符串改成 “Nice to meet you”

MainActivity 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private TextView text;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text);
Button changeText = (Button) findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
text.setText("Nice to meet you");
}
}).start();
break;
default:
break;
}
}

}

我们在“Change Text”按钮的点击事件里面开启了一个子线程,然后在子线程中调用 TextView 的 setText()方法将显示的字符串改成 “Nice to meet you”。代码的逻辑非常
简单,只不过我们是在子线程中更新 UI 的。现在运行一下程序,并点击“Change Text”按钮,你会发现程序果然崩溃了。观察 Logcat 中的错误日志,可以看出是由于在子线程中更新 UI 所导致的

1
android.view.ViewRootImpl$CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.

由此证实了 Android 确实是不允许在子线程中进行 UI 操作的。但是有些时候,我们必须在子线程里去执行一些耗时任务,然后根据任务的执行结果来更新相应的 UI 控件,那这该怎么办呢?

对于这种情况,Android 提供了一套异步消息处理机制,完美地解决了在子线程中进行 UI 操作的问题。我们先来学习一下异步消息处理的使用方法。

修改 MainActivity 中的代码,如下所示:

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
public class MainActivity extends AppCompatActivity {

private static final int UPDATE_TEXT = 1;

private Handler handler = new Handler(){
public void handleMessage(Message msg){
switch (msg.what){
case UPDATE_TEXT:
TextView textView = findViewById(R.id.id_text);
textView.setText("aaaaaaaaaaa");
break;
default:
break;
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void ClickChangeUI(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
}

}

这里我们先是定义了一个整型常量 UPDATE_TEXT,用于表示更新 TextView 这个动作。然后新增一个 Handler 对象,并重写父类的 handleMessage() 方法,在这里对具体的 Message 进行处理。如果发现 Message 的 what 字段的值等于 UPDATE_TEXT,就更改 TextView 的内容。

接下来是按钮的点击事件中的代码。可以看到,这次我们并没有在子线程里直接进行 UI 操作,而是创建了一个 Messageandroid.os.Message)对象,并将它的 what 字段的值指定为 UPDATE_TEXT,然后调用 Handler 的 sendMessage() 方法将这条 Message 发送出去。很快,Handler 就会收到这条 Message,并在 handleMessage() 方法中对它进行处理。注意 此时 handleMessage() 方法中的代码就是在主线程当中运行的了,所以我们可以放心地在这里进行 UI 操作。

现在重新运行程序并点击按钮,发现程序并没有崩溃,而且 TextView 中的内容更改了。

现在可以说我们了解了 Android 异步消息处理的基本用法,但是我们并不了解它的工作原理,因此下面我们就来分析一下 Android 异步消息处理机制到底是如何工作的。

解析异步消息处理机制

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

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

了解了 Message、Handler、MessageQueue 以及 Looper 的基本概念后,我们再来把异步消息处理的整个流程梳理一遍。

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

一条 Message 经过这样一个流程的辗转调用后,也就从子线程进入到了主线程,从不能更新 UI 变成了可以更新 UI,整个异步消息处理的核心思想也就是如此。

使用 AsyncTask

不过为了更加方便我们在子线程中对 UI 进行操作,Android 还提供了另外一些好用的工具,比如 AsyncTask。借助 AsyncTask,即使你对异步消息处理机制完全不了解,也可以十分简单地从子线程切换到主线程。当然,AsyncTask 背后的实现原理也是基于异步消息处理机制的,只是 Android 帮我们做了很好的封装而已。

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

  • Params

    在执行 AsyncTask 时需要传入的参数,可用于在后台任务中使用。

  • Progress

    后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

  • Result

    当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的自定义 AsyncTask 就可以写成如下方式:

1
2
3
class DownloadTask extends AsyncTask<Void, Integer, Boolean>{
......
}

这里我们把 AsyncTask 的第一个泛型参数指定为 Void,表示在执行 AsyncTask 的时候不需要传入参数给后台任务。第二个泛型参数指定为 Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为 Boolean,则表示使用布尔型数据来反馈执行结果。

当然,目前我们自定义的 DownloadTask 还是一个空任务,并不能进行任何实际的操作,我们还需要去重写 AsyncTask 中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下 4 个:

  1. onPreExecute()
    这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
  2. doInBackground (Params…)
    这个方法中的所有代码都会在子线程中运行,我们应该 在这里去处理所有的耗时任务。任务一旦完成就可以通过 return 语句来将任务的执行结果返回,如果 AsyncTask 的第三个泛型参数指定的是 Void, 就可以不返回任务执行结果。注意,在这个方法中是 不可以进行 UI 操作的,如果需要更新 UI 元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress...) 方法来完成。
  3. onProgressUpdate(Progress…)
    当在后台任务中调用了 publishProgress(Progress...) 方法后,onProgressUpdate(Progress...) 方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中 可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
  4. onPostExecute(Result)
    当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

因此,一个比较完整的自定义 AsyncTask 就可以写成如下方式:

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
public class DownloadTask extends AsyncTask<Void, Integer, Boolean> {

protected void onPreExecute(){
//显示进度对话框
progressDialog.show();
}

@Override
protected Boolean doInBackground(Void... params) {
//任务处理
try {
while (true){
int downloadPercent = doDownload();//虚构方法
publishProgress(downloadPercent);
if (downloadPercent >= 100){
break;
}
}
}catch (Exception e){
e.printStackTrace();
return false;
}
return true;
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
//UI 跟新,即更新下载进度
progressDialog.setMessage("Downloaded " + values[0] + "%");
}

@Override
protected void onPostExecute(Boolean result) {//任务执行完后被调用
super.onPostExecute(result);
//关闭进度对话框
progressDialog.dismiss();
//提示下载结果
if (result){
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
}
}
}

在这个 DownloadTask 中,我们在 doInBackground() 方法里去执行具体的下载任务。这个方法里的代码都是在子线程中运行的,因而不会影响到主线程的运行。注意这里虚构了一个 doDownload() 方法,这个方法用于计算当前的下载进度并返回,我们假设这个方法已经存在了。在得到了当前的下载进度后,下面就该考虑如何把它显示到界面上了,由于 doInBackground() 方法是在子线程中运行的,在这里肯定不能进行 UI 操作,所以我们可以调用 publishProgress() 方法并将当前的下载进度传进来,这样 onProgressUpdate() 方法就会很快被调用,在这里就可以进行 UI 操作了。当下载完成后,doInBackground() 方法会返回一个布尔型变量,这样 onPostExecute() 方法就会很快被调用,这个方法也是在主线程中运行的。然后在这里我们会根据下载的结果来弹出相应的 Toast 提示,从而完成整个 DownloadTask 任务。

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

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

1
new DownloadTask().execute();

以上就是 AsyncTask 的基本用法。从上面这个例子中,可以看出,我们并不需要去考虑什么异步消息处理机制,也不需要专门使用一个 Handler 来发送和接收消息,只需要调用一下 publishProgress() 方法,就可以轻松地从子线程切换到 UI 线程了。

Service 的生命周期

  • **onCreate()**:当 Service 第一次被创建后立即回调该方法,该方法在整个生命周期中只会调用一次!
  • **onDestory()**:当 Service 被关闭时会回调该方法,该方法只会回调一次!
  • onStartCommand(intent, flag, startId)*:可多次调用 StartService 方法, 但不会再创建新的 Service 对象,而是继续复用前面产生的 Service 对象,* 但会继续回调 onStartCommand()方法
  • onbind(intent)*:该方法是 Service 都必须实现的方法,该方法会返回一个 IBinder 对象,* app 通过该对象与 Service 组件进行通信
  • onUnbind(intent)*:当该 Service 上绑定的* 所有客户端都断开 时会回调该方法!
  • **stopSelf()**:Service 自身调用,用于关闭服务。

两种启动方式

启动模式(Started Service)

当调用 startService() 方法启动 Service 时,它会以 启动模式 运行,适合执行单次任务,如播放音乐、下载文件等。在这种模式下,Service 会一直运行,直到手动调用 stopSelf()stopService() 方法将其停止。系统对同一个 Service 只会创建一个 Service 实例。如果 Service 实例不存在,调用 startService() 方法则会实例化一个 Service 对象,否则复用之前创建的 Service 实例。

  • 启动模式生命周期方法:

    1. **onCreate()**:

      • Service 第一次启动时调用。
      • Service 的整个生命周期内只调用一次。
      • 用于初始化资源,如创建线程、初始化服务所需的对象等。
    2. **onStartCommand(Intent intent, int flags, int startId)**:

      • 每次通过 startService() 启动 Service 时都会调用。

      • 处理启动服务的任务逻辑,适合执行持续或定时的后台任务。

      • 返回值

        决定了当系统杀掉 Service 时的行为:

        • **START_NOT_STICKY**:系统不会重启该 Service
        • **START_STICKY**:系统会尝试重新创建 Service,但不会重新传递 Intent。
        • **START_REDELIVER_INTENT**:系统会重新启动 Service 并重新传递最后一个 Intent。
    3. **onDestroy()**:

      • Service 被销毁时调用。
      • 用于清理资源、关闭线程或保存数据等,确保系统释放不再使用的资源。

绑定模式(Bound Service)

当调用 bindService()ServiceActivity 绑定时,它会以 绑定模式 运行,适合执行一些客户端和服务端交互的任务,例如获取位置服务、音频控制等。绑定模式的 Service 生命周期依赖于绑定的客户端数量,所有客户端解除绑定后 Service 会自动销毁。当 Service 只与一个客户端绑定时,调用 unbindService() 或者调用者退出,Service 会被销毁。当 Service 与多个客户端绑定时,只有与所有客户端取消绑定后,Service 才会被销毁。系统对同一个 Service 只会创建一个 Service 实例。如果 Service 实例不存在,调用 bindService 则会实例化一个 Service 对象,否则复用之前创建的 Service 实例。

  • 绑定模式生命周期方法

    1. **onCreate()**:
      • Service 第一次启动时调用,与启动模式相同。
      • 用于初始化 Service 所需的资源。
    2. **onBind(Intent intent)**:
      • 当客户端(如 Activity)调用 bindService() 绑定 Service 时调用。
      • 返回一个 IBinder 对象,用于客户端与 Service 交互。
      • 如果 Service 允许绑定多个客户端,则该方法可能会被多次调用。
    3. **onUnbind(Intent intent)**:
      • 当客户端调用 unbindService() 解除绑定时调用。
      • 当所有客户端都解绑时,Service 会自动销毁。
      • onUnbind() 返回 true,表示支持重新绑定,系统会在下次绑定时调用 onRebind()
    4. **onRebind(Intent intent)**:
      • 当已经调用了 onUnbind() 后,又有新的客户端绑定 Service 时调用。
      • 适用于需要重新执行某些逻辑的场景。
    5. **onDestroy()**:
      • Service 不再需要或所有客户端解除绑定后自动销毁。
      • 用于清理资源、关闭线程等操作。

3. 同时使用启动模式和绑定模式

在某些情况下,Service 可能需要同时支持启动和绑定模式。例如,音频播放服务可能既需要在用户按下播放按钮时启动,也需要在用户界面上绑定以更新播放状态。

  • 混合模式特点:
    • Service 只在 startService()bindService() 调用时创建。
    • Service 的生命周期会在最后一个客户端解绑或调用 stopService() 后终止。
    • 若在绑定模式下启动了 Service,但未使用 unbindService() 解绑或调用 stopService(),则 Service 会一直运行。

Service 的基本用法

创建一个服务

在项目中右键包名 → New → Service → Service,在弹出的对话框中,需要我们给服务命名,同时还有两个属性,Exported 属性表示是否允许除了当前程序之外的其他程序访问这个服务,Enabled 属性表示是否启用这个服务。将两个属性都勾中,点击 Finish 完成创建。这样创建的服务,会自动在 AndroidManifest.xml 中进行声明,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
......
>
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
......
</application>

</manifest>

Activity 与 Service 通信

Activity 与 Service 之间的交流媒介是 Service 中的 onBind() 方法,它带有 Intent 参数,并且返回值为 IBinder,也就是说,可以通过 IBinder 来传递数据。

实现步骤如下:

  1. 在自定义 Service 类中,自定义一个 Binder 类,将要暴露的方法写到该类中,同时实例化这个类。
  2. 然后重写 onBind()方法,将这个 Binder 对象返回。
  3. 在调用该 Service 的 Activity 中,实例化一个 ServiceConnection 对象,重写 onServiceConnected() 方法来获取 Binder 对象,之后调用相关方法即可!

简单的前台服务

通常情况下,Service 都是运行在后台的。但是 Service 的系统优先级比较低,如果系统内存不足,就有可能回收正在后台运行的 Service。

那如何解决上述情况呢?我们可以使用前台服务,从而让 Service 被杀死的可能性降低,所谓的前台服务就是状态栏显示的 Notification。

实现方法:在自定义的 Service 类中重写 onCreate(),根据自己的需求定制 Notification,最后调用 startForeground(id,notification) 即可。

评论