8月8日 第9章 后台默默的劳动者,探究Service

第二版是先学网络,后学服务,我们跟随第三版的进度,先学服务在学网络

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

Service的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,Service仍然能够保持正常运行。

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

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

1.在子线程中更新 UI

和许多其他的GUI库一样,Android的UI也是线程不安全的。也就是说,如果想要更新应用程序 里的UI元素,必须在主线程中进行,否则就会出现异常。

新建一个AndroidThreadTest项目,使用相对布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/changeTextBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Change Text" />
    <TextView
        android:id="@+id/textView"
        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:

package com.example.androidthreadtest;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        TextView textView = findViewById(R.id.textView);
        Button button = findViewById(R.id.changeTextBtn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("Nice to meet you");
                    }
                }).start();
            }
        });
    }
}

我们在 Change Text 按钮的点击事件里面开启了一个子线程,然后在子线程中调用 TextView 的 setText()方法将显示的字符串改成 Nice to meet you。代码的逻辑非常简单,只不过我们是在子线程中更新 UI 的。现在运行一下程序,并点击 Change Text 按钮,你会发现程序崩溃了

然后观察 logcat 中的错误日志,可以看出是由于在子线程中更新 UI 所导致的

由此证实了Android是不允许在子线程中进行UI操作的。但是有些时候,我们必须在子线程里执行一些耗时任务,然后根据任务的执行结果来更新相应的UI控件。 对于这种情况,Android提供了一套异步消息处理机制,完美地解决了在子线程中进行UI操作的问题。不过在下一小节讲

修改MainActivity中的代码

package com.example.androidthreadtest;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    public static final int UPDATE_TEXT = 1;

    private TextView textView;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == UPDATE_TEXT){
                textView.setText("Nice to meet you");
            }
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        textView = findViewById(R.id.textView);
        Button button = findViewById(R.id.changeTextBtn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(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 显示的内容改成 Nice to meet you。

Change Text 按钮的点击事件中的代码,没有在子线程里直接进行 UI 操作,而是创建了一个 Message(android.os.Message)对象,并将它的 what 字段的值指定为 UPDATE_TEXT,然后调用 Handler 的 sendMessage()方法将这条 Message 发送出去。Handler 就会收到这条 Message,并在 handleMessage()方法中对它进行处理。

此时 handleMessage()方法中的代码就是在主线程当中运行的了,所以可以在这里进行 UI 操作。接下来对 Message 携带的 what 字段的值进行判断,如果等于 UPDATE_TEXT, 就将 TextView 显示的内容改成 Nice to meet you;

1.异步消息处理机制

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

  1. Message

      Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间 传递数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1和 arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。

  2. Handler

      Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般 是使用Handler的sendMessage()方法、post()方法等,而发出的消息经过一系列地辗 转处理后,最终会传递到Handler的handleMessage()方法中。

  3. MessageQueue

      MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部 分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

  4. Looper

      Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入一个无限循环当中,然后每当发现MessageQueue中存在一条消息时,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

首先在主线程当中创建一个Handler对象,并重写 handleMessage()方法。

然后当子线程中需要进行UI操作时,就创建一个Message对象,

Handler将这条消息发送出去。这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,

最后分发回Handler的 handleMessage()方法中。由于Handler的构造函数中我们传入了 Looper.getMainLooper(),所以此时handleMessage()方法中的代码也会在主线程中运 行,于是我们在这里就可以安心地进行UI操作了。

2.使用AsyncTask

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

AsyncTask的基本用法:

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

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

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

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

因此可以写成这种形式

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操作,比如说提醒任务执行的结果,以及关闭进度条对话框等。

class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
    @Override
    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) {
            return false;
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 在这里更新下载进度
        progressDialog.setMessage("Downloaded " + values[0] + "%");
    }

    @Override
    protected void onPostExecute(Boolean 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()方法中执行一些任务的收尾工作。 如果想要启动这个任务,只需编写以下代码即可

new DownloadTask().execute();

2.Service的基本用法

1.定义一个Service

首先看一下如何在项目中定义一个Service。新建一个ServiceTest项目,然后右击 com.example.servicetest→New→Service→Service

这里我们将类名定义成MyService,Exported属性表示是否将这个Service暴露给外部其他程序访问,Enabled属性表示是否启用这个Service。将两个属性都勾中,点击“Finish”完成创建

MyService 中的代码;

package com.example.servicetest;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

可以看到,MyService是继承自系统的Service类的。目前MyService中有一个onBind()方法特别醒目。这个方法是Service中唯一的抽象方法,所以必须在子类里实现。暂时先不管它。

@Override
public void onCreate() {
    super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
    super.onDestroy();
}

我们重写了onCreate()、onStartCommand()和onDestroy()这3个方法,它们是每个Service中最常用到的3个方法了。

onCreate()方法会在Service创建的时候调用,

onStartCommand()方法会在每次Service启动的时候调用,

onDestroy()方法会在Service销毁的时候调用。

如果我们希望Service一旦启动就立刻去执行某个动作,就可以将逻辑写在 onStartCommand()方法里。

而当Service销毁时,我们又应该在onDestroy()方法中回收那些不再使用的资源。

另外,每一个Service都需要在AndroidManifest.xml文件中进行注册才能生效。这是Android四大组件共有的特点。不过 Android Studio自动帮完成了注册

AndroidManifest.xml:

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

2.启动和停止Service

修改activity_main.xml

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="启动"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="停止"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button" />

加入了两个按钮,用来启动和停止Service

修改MainActivity中的代码

Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MainActivity.this, MyService.class);
        startService(intent);//启动服务
    }
});
Button button1 = findViewById(R.id.button2);
button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(MainActivity.this, MyService.class);
        stopService(intent);//停止服务
    }
});

