Jetpack - WorkManager

一、概念

用于简化在Android应用程序中执行后台任务的管理。它提供了一种灵活、可靠的方式来调度和执行异步任务,而无需开发人员过多关注任务的管理和设备状态。

很多安卓厂商为了保证电池的续航,所以当应用被退出后,任务的执行可能不是很及时甚至不会执行。 任务有10分钟限制,长时间任务还是用前台Service。适用于数据同步、日志上传、条件出发的后台任务。

1.1 特点

约束条件可以指定任务执行的条件,例如在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行等,有助于减少不必要的任务执行、提高电池寿命和性能。
灵活性可以创建单次或重复性任务,包括延迟执行、周期性执行和根据触发条件执行的任务。还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。
任务链将多个任务串联起来,控制哪部分顺序运行,哪些部分并行运行。这对于处理复杂的工作流非常有用。
可靠性确保任务完成而不会丢失,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。
兼容性针对不同 Android 版本使用不同的后台任务调度API。最低支持 Android4(API14)。

1.2 使用场景

适用场景延迟任务任务不需要立即运行,可以推迟到合适的时机(充电、空闲、网络)。例如:定期同步数据、批量日志上传、夜间备份。
约束条件任务需要满足特定条件才执行。例如:只在有 Wi-Fi 时上传大文件、设备充电时执行耗电操作。
周期性任务需要按固定间隔重复执行的任务(支持最小间隔 15 分钟)。例如:每24小时检查应用更新。
任务链任务有依赖关系或需要按顺序执行。例如:先下载数据,再处理数据,最后上传结果。
不适用场景需要立即执行的任务:实时响应用户操作。
高优先级后台任务:音乐播放、更新定位(使用前台Service)。
短时快速任务:使用协程更轻量。
与生命周期相关任务:使用 lifecycle 组件。

二、添加依赖

最新版本

implementation "androidx.work:work-runtime-ktx:2.10.1"

三、创建任务 CoroutineWorker

Java环境继承 Work,协程环境继承 CoroutineWorker。重写 doWork() 用于耗时任务,需要返回一个 Result 用于将任务结果通知给 WorkManager:success成功、failure失败、retry重试。

class MyWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
    //该方法提供协程环境
    override suspend fun doWork(): Result {
        //耗时任务
        return Result.success()
    }
}

四、配置任务 WorkRequest

配置任务的触发条件:约束(链接WiFi、充电等)、运行周期(一次性、重复性)、延迟、重试策略。

4.1 一次性 / 重复性

创建 WorkRequest,一次性任务使用 OneTimeWorkRequest、重复性任务使用 PeriodicWorkRequest(间隔≥15分钟)。

4.1.1 单次任务 OneTimeWorkRequest

单次任务的初始状态为 ENQUEUED,任务会在满足其 Constraints 和初始延迟计时要求后立即运行。接下来,该任务会转为 RUNNING 状态,然后可能会根据任务的结果转为 SUCCEEDED、FAILED 状态;或者,如果结果是 retry,它可能会回到 ENQUEUED 状态。在此过程中,随时都可以取消任务,取消后任务将进入 CANCELLED 状态。 

对于单次任务,若无需额外配置可使用静态方法 from() 创建,复杂任务使用 Build 构建器模式创建。

//无需额外配置(静态方法)
val myWorkRequest1 = OneTimeWorkRequest.from(MyWork::class.java)
//复杂任务(构建器模式)
val myWorkRequest2 = OneTimeWorkRequestBuilder<MyWork>()
    //进行配置
    .build()

4.1.2 重复任务 PeriodicWorkRequest

重复任务只有一个终止状态 CANCELLED。这是因为重复任务永远不会结束。每次运行后,无论结果如何,系统都会重新对其进行调度。 

  • repeatInterval 用于指定重复周期(但周期内的具体执行时机不确定,取决于设定的约束和系统调度),最小可设置的值为15分钟。
  • flexTimeInterval 用于指定在重复周期中最后的某个时间段内执行,针对任务对时间敏感的需求(例如一小时的周期在最后15分钟期间执行),最小可设置的值为5分钟。

