Android 后台服务 JobIntentService 使用详解

Android 8.0(API 26) 对系统资源的管控更加严格,添加了后台限制规则。

如果满足以下任意条件,应用将被视为处于前台:

  • 具有可见 Activity(不管该 Activity 已启动还是已暂停);
  • 具有前台服务;
  • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序);
  • IME;
  • 壁纸服务;
  • 通知侦听器;
  • 语音或文本服务。

如果以上条件均不满足,应用将被视为处于后台。

Android 8.0(API 26) 以后系统不允许后台应用创建后台服务。因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。
在系统创建服务后应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。如果应用在此时间限制内未调用 startForeground() 方法,则系统将停止服务并声明此应用为 ANR

特殊情况是可以创建后台服务的:

  1. 处理对用户可见的任务时,应用将被置于白名单中,后台应用将被置于一个临时白名单中并持续数分钟。位于白名单中时,应用可以无限制地启动服务,并且其后台服务也可以运行。
    • 处理一条高优先级 Firebase 云消息传递 (FCM) 消息;
    • 接收广播,例如短信/彩信消息;
    • 从通知执行 PendingIntent。
  2. bindService() 方法不受后台限制。

由于 Android 8.0(API 26) 以后不能使用后台服务,使用 Service 需要使用 ContextCompat.startForegroundService() 启动前台服务,而且通知栏有 Notification 显示该 Service 正在运行,这可能会带来不好的用户体验。如果还是希望使用服务在后台默默工作、通过使用服务开启子进程等等,可以使用 JobIntentService

简介

JobIntentService实质为 Service,其继承关系如下所示:

 java.lang.Object
    ↳   android.content.Context
       ↳    android.content.ContextWrapper
           ↳    android.app.Service
               ↳    androidx.core.app.JobIntentService

JobIntentService 在官方文档解释如下:

Helper for processing work that has been enqueued for a job/service. When running on Android O or later, the work will be dispatched as a job via JobScheduler.enqueue. When running on older versions of the platform, it will use Context.startService.

中文解释为:JobIntentService 用于处理被加入到队列的 job/service 任务的一个辅助工具。当运行在 Android O 或更高版本时,任务将作为 job 通过 JobScheduler.enqueue() 进行分发;当运行在较老版本的平台上时,任务仍旧会使用 Context.startService() 执行。

源码分析

Android 8.0 及以上版本 JobIntentServiceJobService 做的事情是相同的,都是等着 JobScheduler 分配任务来执行。

不同点在于,JobService 使用的 handler 使用的是主线程的 Looper,因此需要在 onStartJob() 方法中手动创建 AsyncTask 去执行耗时任务,而 JobIntentService 则帮我们处理这一过程,使用它只需要写需要做的任务逻辑即可,不用担心阻塞主线程的问题。另外,向 JobScheduler 传递任务操作也更简单了,不需要在指定 JobInfo 中的参数,直接 enqueueWork() 方法就可以了。这有点像 ServiceIntentService 的关系。

下面来简单分析一下 JobIntentService 的源码:

public static void enqueueWork(@NonNull Context context, @NonNull Class cls, int jobId,
                                   @NonNull Intent work) {
    enqueueWork(context, new ComponentName(context, cls), jobId, work);
}

public static void enqueueWork(@NonNull Context context, @NonNull ComponentName component,
                                   int jobId, @NonNull Intent work) {
    if (work == null) {
        throw new IllegalArgumentException("work must not be null");
    }
    synchronized (sLock) {
        // 根据版本获取不同的WorkEnqueuer
        WorkEnqueuer we = getWorkEnqueuer(context, component, true, jobId);
        // 每个JobIntentService对应一个唯一JobId,所有给这个Service的work都必须相同
        we.ensureJobId(jobId);
        // 将work加入队列中
        we.enqueueWork(work);
    }
}

