Service知识点整理

Posted 折花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Service知识点整理相关的知识,希望对你有一定的参考价值。

Service简单概述

Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的应用组件。服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity)已销毁也不受影响。此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信(IPC)。例如,服务可以处理网络事物、播放音乐、执行文件I/O或与内容提供程序交互,而这一切均可在后台进行。

Service基本上分为两种形式:

  • 启动
    当应用组件(如Activity)通过调用startService()启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响。已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。例如,它可能通过网络下载或上传文件。操作完成后,服务会自动停止运行。

  • 绑定
    当应用组件通过调用bindService()绑定到服务时,服务即处于“绑定”状态。绑定的服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用线程间通信(IPC)跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件也可以同时绑定到该服务,单全部取消绑定后,该服务即会被销毁。

无论应用处于启动状态还是绑定状态,抑或处于启动并且绑定状态,任何应用组件均可像使用Activity那样通过调用Intent来使用服务(即使此服务来自另外一个应用)。不过,可以通过清单文件将服务声明为私有服务,并阻止其他应用访问。

基础知识

通过创建Service子类(或使用它的一个现有子类)来创建服务。在现实中,需要重写一些回调方法,已处理服务生命周期的某些关键方面并提供一种机制将组件绑定到服务。

onStartCommand()

当另一个组件(如Activity)通过调用startService()请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。在服务工作完成后,需要通过调用stopSelf()stopService()来停止服务。(如果只提供绑定服务,则无需实现此方法)。

onBind()

当另一个组件香通过调用bindService()与服务绑定(例如执行RPC)时,系统将调用此方法。在此方法的现实中,您必须通过返回IBinder提供一个接口,提供客户端用来与服务进行通信。请务必实现此方法,但如果并不希望允许绑定,则应返回null。

onCreate()

首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用onStartCommand()onBind()之前)。如果服务已经在运行,则不会调用此方法。

onDestroy()

当服务不在使用且当被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的监听器、接收器等。这是服务接收的最后一个调用。

使用清单文件声明服务

如同Activity(以及其他组件)一样,必须在应用的清单文件中声明所有服务。
要声明服务,请添加元素作为元素的子元素。例如:

<manifest ...>
    ...
    <application ...>
        <service android:name=".ExampleService"
            android:enabled=["true" | "false"]
            android:exported=["true" | "false"]
            android:isolatedProcess=["true" | "false"]
            android:icon="drawable resource"
            android:lable="string resource"
            android:name="string"
            android:permission="string"
            android:process="string">
            ...
        </service>
    </application>
</manifest>
  • exported:
    代表是否能被其他应用隐式调用,其默认值时由service中有无intent-filter决定的,如果有, 默认值为true,否则为false。为false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。

  • name:
    对应Service类名

  • permission:
    是权限声明

  • process:
    是否需要单独在进程中运行,当设置为android:process=” :remote”时,代表Service在单独的进程中运行。注意“:”很重要,它的意思是只要在当前进程名称前附加上当前的包名,所以“remote” 和”:remote”不是一个意思,前者的进程名称为:remote,而后者的进程名称为:App-pakageName:remote。

  • isolatedProcess:
    设置true意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。与其通信的唯一途径是通过服务的API(bind and start)。

  • enabled:
    是否可以被系统实例化,默认为true因为父标签已有enable属性,所以必须两个都为默认值true的情况下才会被激活,否则不会激活。

启动服务

Intent intent = new Intent(this, TestService.class);
startService(intent);

onStartCommand(Intent intent, int flag, int startId)

  • intent:
    启动时,启动组件传递过来的Intent,如Activity可利用Intent封装所需要的参数并传递给Service

  • flags:
    表示启动请求时是否有额外数据,可选值有 0,START_FLAG_REDELIVERY,START_FLAG_RETRY,0代表没有。

    • START_FLAG_REDELIVERY
      这个值代表了onStartCommand方法的返回值为
      START_REDELIVER_INTENT,而且在上一次服务被杀死前会去调用stopSelf方法停止服务。其中START_REDELIVER_INTENT意味着当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),此时Intent时有值的。
    • START_FLAG_RETRY
      该flag代表当onStartCommand调用后一直没有返回值时,会尝试重新去调用onStartCommand()。
  • startId:
    指明当前服务的唯一ID,与stopSelfResult (int startId)配合使用,stopSelfResult 可以更安全地根据ID停止服务。