我们构建出了一个 Intent 对象,并调用 startService()方法来启动 MyService 这个服务。

构建出了一个 Intent 对象,并调用 stopService()方法来停止 MyService 这个服务。

startService()和 stopService()方法都是定义在 Context 类中的,所以我们 活动里可以直接调用这两个方法。

这里完全是由活动来决定服务何时停止的,如果没有点击停止按钮,服务就会一直处于运行状态。

Service也可以自我停止运行,只需要在Service内部调用stopSelf()方法即可。

我们在MyService中添加log指令打印日志来证实服务被启动停止

@Override
public void onCreate() {
    Log.d("MyService", "onCreate: MyService");
    super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d("MyService", "onStartCommand: MyService");
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
    Log.d("MyService", "onDestroy: MyService");
    super.onDestroy();
}

其中onCreate方法只有在服务创建时进行

onStartCommand方法则是每次服务被打开都会进行

3.Activity和Service进行通信

以上我们的活动只是通知了一下服务可以启动了,但活动不能指挥服务该干什么

接下来是刚刚忽略的onBinder方法

比如,我们希望在MyService里提供一个下载功能,然后在Activity中可以决定什么时候开始下载,以及查看下载进度。实现这个功能的思路是创建一个专门的Binder对象来对下载功能进行管理

修改MyService中的代码

private DownloadBinder mBinder = new DownloadBinder();

@Override
public IBinder onBind(Intent intent) {
    return mBinder;
}
    
class DownloadBinder extends Binder {
    public void startDownload(){
        Log.d("MyService", "startDownload");
    }
    public void getProgress(){
        Log.d("MyService", "getProgress");
    }
}

我们新建了一个 DownloadBinder 类,并让它继承自 Binder,然后在它的内部提供了开始下载以及查看下载进度的方法。在这两个方法中分别打印了一行日志。

在 MyService 中创建了 DownloadBinder 的实例,然后在 onBind()方法里返回了这个实例

看一看Activity如何调用Service里的这些方法了。

首先需要在布局文件里新增两个按钮,修改activity_main.xml中的代码:

<Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="绑定服务"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button2" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="取消绑定"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button3" />

这两个按钮分别是用于绑定和取消绑定Service的,那到底谁需要和Service绑定呢?当然就是 Activity了。当一个Activity和Service绑定了之后,就可以调用该Service里的Binder提供的 方法了