static WorkEnqueuer getWorkEnqueuer(Context context, ComponentName cn, boolean hasJobId,
                                        int jobId) {
    WorkEnqueuer we = sClassWorkEnqueuer.get(cn);
    if (we == null) {
        if (Build.VERSION.SDK_INT >= 26) {
            if (!hasJobId) {
                throw new IllegalArgumentException("Can't be here without a job id");
            }
            // Android 8.0及以上版本
            we = new JobWorkEnqueuer(context, cn, jobId);
        } else {
            // Android 8.0以下版本
            we = new CompatWorkEnqueuer(context, cn);
        }
        sClassWorkEnqueuer.put(cn, we);
    }
    return we;
}
/**
 * Android 8.0及以上版本
 */
static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer {
        private final JobInfo mJobInfo;
        private final JobScheduler mJobScheduler;

        JobWorkEnqueuer(Context context, ComponentName cn, int jobId) {
            super(cn);
            ensureJobId(jobId);
            // jobId和mComponentName来自于JobIntentService的enqueueWork()方法
        JobInfo.Builder b = new JobInfo.Builder(jobId, mComponentName);
            mJobInfo = b.setOverrideDeadline(0).build();
            mJobScheduler = (JobScheduler) context.getApplicationContext().getSystemService(
                    Context.JOB_SCHEDULER_SERVICE);
        }

        @Override
        void enqueueWork(Intent work) {
            if (DEBUG) Log.d(TAG, "Enqueueing work: " + work);
            // 调用JobScheduler.enqueue()方法
            mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work));
        }
}
/**
 * Android 8.0以下版本
 */
static final class CompatWorkEnqueuer extends WorkEnqueuer {
    private final Context mContext;
    private final PowerManager.WakeLock mLaunchWakeLock;
    private final PowerManager.WakeLock mRunWakeLock;
    boolean mLaunchingService;
    boolean mServiceProcessing;

    CompatWorkEnqueuer(Context context, ComponentName cn) {
        super(cn);
        mContext = context.getApplicationContext();
        // Make wake locks.  We need two, because the launch wake lock wants to have
        // a timeout, and the system does not do the right thing if you mix timeout and
        // non timeout (or even changing the timeout duration) in one wake lock.
        // 使用指定的级别和标志创建一个新的唤醒锁
        PowerManager pm = ((PowerManager) context.getSystemService(Context.POWER_SERVICE));
        mLaunchWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                cn.getClassName() + ":launch");
        mLaunchWakeLock.setReferenceCounted(false);
        mRunWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                cn.getClassName() + ":run");
        mRunWakeLock.setReferenceCounted(false);
    }

    @Override
    void enqueueWork(Intent work) {
        Intent intent = new Intent(work);
        // mComponentName来自于JobIntentService的enqueueWork()方法
        intent.setComponent(mComponentName);
        if (DEBUG) Log.d(TAG, "Starting service for work: " + work);
        // 执行Context的startService()方法
        if (mContext.startService(intent) != null) {
            synchronized (this) {
                if (!mLaunchingService) {
                    mLaunchingService = true;
                    if (!mServiceProcessing) {
                        // If the service is not already holding the wake lock for
                        // itself, acquire it now to keep the system running until
                        // we get this work dispatched.  We use a timeout here to
                        // protect against whatever problem may cause it to not get
                        // the work.
                        mLaunchWakeLock.acquire(60 * 1000);
                    }
                }
            }
        }
    }

    @Override
    public void serviceStartReceived() {
        synchronized (this) {
            // Once we have started processing work, we can count whatever last
            // enqueueWork() that happened as handled.
            mLaunchingService = false;
        }
    }

    @Override
    public void serviceProcessingStarted() {
        synchronized (this) {
            // We hold the wake lock as long as the service is processing commands.
            if (!mServiceProcessing) {
                mServiceProcessing = true;
                // Keep the device awake, but only for at most 10 minutes at a time
                // (Similar to JobScheduler.)
                mRunWakeLock.acquire(10 * 60 * 1000L);
                mLaunchWakeLock.release();
            }
        }
    }

    @Override
    public void serviceProcessingFinished() {
        synchronized (this) {
            if (mServiceProcessing) {
                // If we are transitioning back to a wakelock with a timeout, do the same
                // as if we had enqueued work without the service running.
                if (mLaunchingService) {
                    mLaunchWakeLock.acquire(60 * 1000);
                }
                mServiceProcessing = false;
                mRunWakeLock.release();
            }
        }
    }
}

