Android WorkManager使用以及源码分析

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Android WorkManager使用以及源码分析

众少成多积小致巨   2023-03-20 我要评论

1、前言

WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。 WorkManager 可处理三种类型的持久性工作:

  • 立即执行:必须立即开始且很快就完成的任务,可以加急。
  • 长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。
  • 可延期执行:延期开始并且可以定期运行的预定任务。

2、使用

2.1、引用

implementation "androidx.work:work-runtime:2.7.1" //基础使用
implementation "androidx.work:work-multiprocess:2.7.1" //跨进程时引用

2.2 使用

执行一次性任务

Data data = new Data.Builder().putBoolean("is_test", false).build();
WorkManager.getInstance(this).enqueue(
        new OneTimeWorkRequest.Builder(Test.class) // 执行任务一次性任务
                .setInputMerger(NewInputMerge.class) // 输入数据合并策略,这里并没有用,链式处理时,多个上流执行结果合并,作为下流输入数据
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) // 重试策略
                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) // 加急处理
                .addTag("test").addTag("huahua") // 标识
                .setInputData(data) // 输入数据
                .setInitialDelay(5, TimeUnit.SECONDS) // 执行延时时间
                .setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).build()) // 约束,部分约束只对高版本有效
                .build()
);

执行周期性任务

WorkManager.getInstance(this).enqueue(
        new PeriodicWorkRequest.Builder(Test.class, 2, TimeUnit.HOURS) // 执行周期性任务,周期2小时
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                .addTag("test").addTag("huahua")
                .setInputData(data)
                .setInitialDelay(5, TimeUnit.SECONDS) // 首次执行延时时间
                .setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).build())
                .build()
);

拥有名字的任务

WorkManager.getInstance(this).enqueueUniqueWork("test", ExistingWorkPolicy.KEEP,
        new OneTimeWorkRequest.Builder(Test.class).build()); // 此种方法会对重复名字的任务进行处理

监听状态

WorkManager.getInstance(this).getWorkInfosByTagLiveData("test").observe(this, 
        new Observer<List<WorkInfo>>() {
            @Override
            public void onChanged(List<WorkInfo> workInfos) {

            }
        });

定义Work代码

public class Test extends Worker {
    public Test(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
        Data input = getInputData(); // 获取输入数据
        boolean isTest = input.getBoolean("is_test", true);
    }

    @NonNull
    @Override
    public Result doWork() {
        return Result.success();
    }
}

继承使Worker类,实现doWork()方法,此方法是实现任务的主体;doWork()返回的 Result会通知 WorkManager 服务工作是否成功,以及工作失败时是否应重试工作。

  • Result.success():工作成功完成。
  • Result.failure():工作失败。
  • Result.retry():工作失败,应根据其重试政策在其他时间尝试。

配置初始化 不同的版本初始化不同,但是都是通过Provider来进行的,2.6之前是WorkManagerInitializer, 2.6之后是InitializationProvider;这里按照2.7.1的版本来说,老版本有需要留言回复;有两种处理方案

  • 移除默认Provider
  • 按照Provider流程来进行

按照提供初始化流程处理

1.首先注意一个字符串配置:这个很重要,不建议修改

<string name="androidx_startup" translatable="false">androidx.startup</string>

2.提供实现Initializer的类,这个类是被调用的关键

3.移除默认实现

<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>

默认实现Initializer类WorkManagerInitializer,使用的默认configuration

public final class WorkManagerInitializer implements Initializer<WorkManager> {

    private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");

    @NonNull
    @Override
    public WorkManager create(@NonNull Context context) {
        Logger.get().debug(TAG, "Initializing WorkManager with default configuration.");
        WorkManager.initialize(context, new Configuration.Builder().build()); // 这里提供Configuration
        return WorkManager.getInstance(context);
    }

    @NonNull
    @Override
    public List<Class<? extends androidx.startup.Initializer<?>>> dependencies() {
        return Collections.emptyList(); // 提供需要执行的其它Initializer
    }
}

4.进行注册自定义实现; 在meta-data数据处理,key为你需要调用的初始化类,value必须为R.sting.androidx_startup这个字符串的值; 如下

<provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge" >
        <meta-data
            android:name="androidx.work.WorkManagerInitializer" // 这里为你的实现的Initializer
            android:value="androidx.startup" /> // 这里必须与R.sting.androidx_startup保持一致
    </provider>

2.3 重要概念