PeriodicWorkRequestBuilder()

public inline fun <reified W : ListenableWorker> PeriodicWorkRequestBuilder(
    repeatInterval: Duration
): PeriodicWorkRequest.Builder

public inline fun <reified W : ListenableWorker> PeriodicWorkRequestBuilder(
    repeatInterval: Duration,
    flexTimeInterval: Duration
): PeriodicWorkRequest.Builder
//导包是java的Duration
val myWorkRequest = PeriodicWorkRequestBuilder<MyWork>(
    Duration.ofHours(1),    //一小时为周期
    Duration.ofMinutes(15)    //周期内最后15分钟执行
).build()

//重载版本
val myWorkRequest2 = PeriodicWorkRequestBuilder<MyWork>(
    1, TimeUnit.HOURS,
    15, TimeUnit.MINUTES
).build()

4.2 约束 Constraints

用于确保将任务延迟到满足最佳条件时运行。可设置多个约束,全部满足才会执行。

setRequiredNetworkType()

网络

fun setRequiredNetworkType(networkType: NetworkType)

设置在特定网络环境下才会执行。

NetworkType.

    NOT_REQUIRED    不需要网络
    CONNECTED    已连接网络
    UNMETERED    不需要计费的网络
    NOT_ROAMING    非漫游网络
    METERED    计费网络

setRequiresBatteryNotLow()

电量

fun setRequiresBatteryNotLow(requiresBatteryNotLow: Boolean)

为 true 只有在设备电量充足时才会执行。

setRequiresCharging()

充电

fun setRequiresCharging(requiresCharging: Boolean)

为 true 只有在设备充电时才会执行。

setRequiresDeviceIdle()

空闲

fun setRequiresDeviceIdle(requiresDeviceIdle: Boolean)

为 true 只有在设备必须处于空闲状态才会执行。对批量操作非常有用,因为批量操作可能会降低设备商正在积极运行的应用性能。

setRequiresStorageNotLow()

存储

fun setRequiresStorageNotLow(requiresStorageNotLow: Boolean)

为 true 只有在存储空间充足时才会执行。

//创建约束
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .setRequiresBatteryNotLow(true)
    .setRequiresCharging(true)
    .build()
//设置给WorkRequest
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
        .setConstraints(constraints)
        .build()

4.3 延迟 setInitialDelay()

执行前进行一段时间的延迟。重复性任务只在首次执行会延迟。

fun setInitialDelay(duration: Duration)
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
        .setInitialDelay(Duration.ofMinutes(15))
        .build()

4.4 输入输出数据 setInputData()

输入输出数据都是Map形式的Data类型。

fun setInputData(inputData: Data)
class MyWork(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    //定义key传参和读取更安全
    companion object {
        const val URL = "url"
    }
    override suspend fun doWork(): Result {
        val url = inputData.getString(URL) ?: return Result.failure()
        val age = inputData.getInt("age", 0)
        downloadPic(url)
        return Result.success()
    }
}
//方式一(参数2是Any?在Work中获取时不好把控)
val data1 = workDataOf(MyWork.URL to "http://...")
//方式二(推荐)
val data2 = Data.Builder()
    .putString(MyWork.URL, "http://...")    //推荐使用MyWork中定义好的key
    .putInt("age", 18)
    .build()
//通过WorkRequest输入
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
    .setInputData(data2)
    .build()

4.5 重试 setBackoffCriteria()

doWork() 中返回 Resulr.retry() 的时候代表任务需要重新执行,通过 setBackoffCriteria() 配置重试时间(最小值为10秒)和时间的增长策略。

setBackoffCriteria()

fun setBackoffCriteria(backoffPolicy: BackoffPolicy, duration: Duration)