private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        downloadBinder = (MyService.DownloadBinder) service;
        downloadBinder.startDownload();
        downloadBinder.getProgress();
    }
};

//onCreate方法内
Button button2 = findViewById(R.id.button3);
button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent bindIntent = new Intent(MainActivity.this, MyService.class);
        bindService(bindIntent,connection,BIND_AUTO_CREATE);//绑定服务
    }
});
Button button3 = findViewById(R.id.button4);
button3.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        unbindService(connection);//解绑
    }
});

这里我们首先创建了一个 ServiceConnection 的匿名类,在里面重写了 onServiceConnected()方法和 onServiceDisconnected()方法,这两个方法分别会在活动与服务成功绑定以及活动与服务的连接断开的时候调用。

在 onServiceConnected()方法中,我们又通过向下转型得到了 DownloadBinder 的实例,这样活动和服务之间的关系就变得非常紧密了。

现在我们可以在活动中根据具体的场景来调用 DownloadBinder 中的任何 public 方法,即指挥服务干什么服务就去干什么的功能。

这里仍然只是做了个简单的测试,在 onServiceConnected()方法中调用了 DownloadBinder 的 startDownload()和 getProgress()方法

现在活动和服务还没进行绑定,这个功能是在 Bind Service 按钮的点击事件里完成的。我们仍然是构建出了一个 Intent 对象,然后调用 bindService()方法将 MainActivity 和 MyService 进行绑定。

bindService()方法接收 3 个参数,第一个参数就是刚刚构建出的 Intent 对象,

第二个参数是前面创建出的 ServiceConnection 的实例,

第三个参数则是一个标志位,这里传入 BIND_AUTO_CREATE 表示在活动和服务进行绑定后自动创建服务。这会使得 MyService 中的 onCreate()方法得到执行,但 onStartCommand()方法不会执行。

点击一下绑定服务按钮

任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和 MainActivity绑定,还可以和任何一个其他的Activity进行绑定,而且在绑定完成后,它们都可以获取相同的DownloadBinder实例

3.Service的生命周期

之前我们学习过了Activity以及Fragment的生命周期。类似地,Service也有自己的生命周期,前面我们使用到的onCreate()、onStartCommand()、onBind()和onDestroy()等方法都是在Service的生命周期内可能回调的方法。

一旦在项目的任何位置调用了Context的startService()方法,相应的Service就会启动, 并回调onStartCommand()方法。如果这个Service之前还没有创建过,onCreate()方法会先于onStartCommand()方法执行。Service启动了之后会一直保持运行状态,直到 stopService()或stopSelf()方法被调用,或者被系统回收。

注意,虽然每调用一次 startService()方法,onStartCommand()就会执行一次,但实际上每个Service只会存在 一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService() 或stopSelf()方法,Service就会停止。

还可以调用Context的bindService()来获取一个Service的持久连接,这时就会回调 Service中的onBind()方法。

如果这个Service之前还没有创建过,onCreate()方法会先于onBind()方法执行。之后,调用方可以获取到onBind()方法里返回的IBinder对象的实例,这样就能自由地和Service进行通信了。只要调用方和Service之间的连接没有断开, Service就会一直保持运行状态,直到被系统回收。

当调用了startService()方法后,再去调用stopService()方法。这时Service中的 onDestroy()方法就会执行,表示Service已经销毁了。

当调用了bindService() 方法后,再去调用unbindService()方法,onDestroy()方法也会执行,这两种情况都很理解。

我们可能对一个Service既调用了startService()方法,又调用了bindService()方法的

如果想要销毁Service,我们必须让两种条件同时不满足:启动和绑定

一个Service只要被启动被绑定了之后,就会处于运行状态。

这种情况下要同时调用stopService()和 unbindService()方法,onDestroy()方法才会执行。

4.其他

1.使用前台Service

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

由于状态栏中一直有一个正在运行的图标,相当于我们的应用以另外一种形式保持在前台可见 状态,所以系统不会倾向于回收前台Service。另外,用户也可以通过下拉状态栏清楚地知道当 前什么应用正在运行,因此也不存在某些恶意应用长期在后台偷偷占用手机资源的情况。