任务标识

  • id : 通过WorkRequest对象getStringId获取,每次添加一次任务,就会得到唯一的id
  • name :任务名字,暂时一次性任务才可以有; 一个任务最多一个名字,而一个名字可以对应多个任务; 同名字任务可以通过定义不同名字冲突来解决
  • tag: 一个任务可以拥有多个tag,一个tag可以对应多个任务

任务类型

  • 一次性任务: 仅仅执行一次, 如果结果返回retry时,按照重试退避政策,进行重试,直至成功
  • 周期性任务: 按照周期执行,无论结果返回什么情况,每次周期内仅仅执行一次

任务链

使用 WorkManager 创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。使用 WorkManager.beginWith(OneTimeWorkRequest 或 WorkManager.beginWith(List)返回WorkContinuation实例,然后通过其 then(OneTimeWorkRequest)或 then(List)添加任务链,最后使用WorkContinuation.enqueue()进行执行。

任务链先添加的任务为后续任务的先决条件, 也就是前面任务成功了后面任务才会执行

重试退避政策

针对worker执行返回Result.retry()时处理策略, 使用方法setBackoffCriteria设置, 有两个指标,策略和延时时间(允许的最小值,即 10 秒);情况有下面2种

  • 线性LINEAR: 每次失败后重新开启任务时间延时为 延时时间次数倍(延时时间 * n)
  • EXPONENTIAL: 每次失败后重新开启任务时间延时为 延时时间的指数倍(延时时间 * 2^n)

输入合并器

针对一次性任务,且在工作链中,父级工作请求的输出将作为子级的输入传入的场景中使用。输入中会存在相同关键字,这时,输入就会存在冲突

WorkManager 提供两种不同类型的 InputMerger:

  • OverwritingInputMerger:会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。
  • ArrayCreatingInputMerger: 会尝试合并输入,并在必要时创建数组。

冲突解决政策

调度唯一工作时,发生冲突时要执行的操作。可以通过在将工作加入队列时传递一个枚举来实现此目的。

对于一次性工作,您需要提供一个 ExistingWorkPolicy,有4 个选项。

  • REPLACE:用新工作替换现有工作。此选项将取消现有工作。
  • KEEP:保留现有工作,并忽略新工作。
  • APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作到现有工作,在现有工作完成后运行。 现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLED 或 FAILED 状态,新工作也会变为 CANCELLED 或 FAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE
  • APPEND_OR_REPLACE: 类似于 APPEND,不过它并不依赖于先决条件工作状态。**即使现有工作变为 CANCELLED 或 FAILED 状态,新工作仍会运行。

约束条件

任务执行的先决条件,使用Contraints.Builder()进行构建实例。有一下约束

  • NetworkType : 约束运行工作所需的网络类型。
  • BatteryNotLow : 如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。
  • RequiresCharging : 如果设置为 true,那么工作只能在设备充电时运行。
  • DeviceIdle:如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。
  • StorageNotLow: 如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。

还有以下约束,对于>=24的版本有效:

  • setContentUriTriggers方法: 设置触发任务的uri
  • setTriggerContentUpdateDelay方法:设置触发执行的延迟时间
  • setTriggerMaxContentDelay方法:设置处罚执行的最大延时

3、原理

合理分为几个部分来说

  • 1. 约束检测: 约束检测的逻辑以及实现
  • 2. 任务调度: 3种调度器, alarm、greedy、JobScheduler; 用于唤起任务执行
  • 3. 任务执行流程 :请求的包装,任务如何加入调度器,以及调度完成后执行任务详情

这里有需要理解的一个技术点:SettableFuture,实现了ListenableFuture接口,并且增加了下面接口ListenableFuture。这个类在源码中频繁使用

ListenableFuture只有一个方法

void addListener(Runnable listener, Executor executor)

调用了这个方法,这个类才有使用意义;调用之后,表示SettableFuture若完成,则使用executor执行listener

另外一个让我觉得有意思的地方,或者说不同之前future的地方是, SettableFuture未实现Runnable接口,也就是其结果不是来源于自己,来源于调用下面3个方法

    public boolean set(@Nullable V value) // 正常设置结果

    public boolean setException(Throwable throwable) // 执行结果异常

    public boolean setFuture(ListenableFuture<? extends V> future) // 设置执行结果来源,也就是ListenableFuture的执行结果

3.1 约束检测

类图如下

结合源码分析,结论有:

  • WorkConstraintsTracker类:使用入口,构造器传入WorkConstraintsCallback为结果回调,也可以直接调用方法areAllConstraintsMet来判断是否满足约束
  • ConstraintController类:一种约束控制器类,通过OnConstraintUpdatedCallback回调实时结果给WorkConstraintsTracker, 通过ConstraintListener与ConstraintTracker联系
  • ConstraintTracker:实际约束值的检测者,ConstraintListener回调实时传递值与ConstraintController;通过setState触发回调

实际检测技术点,也就是ConstraintTracker实现

3.1.1 StorageNotLowTracker类

存储空间的是否低,根据系统广播来处理

Intent.ACTION_DEVICE_STORAGE_OK // 存储空间够用
Intent.ACTION_DEVICE_STORAGE_LOW // 存储空间很低

初步检测状态:

@Override
public Boolean getInitialState() {
    Intent intent = mAppContext.registerReceiver(null, getIntentFilter());
    if (intent == null || intent.getAction() == null) {
        return true;
    } else {
        switch (intent.getAction()) {
            case Intent.ACTION_DEVICE_STORAGE_OK:
                return true;
            case Intent.ACTION_DEVICE_STORAGE_LOW:
                return false;
            default:
                return null;
        }
    }
}

实时更新通过接收广播信息:

public void onBroadcastReceive(Context context, @NonNull Intent intent) {
    if (intent.getAction() == null) {
        return;
    }

    switch (intent.getAction()) {
        case Intent.ACTION_DEVICE_STORAGE_OK:
            setState(true);
            break;
        case Intent.ACTION_DEVICE_STORAGE_LOW:
            setState(false);
            break;
    }
}

3.1.2 BatteryChargingTracker类

充电状态,同样通过广播来进行处理;

接收广播根据版本不同而不同如下:

    public IntentFilter getIntentFilter() {
        IntentFilter intentFilter = new IntentFilter();
        if (Build.VERSION.SDK_INT >= 23) {
            intentFilter.addAction(BatteryManager.ACTION_CHARGING);
            intentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
        } else {
            intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
            intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
        }
        return intentFilter;
    }

初始状态:

    @Override
    public Boolean getInitialState() {
        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent intent = mAppContext.registerReceiver(null, intentFilter);
        if (intent == null) {
            Logger.get().error(TAG, "getInitialState - null intent received");
            return null;
        }
        return isBatteryChangedIntentCharging(intent);
    }
    
    private boolean isBatteryChangedIntentCharging(Intent intent) {
        boolean charging;
        if (Build.VERSION.SDK_INT >= 23) {
            int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
            charging = (status == BatteryManager.BATTERY_STATUS_CHARGING
                    || status == BatteryManager.BATTERY_STATUS_FULL);
        } else {
            int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
            charging = (chargePlug != 0);
        }
        return charging;
    }

实时状态:

@Override
public void onBroadcastReceive(Context context, @NonNull Intent intent) {
    String action = intent.getAction();
    if (action == null) {
        return;
    }
    Logger.get().debug(TAG, String.format("Received %s", action));
    switch (action) {
        case BatteryManager.ACTION_CHARGING:
            setState(true);
            break;
        case BatteryManager.ACTION_DISCHARGING:
            setState(false);
            break;
        case Intent.ACTION_POWER_CONNECTED:
            setState(true);
            break;
        case Intent.ACTION_POWER_DISCONNECTED:
            setState(false);
            break;
    }
}

3.1.3 BatteryNotLowTracker类

电量是否低,同样通过广播实时获取状态

    Intent.ACTION_BATTERY_OKAY //电量正常
    Intent.ACTION_BATTERY_LOW //电量低

初始状态:

    @Override
    public Boolean getInitialState() {
        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent intent = mAppContext.registerReceiver(null, intentFilter);
        if (intent == null) {
            Logger.get().error(TAG, "getInitialState - null intent received");
            return null;
        }

        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
        int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        float batteryPercentage = level / (float) scale;

        return (status == BatteryManager.BATTERY_STATUS_UNKNOWN
                || batteryPercentage > BATTERY_LOW_THRESHOLD); // 这里是小于15%算是低
    }

实时更新:

    @Override
    public void onBroadcastReceive(Context context, @NonNull Intent intent) {
        if (intent.getAction() == null) {
            return;
        }
        Logger.get().debug(TAG, String.format("Received %s", intent.getAction()));
        switch (intent.getAction()) {
            case Intent.ACTION_BATTERY_OKAY:
                setState(true);
                break;
            case Intent.ACTION_BATTERY_LOW:
                setState(false);
                break;
        }
    }

3.1.4 NetworkStateTracker类

网络状态,这个根据版本不同使用也不同

  • 大于等于24时, ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback networkCallback) 来进行监听网络状态变化
  • 小于24时,通过广播ConnectivityManager.CONNECTIVITY_ACTION来进行处理