策略定义了后续再次重试时间的增长方式,假设时间设为10秒,BackoffPolicy.LINEAR 后续为 20、30、40,BackoffPolicy.EXPONENTIAL 后续为 20、40、80。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .setBackoffCriteria(
       BackoffPolicy.LINEAR,
       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
       TimeUnit.MILLISECONDS)
   .build()

4.6 加急 setExpedited()

        针对一些很重要或由用户启动的后台任务(付款订阅,发送消息)、需要立即执行并在几分钟内完成的简短任务、即便关闭应用也应继续执行。但在某些情况下会被延迟:

  • 负载:系统负载过高,过多的任务已经在运行或系统内存不足时。
  • 配额:应用在前台运行时加急不会被限制,在后台时系统为了有效在应用间平衡资源,每个应用都会获得执行时间配额(取决于待机模式存储分区和进程重要性),用完后在刷新前无法再加急。
setExpedited()

fun setExpedited(policy: OutOfQuotaPolicy)

传入配额超出后的执行策略

OutOfQuotaPolicy

        .RUN_AS_NON_EXPEDITED_WORK_REQUEST    变成普通任务

        .DROP_WORK_REQUEST    丢弃任务

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

4.6.1 兼容性适配(前台Service)

Android12 之前的版本加急会采用前台服务,因此需要构建一个通知(12之后的版本不会显示通知)。重写 getForegroundInfo() 来构建通知,在 doWork() 中,调用 setForeground() 将其传入。需要注意处理通知渠道、前台服务类型和服务权限声明。

class MyWork(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    private val channelId = "后台下载"
    private val notificationId = 123456
    override suspend fun doWork(): Result {
        //设置前台服务
        setForeground(getForegroundInfo())
        return Result.success()
    }
    //重写提供通知(注意处理通知渠道、前台服务类型声明和前台服务权限申请)
    override suspend fun getForegroundInfo(): ForegroundInfo {
        val notification = NotificationCompat.Builder(APP.context, channelId)
            .setContentText("后台下载中")
            .build()
        //参数三是前台服务类型
        return ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
    }
}

4.7 标记 addTag()

通过标记一组相关的任务,可以批量取消任务请求或获取状态。单个请求可添加多个标记。

val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
    .addTag("download")
    .addTag("important")
    .build()

五、管理任务

5.1 普通提交、唯一性提交

将 WorkRequest 提交后,WorkManager 就会根据配置执行 doWork() 里的业务代码。注意避免重复提交,否则会执行多次。唯一性可以确保同一时刻只有一个具体特定名称的任务实例。uniqueName(唯一性名称)是由开发者指定仅绑定一个任务实例(ID是由WorkManager生成,Tag能绑定多个实例)。

普通提交

fun enqueue(request: WorkRequest): Operation

单次、重复任务都可提交。

唯一性提交

fun enqueueUniqueWork(
        uniqueWorkName: String,        //唯一性名称
        existingWorkPolicy: ExistingWorkPolicy,        //如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作
        request: OneTimeWorkRequest        //需要提交的任务
): Operation

适用于提交单次任务。

enqueueUniquePeriodicWork(
        uniqueWorkName: String,
        existingPeriodicWorkPolicy: ExistingPeriodicWorkPolicy,
        request: PeriodicWorkRequest
): Operation

适用于提交重复任务。

ExistingWorkPolicy

REPLACE:新任务替换当前任务,当前任务会被取消。

KEEP:忽略新任务,保留当前任务。

APPEND:将新任务链接到当前任务尾部,当前任务完成后执行新任务,当前任务是新任务的先决条件,如果当前任务变为 CANCELLED 或 FAILED 状态,新任务也会变为相同状态且不会执行。若想新任务能得到执行用下面的。

APPEND_AND_REPLACE:同上,使用该策略无论当前任务的状态如何都执行新任务。

ExistingPeriodicWorkPolicy

KEEP:忽略新任务,保留当前任务。

UPDATE:下一个周期采用新任务。