实际上onStartCommand的返回值int类型才是最最值得注意的,它有三种可选值,START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT,它们具体含义如下:

  • START_STICKY
    当Service因内存不足而被系统kill后,一段时间后内存再次空闲时,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand方法,但其中的Intent将是null,除非有挂起的Intent,如pendingintent,这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务。

  • START_NOT_STICKY
    当Service因内存不足而被系统kill后,即使系统内存再次空闲时,系统也不会尝试重新创建此Service。除非程序中再次调用startService启动此Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。

  • START_REDELIVER_INTENT
    当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),任何挂起 Intent均依次传递。与START_STICKY不同的是,其中的传递的Intent将是非空,是最后一次调用startService中的intent。这个值适用于主动执行应该立即恢复的作业(例如下载文件)的服务。

由于每次启动服务(调用startService)时,onStartCommand方法都会被调用,因此我们可以通过该方法使用Intent给Service传递所需要的参数,然后在onStartCommand方法中处理的事件,最后根据需求选择不同的Flag返回值,以达到对程序更友好的控制。

Service绑定服务

绑定服务是 Service 类的实现,可让其他应用与其绑定和交互。要提供服务绑定,您必须实现 onBind() 回调方法。该方法返回的 IBinder 对象定义了客户端用来与服务进行交互的编程接口。

绑定到已启动服务
正如服务文档中所述,您可以创建同时具有已启动和绑定两种状态的服务。 也就是说,可通过调用 startService() 启动该服务,让服务无限期运行;此外,还可通过调用 bindService() 使客户端绑定到服务。

如果您确实允许服务同时具有已启动和绑定状态,则服务启动后,系统“不会”在所有客户端都取消绑定时销毁服务。 为此,您必须通过调用 stopSelf() 或 stopService() 显式停止服务。

尽管您通常应该实现 onBind() 或 onStartCommand(),但有时需要同时实现这两者。例如,音乐播放器可能发现让其服务无限期运行并同时提供绑定很有用处。 这样一来,Activity 便可启动服务进行音乐播放,即使用户离开应用,音乐播放也不会停止。 然后,当用户返回应用时,Activity 可绑定到服务,重新获得回放控制权。

请务必阅读管理绑定服务的生命周期部分,详细了解有关为已启动服务添加绑定时该服务的生命周期信息。
客户端可通过调用 bindService() 绑定到服务。调用时,它必须提供 ServiceConnection 的实现,后者会监控与服务的连接。bindService() 方法会立即无值返回,但当 Android 系统创建客户端与服务之间的连接时,会对 ServiceConnection 调用 onServiceConnected(),向客户端传递用来与服务通信的 IBinder。

多个客户端可同时连接到一个服务。不过,只有在第一个客户端绑定时,系统才会调用服务的 onBind() 方法来检索 IBinder。系统随后无需再次调用 onBind(),便可将同一 IBinder 传递至任何其他绑定的客户端。

当最后一个客户端取消与服务的绑定时,系统会将服务销毁(除非 startService() 也启动了该服务)。

实际上我们必须提供一个 IBinder接口的实现类,该类用以提供客户端用来与服务进行交互的编程接口,该接口可以通过三种方法定义接口:

  • 扩展Binder类
    如果服务是提供给自有应用专用的,并且Service(服务端)与客户端相同的进程中运行(常见情况),则应通过扩展 Binder 类并从 onBind() 返回它的一个实例来创建接口。客户端收到 Binder 后,可利用它直接访问 Binder 实现中以及Service 中可用的公共方法。如果我们的服务只是自有应用的后台工作线程,则优先采用这种方法。 不采用该方式创建接口的唯一原因是,服务被其他应用或不同的进程调用。

  • 使用Messenger
    Messenger可以翻译为信使,通过它可以在不同的进程中共传递Message对象(Handler中的Messager,因此 Handler 是 Messenger 的基础),在Message中可以存放我们需要传递的数据,然后在进程间传递。如果需要让接口跨不同的进程工作,则可使用 Messenger 为服务创建接口,客户端就可利用 Message 对象向服务发送命令。同时客户端也可定义自有 Messenger,以便服务回传消息。这是执行进程间通信 (IPC) 的最简单方法,因为 Messenger 会在单一线程中创建包含所有请求的队列,也就是说Messenger是以串行的方式处理客户端发来的消息,这样我们就不必对服务进行线程安全设计了。

  • 使用AIDL
    AIDL(Android 接口定义语言)执行所有将对象分解成原语的工作,操作系统可以识别这些原语并将它们编组到各进程中,以执行 IPC。 之前采用 Messenger 的方法实际上是以 AIDL 作为其底层结构。 如上所述,Messenger 会在单一线程中创建包含所有客户端请求的队列,以便服务一次接收一个请求。 不过,如果您想让服务同时处理多个请求,则可直接使用 AIDL。 在此情况下,您的服务必须具备多线程处理能力,并采用线程安全式设计。
    如需直接使用 AIDL,您必须创建一个定义编程接口的 .aidl 文件。Android SDK 工具利用该文件生成一个实现接口并处理 IPC 的抽象类,您随后可在服务内对其进行扩展。