修改MyService中的代码

@SuppressLint("ForegroundServiceType")
@Override
public void onCreate() {
    super.onCreate();
    Log.d("MyService", "onCreate: MyService");
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
        NotificationChannel channel = new NotificationChannel("my_service","前台服务通知",NotificationManager.IMPORTANCE_DEFAULT);
        manager.createNotificationChannel(channel);
    }
    Intent intent = new Intent(this, MainActivity.class);
    PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
    Notification notification = new NotificationCompat.Builder(this,"my_service")
            .setContentTitle("前台服务运行中")
            .setContentText("服务运行中")
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.mipmap.ic_launcher)
            .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                    R.mipmap.ic_launcher))
            .setContentIntent(pi)
            .build();
    startForeground(1, notification);
}

里只是修改了onCreate()方法中的代码,这是我们在第8章中学习的创建通知的方法,只不过这次在构建Notification对象后并没有使用NotificationManager将通知显示出来,而是调用了startForeground()方法。

这个方法接收两个参数:第一个参数是通知的id,类似于notify()方法的第一个参数;第二个参数则是构建的Notification对象。调用startForeground()方法后就会让MyService变成一个前台Service,并在系统状态栏显示出来。

另外,从Android 9.0系统开始,使用前台Service必须在AndroidManifest.xml文件中进行权限声明才行

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

2.使用IntentService

服务中的代码都是默认运行在主线程当中的,如果直接在服务里去处理一些耗时的逻辑,就很容易出现 ANR(Application Not Responding) 的情况

这个时候需要用到Android多线程编程的技术,应该在Service的每个具体的方法里开启一个子线程,然后在这里处理那些耗时的逻辑。一个比较标准的Service就可以写成

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 处理具体的逻辑
        }
    }).start();
    return super.onStartCommand(intent, flags, startId);
}

这种Service一旦启动,就会一直处于运行状态,必须调用stopService()或 stopSelf()方法,或者被系统回收,Service才会停止。所以,如果想要实现让一个Service 在执行完毕后自动停止的功能,就可以这样写

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 处理具体的逻辑
            stopSelf();
        }
    }).start();
    return super.onStartCommand(intent, flags, startId);
}

虽说这种写法并不复杂,但是总会有一些程序员忘记开启线程,或者忘记调用stopSelf()方法。

为了可以简单地创建一个异步的、会自动停止的Service,Android专门提供了一个 IntentService类,这个类就很好地解决了前面所提到的两种尴尬,下面我们就来看一下它的用法。

新建一个MyIntentService类继承自IntentService

package com.example.servicetest;

import android.app.IntentService;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;

public class MyIntentService extends IntentService {
    public MyIntentService() {
        super("MyIntentService"); // 调用父类的有参构造函数
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        // 打印当前线程的 id 
        Log.d("MyIntentService", "Thread id is " + Thread.currentThread(). getId());
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyIntentService", "onDestroy executed");
    }
}

这里要提供一个无参构造函数,并且必须在其内部调用父类的有参构造函数。

然后在子类中去实现 onHandleIntent()这个抽象方法,在这个方法中去处理一些具体的逻辑, 不用担心 ANR 的问题,这个方法是在子线程中运行的。

这里为了证实一下,我们在 onHandleIntent()方法中打印了当前线程的 id。另外根据 IntentService 的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止了

接下来修改activity_main.xml中的代码,加入一个用于启动MyIntentService的按钮

<Button
    android:id="@+id/button5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="开始Intent服务"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

然后修改MainActivity中的代码

Button button4 = findViewById(R.id.button5);
button4.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // 打印主线程的 id
        Log.d("MainActivity", "Thread id is " + Thread.currentThread().
                getId());
        Intent intentService = new Intent(MainActivity.this, MyIntentService.class);
        startService(intentService);
    }
});

我们在“Start IntentService”按钮的点击事件里启动了MyIntentService,并在这里打印了一下主线程名,稍后用于和IntentService进行比对,其实IntentService 的启动方式和普通的Service没什么两样。

最后不要忘记,Service都是需要在AndroidManifest.xml里注册的

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

 

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值