CANCEL_AND_REENQUEUE:新任务替换当前任务,当前任务会被取消。

WorkManager.getInstance(context)
    .enqueue(myWorkRequest)

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork("", ExistingPeriodicWorkPolicy.UPDATE, myWorkRequest)

5.2 观察 WorkInfo

任务提交后可能需要根据其执行情况做相应的处理(状态查询、获取进度、获取结果、取消任务),可以通过 id、tag、uniqueName 获取到任务对应的 WorkInfo,进而获取 State 以及 Result.success() 携带的结果。

5.2.1 简单查询 getWorkInfoBy***()

fun getWorkInfoById(id: UUID): ListenableFuture<WorkInfo?>
fun getWorkInfoByIdLiveData(id: UUID): LiveData<WorkInfo?>

fun getWorkInfoByIdFlow(id: UUID): Flow<WorkInfo?>

通过 id 查询。

fun getWorkInfosByTag(tag: String): ListenableFuture<List<WorkInfo>>

fun getWorkInfosByTagLiveData(tag: String): LiveData<List<WorkInfo>>

fun getWorkInfosByTagFlow(tag: String): Flow<List<WorkInfo>>

通过 tag 查询。

fun getWorkInfosForUniqueWork(uniqueWorkName: String): ListenableFuture<List<WorkInfo>>

fun getWorkInfosForUniqueWorkLiveData(uniqueWorkName: String): LiveData<List<WorkInfo>>

fun getWorkInfosForUniqueWorkFlow(uniqueWorkName: String): Flow<List<WorkInfo>>

通过 uniqueName 查询。

WorkManager
    .getInstance(context)
    .getWorkInfoByIdLiveData(myWorkRequest.id)
    .observe(lifecyclerOwner) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.ENQUEUED -> TODO()
            WorkInfo.State.RUNNING -> TODO()
            WorkInfo.State.SUCCEEDED -> TODO()
            WorkInfo.State.FAILED -> TODO()
            WorkInfo.State.BLOCKED -> TODO()
            WorkInfo.State.CANCELLED -> TODO()
            null -> TODO()
        }
    }

5.2.2 复杂查询 WorkQuery

支持按照任务的 id、tag、uniqueName、state进行组合查询。

WorkQuery

fun fromIds(ids: List<UUID>): Builder

fun fromTags(tags: List<String>): Builder

fun fromUniqueWorkNames(uniqueWorkNames: List<String>): Builder

fun fromStates(states: List<WorkInfo.State>): Builder

先通过不同的方式找到对应的任务。

fun addIds(ids: List<UUID>): Builder
fun addTags(tags: List<String>): Builder
fun addUniqueWorkNames(uniqueWorkNames: List<String>): Builder
fun addStates(states: List<WorkInfo.State>): Builder

再添加其它附加条件。

WorkManager

fun getWorkInfos(workQuery: WorkQuery): ListenableFuture<List<WorkInfo>>

fun getWorkInfosLiveData(workQuery: WorkQuery): LiveData<List<WorkInfo>>
fun getWorkInfosFlow(workQuery: WorkQuery): Flow<List<WorkInfo>>

通过 WorkQuery 进行复杂查询。

//查找带有“syncTag”标签,处于SUCCESS状态,且唯一名称为“preProcess”或“sync”的所有任务
val workQuery = WorkQuery.Builder
    //先通过不同的方式找到对应的任务
    .fromTags(listOf("syncTag"))
    //再添加其它附加条件
    .addStates(listOf(WorkInfo.State.SUCCEEDED))
    .addUniqueWorkNames(listOf("preProcess", "sync")
    .build()

WorkManager
    .getInstance(APP.context)
    .getWorkInfos(workQuery)

5.2.3 获取状态

WorkManager
    .getInstance(context)
    .getWorkInfoByIdLiveData(myWorkRequest.id)
    .observe(lifecyclerOwner) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.ENQUEUED -> TODO()
            WorkInfo.State.RUNNING -> TODO()
            WorkInfo.State.SUCCEEDED -> TODO()
            WorkInfo.State.FAILED -> TODO()
            WorkInfo.State.BLOCKED -> TODO()
            WorkInfo.State.CANCELLED -> TODO()
            null -> TODO()
        }
    }