扩展Binder类

前面描述过,如果我们的服务仅供本地应用使用,不需要跨进程工作,则可以实现自有 Binder 类,让客户端通过该类直接访问服务中的公共方法。其使用开发步骤如下

  • 1,创建BindService服务端,继承自Service并在类中,创建一个实现IBinder 接口的实例对象并提供公共方法给客户端调用

  • 2,从 onBind() 回调方法返回此 Binder 实例。

  • 3,在客户端中,从 onServiceConnected() 回调方法接收 Binder,并使用提供的方法调用绑定服务。

注意: 此方式只有在客户端和服务位于同一应用和进程内才有效,如对于需要将 Activity 绑定到在后台播放音乐的自有服务的音乐应用,此方式非常有效。另一点之所以要求服务和客户端必须在同一应用内,是为了便于客户端转换返回的对象和正确调用其 API。服务和客户端还必须在同一进程内,因为此方式不执行任何跨进程编组。

扩展Binder类实现的Service端

private LocalBinder binder = new LocalBinder();

/**
*创建Binder对象,返回给客户端即Activity使用,提供数据交换接口
*/
public class LocalBinder extends Binder{
    //声明当前对象this
    return LocalService.this;
}

/**
*把Binder类返回给客户端
*/
@Nullable
@Override
public IBinder onBind(Intent intent){
    return binder;
}

扩展Binder类客户端实现

/**
*ServiceConnection代表与服务的链接,它只有两个方法,
*onServiceConnected和onServiceDisconnected,
*前者是在操作者的链接一个服务成功时被调用,而后者是在服务崩溃或者被杀死导致的连接中断时被调用
*/
private ServiceConnection conn;
private LocalService mService;

conn = new ServiceConnection(){

    @Override
    public void onServiceConnected(ComponentName name, IBinder binder){
        //获取Binder
        LocalService.LocalBinder binder = (LocalService.LocalBinder) binder;
        mService= binder.getService();
    }

    @Override
    public void onServiceDisconnected(ComponentName name){
        mService= null;
    }
}

//开启绑定
Intent intent = new Intent(this, LocalService.class);
bindService(intent, conn, Service.BIND_AUTO_CREATE);

使用Messenger

如需让服务与远程进程通信,则可使用 Messenger 为您的服务提供接口。利用此方法,您无需使用 AIDL 便可执行进程间通信 (IPC)。

以下是Messenger的使用方法摘要:

  • 服务实现一个 Handler,由其接收来自客户端的每个调用的回调
  • Handler 用于创建 Messenger 对象(对 Handler 的引用)
  • Messenger 创建一个 IBinder,服务通过 onBind() 使其返回客户端
  • 客户端使用 IBinder 将 Messenger(引用服务的 Handler)实例化,然后使用后者将 Message 对象发送给服务
  • 服务在其 Handler 中(具体地讲,是在 handleMessage() 方法中)接收每个 Message。

这样,客户端并没有调用服务的“方法”。而客户端传递的“消息”(Messenger对象)是服务在其Handler中接收的。

    /** Command to the service to display a message */
    static final int MSG_SAY_HELLO = 1;

    /**
     * Handler of incoming messages from clients.
     */
    class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SAY_HELLO:
                    Toast.makeText(getApplicationContext(), "hello!", Toast.LENGTH_SHORT).show();
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
    /**
     * Target we publish for clients to send messages to IncomingHandler.
     */
    final Messenger mMessenger = new Messenger(new IncomingHandler());

    /**
     * When binding to the service, we return an interface to our messenger
     * for sending messages to the service.
     */
    @Override
    public IBinder onBind(Intent intent) {
        Toast.makeText(getApplicationContext(), "binding", Toast.LENGTH_SHORT).show();
        return mMessenger.getBinder();
    }

请注意,服务就是在 Handler 的 handleMessage() 方法中接收传入的 Message,并根据 what 成员决定下一步操作。