案例演示

下面是 JobIntentService 的一个实现示例:

import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;

import androidx.core.app.JobIntentService;

/**
 * Example implementation of a JobIntentService.
 */
public class SimpleJobIntentService extends JobIntentService {
    /**
     * Unique job ID for this service.
     */
    static final int JOB_ID = 1000;

    /**
     * Convenience method for enqueuing work in to this service.
     */
    static void enqueueWork(Context context, Intent work) {
        enqueueWork(context, SimpleJobIntentService.class, JOB_ID, work);
    }

    @Override
    protected void onHandleWork(Intent intent) {
        // We have received work to do.  The system or framework is already
        // holding a wake lock for us at this point, so we can just go.
        Log.i("SimpleJobIntentService", "Executing work: " + intent);
        String label = intent.getStringExtra("label");
        if (label == null) {
            label = intent.toString();
        }
        toast("Executing: " + label);
        for (int i = 0; i < 5; i++) {
            Log.i("SimpleJobIntentService", "Running service " + (i + 1) + "/5 @ " 
                    + SystemClock.elapsedRealtime());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
        Log.i("SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        toast("All work complete");
    }

    final Handler mHandler = new Handler();

    // Helper for showing tests
    void toast(final CharSequence text) {
        mHandler.post(new Runnable() {
            @Override public void run() {
                Toast.makeText(SimpleJobIntentService.this, text, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

使用 JobIntentService 时需要注意以下几点:

  1. 当运行在 Android O 上时,JobScheduler 将为任务处理唤醒锁(从排队等待任务开始,直到任务被分派,以及在运行时持有唤醒锁);当运行在以前版本的平台上时,这个唤醒锁处理在类中通过直接调用 PowerManager 来模拟,因此应用程序必须请求 android.Manifest.permission.WAKE_LOCK 权限:

    <uses-permission android:name="android.permission.WAKE_LOCK" />
    
  2. JobIntentService 本质上也是一个 Service,因此需要在 AndroidManifest.xml 声明,以便系统与之交互:

    <service android:name="SimpleJobIntentService"
         android:permission="android.permission.BIND_JOB_SERVICE" >
         ...
    </service>
    
  3. JobIntentService 具体使用起来非常简单,它已经封装了大量的内部逻辑,只需要调用 enqueueWork() 方法就可以了:

    Intent intent = new Intent();
    SimpleJobIntentService.enqueueWork(context, intent);
    

总结

由于 Android O 的后台限制,创建后台服务需要使用 JobScheduler 来由系统进行调度任务的执行,而使用 JobService 的方式比较繁琐,Android 8.0 或更高版本时提供了 JobIntentService 帮助开发者更方便的将任务交给 JobScheduler 调度,其本质是 Service 后台任务在它的 onHandleWork() 方法中进行,子类重写该方法即可,使用较简单。

注意:

  • JobIntentService 处理了亮屏/锁屏,因此需要添加 android.Manifest.permission.WAKE_LOCK 权限;
  • JobIntentService 本质上也是一个 Service,因此需要在 AndroidManifest.xml 声明,以便系统与之交互;
  • JobIntentService 使用 enqueueWork() 来为分发和处理的新任务,它将在 onHandleWork() 方法中执行,该方法在工作线程中执行。

参考

Google 官方文档


评论
 上一篇
Gradle 入门详解 Gradle 入门详解
Java 作为一门世界级主流编程语言,有一款高效易用的项目管理工具是 Java 开发者想要的。先是 2000 年的 Ant,后有 2004 年 Maven 的诞生,都在 Java 市场上取得了巨大的成功。但二者都有一定的不足和局限性。 20
2019-06-10
下一篇 
Android Studio 提高 Gradle 构建速度的小技巧 Android Studio 提高 Gradle 构建速度的小技巧
长构建时间会减慢开发过程,因此本文将介绍一些可以帮助解决构建速度瓶颈的技巧。 提高构建速度的一般过程如下所示: 采取一些可以立即为大多数 Android Studio 项目带来好处的措施,优化构建配置。 分析构建配置,识别并诊断一些对项目
2019-05-28
  目录