5.2.4 设置和获取进度

class MyWork(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    companion object {
        const val PROGRESS = "progress"
    }
    override suspend fun doWork(): Result {
        val firstProgress = workDataOf(PROGRESS to 0)
        val lastProgress = workDataOf(PROGRESS to 100)
        setProgress(firstProgress)
        delay(1000)
        setProgress(lastProgress)
        return Result.success()
    }
}
WorkManager
    .getInstance(APP.context)
    .getWorkInfoByIdLiveData(workRequset.id)
    .observe(livfcycleOwner) { workInfo ->
        workInfo?.let {
            val progress = it.progress.getInt(MyWork.PROGRESS, 0)
            if (progress == 0) {}
            if (progress == 100) {}
        }
    }

5.3 取消

        当不再需要运行先前提交的任务时,可以通过 id、tag、uniqueName 进行取消,或全部取消。如果任务已经完成则不会执行任何操作,否则任务的状态会变成 CANCELLED 之后就不会再执行这个任务,其它依赖此任务的任务也会变为 CANCELLED。任务会因为以下几种原因而停止:

  • 你明确要求取消它( 例如通过调用 WorkManager.cancelWorkbyId() 取消)。
  • 对于唯一性工作,明确的将 ExistingWorkPolicy = REPLACE 的新任务提交,当前的任务会被立即视为已取消。
  • 任务的约束条件已不再满足。
  • 系统处于某种原因指示你的应用停止工作。如果超过10分钟的执行期限,可能会发生这种情况。该任务会调度为在稍后重试。

fun cancelWorkById(id: UUID): Operation

通过 id 取消。

fun cancelAllWorkByTag(tag: String): Operation

通过 tag 取消。

fun cancelUniqueWork(uniqueWorkName: String): Operation

通过 uniqueName 取消。

fun cancelAllWork(): Operation

全部取消。

5.4 更新

5.4.1 使用 updateWork()

避免取消任务通常应避免取消现有任务再将新任务提交,可能会导致重复执行某些任务和额外编写大量代码。(计算期间被取消,新任务需要重新计算。取消重复任务,新任务需要计算时间偏移来保证执行时间的一致性)
应该取消的任务一次性任务和重复性任务不能通过更新去替换。
适合更新的场景约束

获取现有任务的 id,创建新的 WorkRequest,设置约束 Constraints,WorkManager 调用 updateWork() 将新的任务传递。 

val oldWorkRequset = OneTimeWorkRequestBuilder<MyWork>().build()
val workManager = WorkManager.getInstance(APP.context)
workManager.enqueue(oldWorkRequset)
//拿到旧任务的id
//不能直接通过旧WorkRequest拿到,就通过uniqueName或tag拿到WorkInfo再拿到id
val oldWorkId = oldWorkRequset.id
//创建新的约束
val newConstraints = Constraints.Builder()
    .setRequiresCharging(false)
    .build()
//创建新的任务
val newRequest = OneTimeWorkRequestBuilder<MyWork>()
    .setConstraints(newConstraints)
    .setId(oldWorkId)   //传入旧的任务id
    .build()
//更新任务(每次更新都会将 generation + 1)
workManager.updateWork(newRequest)
//更新后,通过新旧WorkInfo获取的ID、generation相同
val oldWorkInfo = workManager.getWorkInfoById(oldWorkRequset.id).get()
val newWorkInfo = workManager.getWorkInfoById(newWorkRequest.id).get()
//都是7ae9519a-dc70-4a04-84fc-e91900d50c11
Log.e("oldWorkInfo", oldWorkInfo?.id.toString())
Log.e("newWorkInfo", newWorkInfo?.id.toString())
//都是1
Log.e("oldWorkInfo", oldWorkInfo?.generation.toString())
Log.e("newWorkInfo", newWorkInfo?.generation.toString())