初始状态:

NetworkState getActiveNetworkState() {
    NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
    boolean isConnected = info != null && info.isConnected();
    boolean isValidated = isActiveNetworkValidated();
    boolean isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
    boolean isNotRoaming = info != null && !info.isRoaming();
    return new NetworkState(isConnected, isValidated, isMetered, isNotRoaming);
}          

实时状态: 在回调NetworkCallback或者接收到广播onReceive时,调用getActiveNetworkState得到

3.2 任务调度器

实现Scheduler接口,调度器有3个:

  • SystemJobScheduler: 使用JobSceuler来完成任务唤起;小于23版本时使用
  • SystemAlarmScheduler: 使用Alarm来完成任务唤起;大于等于23版本使用
  • GreedyScheduler:在进程内直接调用,不同android版本均会建立 其实还有一个androidx.work.impl.background.gcm.GcmScheduler,这个实现需要借助google的GCM推送服务来实现,这里用了反射(国内不支持),并且优先于SystemAlarmScheduler

调度器的创建在WorkManagerImpl类

   public List<Scheduler> createSchedulers(
            @NonNull Context context,
            @NonNull Configuration configuration,
            @NonNull TaskExecutor taskExecutor) {

        return Arrays.asList(
                Schedulers.createBestAvailableBackgroundScheduler(context, this), // 可以被系统唤起调用手段
                new GreedyScheduler(context, configuration, taskExecutor, this)); // 进程内调用手段
    }