客户端只需根据服务返回的 IBinder 创建一个 Messenger,然后利用 send() 发送一条消息。例如,以下就是一个绑定到服务并向服务传递 MSG_SAY_HELLO 消息的简单 Activity:

    /** Messenger for communicating with the service. */
    Messenger mService = null;
    /** Flag indicating whether we have called bind on the service. */
    boolean mBound;
    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the object we can use to
            // interact with the service.  We are communicating with the
            // service using a Messenger, so here we get a client-side
            // representation of that from the raw IBinder object.
            mService = new Messenger(service);
            mBound = true;
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mBound = false;
        }
    };

public void sayHello(View v) {
        if (!mBound) return;
        // Create and send a message to the service, using a supported 'what' value
        Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
        try {
            mService.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

此示例并未说明服务如何对客户端作出响应。如果您想让服务作出响应,则还需要在客户端中创建一个 Messenger。然后,当客户端收到 onServiceConnected() 回调时,会向服务发送一条 Message,并在其 send() 方法的 replyTo 参数中包含客户端的 Messenger。

Messenger方式进行进程间通信的原理图:

Messenger进程间通信流程图

管理绑定服务的声明周期

当服务与所有客户端之间的绑定全部取消时,Android系统便会销毁服务(除非还使用onStartCommand()启动了该服务)。因此,如果是纯粹的绑定服务,则无需对其生命周期进行管理——Android系统会根据它是否绑定到任何客户端代管理。

如果选择实现onStartCommand()回调方法,则必须显式停止五福,因为系统现在已将服务视为已启动。在此情况下,服务将一直运行到其通过stopSelf()自行停止,或其他组件调用stopService()为止,无论其是否绑定到任何客户端。

此外,如果服务启动并接受绑定,当系统调用onUnbind()方法时,如果在客户端下一次绑定到服务时接收onRebind()调用,则可选择返回true。onRebind()返回空值,但客户端仍在其onServiceConnected()回调中接收IBinder。

绑定服务生命周期的逻辑图:
绑定服务生命周期逻辑图

关于启动服务与绑定服务间的转换问题

虽然服务的状态有启动和绑定两种,但实际上一个服务可以同时是这两种状态,也就是说,它既可以是启动服务(以无限期运行),也可以是绑定服务。有点需要注意的是Android系统仅会为一个Service创建一个实例对象,所以不管是启动服务还是绑定服务,操作的是同一个Service实例,而且由于绑定服务或者启动服务执行顺序问题将会出现以下两种情况:

  • 先绑定服务后启动服务
    如果当前Service实例先以绑定状态运行,然后再以启动状态运行,那么绑定服务将会转为启动服务运行,这时如果之前绑定的宿主(Activity)被销毁了,也不会影响服务的运行,服务还是会一直运行下去,指定收到调用停止服务或者内存不足时才会销毁该服务。

  • 先启动服务后绑定服务
    如果当前Service实例先以启动状态运行,然后再以绑定状态运行,当前启动服务并不会转为绑定服务,但是还是会与宿主绑定,只是即使宿主解除绑定后,服务依然按启动服务的生命周期在后台运行,直到有Context调用了stopService()或是服务本身调用了stopSelf()方法抑或内存不足时才会销毁服务。

以上两种情况显示出启动服务的优先级确实比绑定服务高一些。不过无论Service是处于启动状态还是绑定状态,或处于启动并且绑定状态,我们都可以像使用Activity那样通过调用 Intent 来使用服务(即使此服务来自另一应用)。 当然,我们也可以通过清单文件将服务声明为私有服务,阻止其他应用访问。最后这里有点需要特殊说明一下的,由于服务在其托管进程的主线程中运行(UI线程),它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。 这意味着,如果服务将执行任何耗时事件或阻止性操作(例如 MP3 播放或联网)时,则应在服务内创建新线程来完成这项工作,简而言之,耗时操作应该另起线程执行。只有通过使用单独的线程,才可以降低发生“应用无响应”(ANR) 错误的风险,这样应用的主线程才能专注于用户与 Activity 之间的交互, 以达到更好的用户体验。

管理服务生命周期

管理服务生命周期流程图

服务的整个生命周期从调用 onCreate() 开始起,到 onDestroy() 返回时结束。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。
  无论服务是通过 startService() 还是 bindService() 创建,都会为所有服务调用 onCreate() 和 onDestroy() 方法。
  服务的有效生命周期从调用 onStartCommand() 或 onBind() 方法开始。每种方法均有 Intent 对象,该对象分别传递到 startService() 或 bindService()。
  对于启动服务,有效生命周期与整个生命周期同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,有效生命周期在 onUnbind() 返回时结束。

从执行流程图来看,服务的生命周期比 Activity 的生命周期要简单得多。但是,我们必须密切关注如何创建和销毁服务,因为服务可以在用户没有意识到的情况下运行于后台。管理服务的生命周期(从创建到销毁)有以下两种情况:

  • 启动服务
    该服务在其他组件调用 startService() 时创建,然后无限期运行,且必须通过调用 stopSelf() 来自行停止运行。此外,其他组件也可以通过调用 stopService() 来停止服务。服务停止后,系统会将其销毁。

  • 绑定服务
    该服务在另一个组件(客户端)调用 bindService() 时创建。然后,客户端通过 IBinder 接口与服务进行通信。客户端可以通过调用 unbindService() 关闭连接。多个客户端可以绑定到相同服务,而且当所有绑定全部取消后,系统即会销毁该服务。 (服务不必自行停止运行)

虽然可以通过以上两种情况管理服务的生命周期,但是我们还必须考虑另外一种情况,也就是启动服务与绑定服务的结合体,也就是说,我们可以绑定到已经使用 startService() 启动的服务。例如,可以通过使用 Intent(标识要播放的音乐)调用 startService() 来启动后台音乐服务。随后,可能在用户需要稍加控制播放器或获取有关当前播放歌曲的信息时,Activity 可以通过调用 bindService() 绑定到服务。在这种情况下,除非所有客户端均取消绑定,否则 stopService() 或 stopSelf() 不会真正停止服务。因此在这种情况下我们需要特别注意。

Android 5.0以上的隐式启动问题

既然有隐式启动,那么就会有显示启动,那就先来了解一下什么是隐式启动和显示启动。

  • 显式启动
//显式启动
Intent intent = new Intent(this, ForegroundService.class);
startService(intent);
  • 隐式启动
    需要设置一个Action,我们可以把Action的名字设置成Service的全路径名字,在这种情况下android:exported默认为true。
final Intent serviceIntent = new Intent();
serviceIntent.setAction("com.android.ForegroundService");
startService(serviceIntent);
  • 存在的意义
    如果在同一个应用中,两者都可以用。在不同应用时,只能用隐式启动。

  • Android 5.0以上的隐式启动问题
     Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service。如果使用隐式启动Service,会出没有指明Intent的错误
    5.0以后隐式启动的报错

主要原因我们可以从源码中找到,这里看看Android 4.4的ContextImpl源码中的validateServiceIntent(Intent service),可知如果启动service的intent的component和package都为空并且版本大于KITKAT的时候只是报出一个警报,告诉开发者隐式声明intent去启动Service是不安全的.

而在android5.0之后呢?我们这里看的是android6.0的源码如下

从源码可以看出如果启动service的intent的component和package都为空并且版本大于LOLLIPOP(5.0)的时候,直接抛出异常,该异常与之前隐式启动所报的异常时一致的。那么该如何解决呢?

  • 解决方式

    • 设置Action和packageName

      final Intent serviceIntent = new Intent();
      serviceIntent.setAction("com.android.ForegroundService);
      serviceIntent.setPackage(getPackageName());//设置应用包名
      startService(serviceIntent);
      
    • 将隐式启动转为显式启动

      public static Intent getExplicitIntent(Context context, Intent implicitIntent) {
          // Retrieve all services that can match the given intent
           PackageManager pm = context.getPackageManager();
           List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
           // Make sure only one match was found
           if (resolveInfo == null || resolveInfo.size() != 1) {
               return null;
           }
           // Get component info and create ComponentName
           ResolveInfo serviceInfo = resolveInfo.get(0);
           String packageName = serviceInfo.serviceInfo.packageName;
           String className = serviceInfo.serviceInfo.name;
           ComponentName component = new ComponentName(packageName, className);
           // Create a new intent. Use the old one for extras and such reuse
           Intent explicitIntent = new Intent(implicitIntent);
           // Set the component to be explicit
           explicitIntent.setComponent(component);
           return explicitIntent;
          }

    调用方式如下:

    Intent mIntent=new Intent();//辅助Intent
    mIntent.setAction("com.android.ForegroundService");
    final Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent));
    startService(serviceIntent);

以上是关于Service知识点整理的主要内容,如果未能解决你的问题,请参考以下文章

Service Worker基础知识整理

IOS开发-OC学习-常用功能代码片段整理

VS2015 代码片段整理

What's the difference between @Component, @Repository & @Service annotations in Spring?(代码片段

小程序各种功能代码片段整理---持续更新

常用python日期日志获取内容循环的代码片段