5.4.2 使用 ExistingPeriodicWorkPolicy.UPDATE

六、任务链

当需要以特定顺序运行多个单次任务时,可以串接单个任务(顺序执行)或任务集合(集合中的任务并行执行)。前一个节点执行成功才会继续往下执行。

如果节点失败后续节点不会被执行。若配置了重试策略,失败的任务会重试,不会影响与它并行执行(同级)的节点。

若未配置重试策略或重试已用尽,后续任务不会执行并被标记为 FAILED。

若有节点被取消,后续的任务都会被标记为 CANCELLED。 

向 FAILED 或 CANCELLED 的任务链追加提交任务,新的任务也会被同样标记,如果想新任务能被执行,将 ExistingWorkPolicy 配置为 APPEND_OR_REPLACE。

6.1 基本用法

通过 beginWith() 创建首个节点,通过 then() 追加节点,可传入单个任务或任务集合,集合中的任务会并行执行。

val requset1 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset2 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset3 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset4 = OneTimeWorkRequestBuilder<MyWork>().build()
val requset5 = OneTimeWorkRequestBuilder<MyWork>().build()

WorkManager
    .getInstance(APP.context)
    .beginWith(requset1)
    .then(listOf(requset2, requset3, requset4)) //这三个会并行运行
    .then(requset5)
    .enqueue()

6.1 处理输入冲突

前一个节点的输出结果会被当做后一个节点的输入参数。如果前一个节点是任务集合,通过任务的变量名区分结果,若存在相同的变量名,默认后执行完的会覆盖掉前一个的值,若想保留两个值,后一个节点的任务需要通过 setInputMerger() 配置不同的输入冲突策略。

OverwritingInputMerger(默认)

变量名相同时,后执行完的那个会覆盖掉前一个。

ArrayCreatingInputMerger

变量名相同时,通过数组保存每个值。
val requset5 = OneTimeWorkRequestBuilder<MyWork>()
    .setInputMerger(ArrayCreatingInputMerger::class)    //配置输入冲突策略
    .build()

WorkManager
    .getInstance(APP.context)
    .beginWith(listOf(requset1, requset1, requset2)) //集合中有两个名为 request1 的任务
    .then(requset5)    //已配置
    .enqueue()

6.1.1 OverwritingInputMerger(默认)

任务集合中有两个变量名是 “plantName1” 的任务,由于是并行执行,后执行完的会覆盖先执行完的。(图示假设第二个“plantName1”后执行完)

6.1.2 ArrayCreatingInputMerger

相同变量名的任务,值会通过数组保存。 

七、延迟初始化

默认在启动应用时 WorkManager 就会初始化,根据合并规则在 app 下的  AndroidManifest 中重写来覆盖。

 <!-- 如果应用不需要InitializationProvider的话可以直接移除InitializationProvider -->
<provider
   android:name="androidx.startup.InitializationProvider"
   android:authorities="${applicationId}.androidx-startup"
   tools:node="remove">
</provider> 

<!--如果还用到其他Initializer,只是禁用WorkManagerInitializer的话-->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
 </provider>

 八、自定义配置

让自定义的 Application 类实现 Configuration.Provider 接口,并提供自己的 Configuration.Provider.getWorkManagerConfiguration() 实现。当需要使用 WorkManager 时,请务必调用方法 WorkManager.getInstance(Context)。WorkManager 会调用应用的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration(无需自行调用 WorkManager.initialize())

class MyApplication() : Application(), Configuration.Provider {
     override fun getWorkManagerConfiguration() =
           Configuration.Builder()
                .setMinimumLoggingLevel(android.util.Log.INFO)
                .setExecutor(Executors.newFixedThreadPool(8))
                .build()
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值