调度器被调用的逻辑在Schedulers

public static void schedule(Configuration configuration,WorkDatabase workDatabase,List<Scheduler> schedulers) {
        if (schedulers == null || schedulers.size() == 0) {
            return;
        }
        WorkSpecDao workSpecDao = workDatabase.workSpecDao();
        List<WorkSpec> eligibleWorkSpecsForLimitedSlots;
        List<WorkSpec> allEligibleWorkSpecs;
        workDatabase.beginTransaction();
        try {
            eligibleWorkSpecsForLimitedSlots = workSpecDao.getEligibleWorkForScheduling(configuration.getMaxSchedulerLimit()); 
            allEligibleWorkSpecs = workSpecDao.getAllEligibleWorkSpecsForScheduling(MAX_GREEDY_SCHEDULER_LIMIT);

            if (eligibleWorkSpecsForLimitedSlots != null && eligibleWorkSpecsForLimitedSlots.size() > 0) {
                long now = System.currentTimeMillis();
                for (WorkSpec workSpec : eligibleWorkSpecsForLimitedSlots) {
                    workSpecDao.markWorkSpecScheduled(workSpec.id, now); // 进行插槽状态处理,非默认才会被GreedyScheduler进行调度
                }
            }
            workDatabase.setTransactionSuccessful();
        } finally {
            workDatabase.endTransaction();
        }

        if (eligibleWorkSpecsForLimitedSlots != null && eligibleWorkSpecsForLimitedSlots.size() > 0) {
            WorkSpec[] eligibleWorkSpecsArray =
                    new WorkSpec[eligibleWorkSpecsForLimitedSlots.size()];
            eligibleWorkSpecsArray =
                    eligibleWorkSpecsForLimitedSlots.toArray(eligibleWorkSpecsArray);
            for (Scheduler scheduler : schedulers) {
                if (scheduler.hasLimitedSchedulingSlots()) {
                    scheduler.schedule(eligibleWorkSpecsArray);
                }
            }
        }

        if (allEligibleWorkSpecs != null && allEligibleWorkSpecs.size() > 0) {
            WorkSpec[] enqueuedWorkSpecsArray = new WorkSpec[allEligibleWorkSpecs.size()];
            enqueuedWorkSpecsArray = allEligibleWorkSpecs.toArray(enqueuedWorkSpecsArray);
            for (Scheduler scheduler : schedulers) {
                if (!scheduler.hasLimitedSchedulingSlots()) {
                    scheduler.schedule(enqueuedWorkSpecsArray);
                }
            }
        }
    }

GreedyScheduler调度器处理任务正在排队,任务最大限制是200条;其它调度器执行在排队且未被调度器处理的,且和现在调度器已经处理的总和不超过配置中的最大限制,默认是版本23是10,其它版本20

3.2.1 GreedyScheduler调度器

类图:

通过流程分析有以下结论:

  • 延时通过DelayedWorkTracker来处理,其实是通过Handler机制处理,在主Handler中进行
  • 约束通过WorkConstraintsTracker来进行检测,通过自身实现回调WorkConstraintsCallback来处理
  • 满足执行条件的任务,通过WorkManagerImpl进行执行
  • 实现ExecutionListener接口,来检测已经完成的任务,避免重复执行
  • 必须在配置的进程中处理,默认为应用主进程

3.2.2 SystemJobScheduler调度器

类图:

  • SystemJobInfoConverter进行数据转换JobScheduler需要的入参,以及其它数据互相转换
  • SystemJobService负责调用WorkManagerImpl类方法去执行任务;以及任务执行完成的处理
  • 约束由JobScheduler自己内部进行;而这里只需要调用方法即可

这个调度器比较简单,其实现主要通过了JobScheduler的实现,JobScheduler的实现原理这里不做介绍

3.2.3 SystemAlarmScheduler调度器

这个调度器实现就比较复杂了;类图:

  • SystemAlarmService服务,任务各种情况通过启动此服务来处理
  • SystemAlarmDispatcher, 任务各种情况分发处理节点,并回传无任务处理情况,销毁服务
  • CommandHander具体处理各种intent事件,以及内部任务调度完成后收尾(实现了ExecutionListener接口,其意义却不是任务已经执行完成后的处理)
  • ConstraintsCommandHandler:处理了约束情况,使用WorkConstraintsTracker检测当时状态;事件是由*Proxy类广播启动service进而通知的, ConstraintProxyUpdateReceiver控制各种约束广播是否可用
  • Alarms,使用AlramManager机制处理任务延时,并启动服务
  • RescheduleReceiver:开机或者时间事件广播,重启动WorkManager处理剩余任务的;这里只有静态注册,非所有版本有效
  • DelayMetCommandHandler:进行约束实时更新判断,并在满足执行时通过WorkManagerImpl执行任务;在类图中没有表明其实现了WorkTimer.TimeLimitExceededListener(这个是对任务从调度状态到运行状态的一个控制,10分钟未完成状态变化重新处理)
  • 使用了PowerManager.WakeLock保证调度过程中cpu运行

3.3、任务流程

任务流程涉及调度的具体过程,在以下流程图中就会直接从Schedulers方法直接异步执行启动任务;调度过程省略;

3.3.1 enqueue流程

WorkManager调用时,均是先包装成WorkContinuationImpl然后调用其方法enqueue执行;

WorkContinuationImpl: 包含了一个链表,指向了所有依赖的WorkContinuation对象;也记录了任务本身的一些信息和流程状态

流程图

  • WorkerWrapper:handleResult方法后续是对不同结果以及任务类别,对任务数据进行相应处理;其中进行了输入数据的合并;在执行中多个关键点对取消进行校验处理
  • WorkForegroundRunnable:启动前台服务,这个需要在实现Work中提供通知信息(方法getForegroundInfoAsync);如果有加急处理,也是启动前台服务
  • ListenableWorker中,通过setProgressAsync可以设置进度数据;成功时,doWork返回的结果中含有成功数据
  • EnqueueRunnable:enqueueWorkWithPrerequisites方法中对重复任务依据策略进行了处理
  • CancelWorkRunable:iterativelyCancelWorkAndDependents删除了依赖其的任务
  • 中间大量使用SettableFuture来让各个Runnable执行等待其执行的先决条件

3.3.2 cancel流程

取消时,可以通过任务的id、name、tag或者取消所有;不管是哪种调用,都是通过标识获取满足条件的任务id集合,并进行取消;其流程也只是在id集合查询时不同而已

流程图

  • WorkWrapper进行打断时,其流程也就是任务执行时打断处理,在执行过程中多处对打断进行了检测处理
  • 取消时,对调度、执行中的任务分别进行取消;同时同步状态到数据库

4、总结

上面只是大概介绍了常规使用,以及相关类图、流程,代码细节并没有展示。代码中的一些技术点需要至少会用

  • AlarmMananger
  • JobScheduler
  • 电量低、内存不足、充电状态、网络状态检测以及实时更新
  • SettableFuture
  • ......

从代码架构上,也有需要去多思考的地方

  • 各个部分的分离抽象
  • 广播、服务组件的打开关闭、以及服务的销毁、任务执行完毕后的回调收尾等性能的考量
  • 数据库表的设计
  • 不论哪个版本,均有两个调度器去执行任务,他们有可能去处理同一个任务,这带来的复杂度处理逻辑以及优势
  • ......

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们