Jetpack WorkManager原理解析

SystemAlarmService先来说一下WorkManager到底是干嘛的?谷歌为啥要在Jetpack组件库里面加入WorkManager,从表面意思可以理解它为工作管理器,也就是说是用来处理任务的可靠方式,比如我想在充电的时候处理一些事情,比如我想在电量充足的情况下处理一些事情,比如我想在网络连接的时候处理一些事情,又比如我想在连上wifi的时候处理一些事情,又比如我想在内存不足的时候处理一些事情。当然这些方式你用别的方法也可以实现,比如监听网络连接然后去处理事情,但是android的碎片化严重,明明是一个很小的功能,你却要写很多兼容性代码,不说现在的从5.0适配到12系统,更别说过去的版本,更有甚者厂商直接将源码修改了,所以说WorkManager最大的优势就是帮你做了系统的兼容性,可以提高你的开发效率,当然如果厂商改源码了,你还是需要去适配。

先来看一下WorkManager的一个简单的使用,现在有这么一个需求:“为了保证一个接口在网络波动比较大或者网络断开的时候仍然可以不影响它的可靠性执行”?当然在网络不好的情况下或者网络断开的情况下,这时,你去请求接口必败无疑。那么有没有一种折中的方式让它可靠一点呢,当然方法是有的,监听网络,在网络连接的时候才去真正的执行接口调用,也就是说如果马上要执行接口了,但是网络此时是断开的,那么先让执行暂停,等网络打开后再执行。那么WorkManager就可以很简单的实现这个功能。代码如下:

  val uploadConstraints = Constraints.Builder()
                                .setRequiredNetworkType(NetworkType.CONNECTED).build()
                            val objString = Gson().toJson(bean)
                            val inputData = Data.Builder().putString("data", objString).build()
                            val uploadTask = OneTimeWorkRequest.Builder(UploadWorker::class.java)
                                .setConstraints(uploadConstraints).addTag("uploadConstraints")
                                .setInputData(inputData)
                                .build()
                            WorkManager.getInstance(BaseApplication.getBaseContext())
                                .enqueue(uploadTask)

其中Constraints是用来加入限制条件OneTimeWorkRequest是执行一次的请求,当然还有自带请求多次的类,其中UploadWorker类需要开发者自己去扩展执行哪些工作,代码如下:

class UploadWorker(context: Context, params: WorkerParameters): Worker(context, params) {
    override fun doWork(): Result {
        Log.i("huoying","执行UploadWorker")
      
        return Result.success()
    }

}

开发人员需要复写doWork方法,关于WorkManager的其他用法请参考WorkManager使用方法

接下来进入正题WorkManger是怎么执行的呢?接下来就从网络连接时执行任务为切入点,加入队列执行任务的方法是enqueue方法,而真正执行的类是WorkManagerImpl类,实现代码如下:

public Operation enqueue(
            @NonNull List<? extends WorkRequest> requests) {

        // This error is not being propagated as part of the Operation, as we want the
        // app to crash during development. Having no workRequests is always a developer error.
        if (requests.isEmpty()) {
            throw new IllegalArgumentException(
                    "enqueue needs at least one WorkRequest.");
        }
        return new WorkContinuationImpl(this, requests).enqueue();
    }

真正的实现类为WorkContinuationImpl,看一下

 public @NonNull Operation enqueue() {
        // Only enqueue if not already enqueued.
        if (!mEnqueued) {
            // The runnable walks the hierarchy of the continuations
            // and marks them enqueued using the markEnqueued() method, parent first.
            EnqueueRunnable runnable = new EnqueueRunnable(this);
            mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
            mOperation = runnable.getOperation();
        } else {
            Logger.get().warning(TAG,
                    String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
        }
        return mOperation;
    }

真正的实现是在EnqueueRunnable的的run方法里面实现的,如下:

public void run() {
        try {
            if (mWorkContinuation.hasCycles()) {
                throw new IllegalStateException(
                        String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
            }
            boolean needsScheduling = addToDatabase();
            if (needsScheduling) {
                // Enable RescheduleReceiver, only when there are Worker's that need scheduling.
                final Context context =
                        mWorkContinuation.getWorkManagerImpl().getApplicationContext();
                PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                scheduleWorkInBackground();
            }
            mOperation.setState(Operation.SUCCESS);
        } catch (Throwable exception) {
            mOperation.setState(new Operation.State.FAILURE(exception));
        }
    }

其中addToDatabase方法是先把任务信息插入到数据库中,这也就保证了app就算重新启动,没有执行的任务也可以可靠的执行,只要达到触发条件,插入数据库之后就会执行scheduleworkInBackground方法从数据库中取出请求,实现如下:

  public static void schedule(
            @NonNull Configuration configuration,
            @NonNull 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 {
            // Enqueued workSpecs when scheduling limits are applicable.
            eligibleWorkSpecsForLimitedSlots = workSpecDao.getEligibleWorkForScheduling(
                    configuration.getMaxSchedulerLimit());

            // Enqueued workSpecs when scheduling limits are NOT applicable.
            allEligibleWorkSpecs = workSpecDao.getAllEligibleWorkSpecsForScheduling(
                    MAX_GREEDY_SCHEDULER_LIMIT);

            if (eligibleWorkSpecsForLimitedSlots != null
                    && eligibleWorkSpecsForLimitedSlots.size() > 0) {
                long now = System.currentTimeMillis();

                // Mark all the WorkSpecs as scheduled.
                // Calls to Scheduler#schedule() could potentially result in more schedules
                // on a separate thread. Therefore, this needs to be done first.
                for (WorkSpec workSpec : eligibleWorkSpecsForLimitedSlots) {
                    workSpecDao.markWorkSpecScheduled(workSpec.id, now);
                }
            }
            workDatabase.setTransactionSuccessful();
        } finally {
            workDatabase.endTransaction();
        }

        if (eligibleWorkSpecsForLimitedSlots != null
                && eligibleWorkSpecsForLimitedSlots.size() > 0) {

            WorkSpec[] eligibleWorkSpecsArray =
                    new WorkSpec[eligibleWorkSpecsForLimitedSlots.size()];
            eligibleWorkSpecsArray =
                    eligibleWorkSpecsForLimitedSlots.toArray(eligibleWorkSpecsArray);

            // Delegate to the underlying schedulers.
            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);
            // Delegate to the underlying schedulers.
            for (Scheduler scheduler : schedulers) {
                if (!scheduler.hasLimitedSchedulingSlots()) {
                    scheduler.schedule(enqueuedWorkSpecsArray);
                }
            }
        }
    }

这个方法的意思就是从数据库中取出请求数据,然后判断请求数据的条件是否满足,如果满足直接执行,如果不满足咋暂时不执行,只有达到条件的时候才去执行,例如上面说的从网络为切入点,当网络连接上的时候再去执行请求,那么肯定会有监听网络变化的话广播,那么可以推测这个框架肯定有这个功能,先找到这个框架的清单文件,如下:

 <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" />
        </provider>

        <service
            android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
            android:directBootAware="false"
            android:enabled="@bool/enable_system_alarm_service_default"
            android:exported="false"
            tools:targetApi="n" />
        <service
            android:name="androidx.work.impl.background.systemjob.SystemJobService"
            android:directBootAware="false"
            android:enabled="@bool/enable_system_job_service_default"
            android:exported="true"
            android:permission="android.permission.BIND_JOB_SERVICE"
            tools:targetApi="n" />
        <service
            android:name="androidx.work.impl.foreground.SystemForegroundService"
            android:directBootAware="false"
            android:enabled="@bool/enable_system_foreground_service_default"
            android:exported="false"
            tools:targetApi="n" />

        <receiver
            android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
            android:directBootAware="false"
            android:enabled="true"
            android:exported="false"
            tools:targetApi="n" />
        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
                <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.intent.action.BATTERY_OKAY" />
                <action android:name="android.intent.action.BATTERY_LOW" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
                <action android:name="android.intent.action.DEVICE_STORAGE_OK" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.TIME_SET" />
                <action android:name="android.intent.action.TIMEZONE_CHANGED" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
            android:directBootAware="false"
            android:enabled="@bool/enable_system_alarm_service_default"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="androidx.work.impl.background.systemalarm.UpdateProxies" />
            </intent-filter>
        </receiver>
        <receiver
            android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver"
            android:directBootAware="false"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.DUMP"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="androidx.work.diagnostics.REQUEST_DIAGNOSTICS" />
            </intent-filter>
        </receiver>

它里面声明了很多广播和服务,其中网络监听的广播为:

  <receiver
            android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
            android:directBootAware="false"
            android:enabled="false"
            android:exported="false"
            tools:targetApi="n" >
            <intent-filter>
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
        </receiver>

实现如下:

  public void onReceive(Context context, Intent intent) {
        Logger.get().debug(TAG, String.format("onReceive : %s", intent));
        Intent constraintChangedIntent = CommandHandler.createConstraintsChangedIntent(context);
        context.startService(constraintChangedIntent);
    }

真正实现是在服务里面实现的,也就是SystemAlarmService类,如下:

public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        if (mIsShutdown) {
            Logger.get().info(TAG,
                    "Re-initializing SystemAlarmDispatcher after a request to shut-down.");

            // Destroy the old dispatcher to complete it's lifecycle.
            mDispatcher.onDestroy();
            // Create a new dispatcher to setup a new lifecycle.
            initializeDispatcher();
            // Set mIsShutdown to false, to correctly accept new commands.
            mIsShutdown = false;
        }

        if (intent != null) {
            mDispatcher.add(intent, startId);
        }

        // If the service were to crash, we want all unacknowledged Intents to get redelivered.
        return Service.START_REDELIVER_INTENT;
    }
 private void processCommand() {
        assertMainThread();
        PowerManager.WakeLock processCommandLock =
                WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG);
        try {
            processCommandLock.acquire();
            // Process commands on the background thread.
            mWorkManager.getWorkTaskExecutor().executeOnBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    synchronized (mIntents) {
                        mCurrentIntent = mIntents.get(0);
                    }

                    if (mCurrentIntent != null) {
                        final String action = mCurrentIntent.getAction();
                        final int startId = mCurrentIntent.getIntExtra(KEY_START_ID,
                                DEFAULT_START_ID);
                        Logger.get().debug(TAG,
                                String.format("Processing command %s, %s", mCurrentIntent,
                                        startId));
                        final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
                                mContext,
                                String.format("%s (%s)", action, startId));
                        try {
                            Logger.get().debug(TAG, String.format(
                                    "Acquiring operation wake lock (%s) %s",
                                    action,
                                    wakeLock));

                            wakeLock.acquire();
                            mCommandHandler.onHandleIntent(mCurrentIntent, startId,
                                    SystemAlarmDispatcher.this);
                        } catch (Throwable throwable) {
                            Logger.get().error(
                                    TAG,
                                    "Unexpected error in onHandleIntent",
                                    throwable);
                        }  finally {
                            Logger.get().debug(
                                    TAG,
                                    String.format(
                                            "Releasing operation wake lock (%s) %s",
                                            action,
                                            wakeLock));
                            wakeLock.release();
                            // Check if we have processed all commands
                            postOnMainThread(
                                    new DequeueAndCheckForCompletion(SystemAlarmDispatcher.this));
                        }
                    }
                }
            });
        } finally {
            processCommandLock.release();
        }
    }

SystemAlarmService通过SystemAlarmDispatcher触发processCommand方法来真正的处理请求,然后调用CommandHandler的handleScheduleWorkIntent方法取出任务去执行任务,如下:

  private void handleScheduleWorkIntent(
            @NonNull Intent intent,
            int startId,
            @NonNull SystemAlarmDispatcher dispatcher) {

        Bundle extras = intent.getExtras();
        String workSpecId = extras.getString(KEY_WORKSPEC_ID);
        Logger.get().debug(TAG, String.format("Handling schedule work for %s", workSpecId));

        WorkManagerImpl workManager = dispatcher.getWorkManager();
        WorkDatabase workDatabase = workManager.getWorkDatabase();
        workDatabase.beginTransaction();

        try {
            WorkSpecDao workSpecDao = workDatabase.workSpecDao();
            WorkSpec workSpec = workSpecDao.getWorkSpec(workSpecId);

            // It is possible that this WorkSpec got cancelled/pruned since this isn't part of
            // the same database transaction as marking it enqueued (for example, if we using
            // any of the synchronous operations).  For now, handle this gracefully by exiting
            // the loop.  When we plumb ListenableFutures all the way through, we can remove the
            // *sync methods and return ListenableFutures, which will block on an operation on
            // the background task thread so all database operations happen on the same thread.
            // See b/114705286.
            if (workSpec == null) {
                Logger.get().warning(TAG,
                        "Skipping scheduling " + workSpecId + " because it's no longer in "
                        + "the DB");
                return;
            } else if (workSpec.state.isFinished()) {
                // We need to schedule the Alarms, even when the Worker is RUNNING. This is because
                // if the process gets killed, the Alarm is necessary to pick up the execution of
                // Work.
                Logger.get().warning(TAG,
                        "Skipping scheduling " + workSpecId + "because it is finished.");
                return;
            }

            // Note: The first instance of PeriodicWorker getting scheduled will set an alarm in the
            // past. This is because periodStartTime = 0.
            long triggerAt = workSpec.calculateNextRunTime();

            if (!workSpec.hasConstraints()) {
                Logger.get().debug(TAG,
                        String.format("Setting up Alarms for %s at %s", workSpecId, triggerAt));
                Alarms.setAlarm(mContext, dispatcher.getWorkManager(), workSpecId, triggerAt);
            } else {
                // Schedule an alarm irrespective of whether all constraints matched.
                Logger.get().debug(TAG,
                        String.format("Opportunistically setting an alarm for %s at %s", workSpecId,
                                triggerAt));
                Alarms.setAlarm(
                        mContext,
                        dispatcher.getWorkManager(),
                        workSpecId,
                        triggerAt);

                // Schedule an update for constraint proxies
                // This in turn sets enables us to track changes in constraints
                Intent constraintsUpdate = CommandHandler.createConstraintsChangedIntent(mContext);
                dispatcher.postOnMainThread(
                        new SystemAlarmDispatcher.AddRunnable(
                                dispatcher,
                                constraintsUpdate,
                                startId));
            }

            workDatabase.setTransactionSuccessful();
        } finally {
            workDatabase.endTransaction();
        }
    }

总结:其实WorkManager就是一个封装了监听广播监听电量监听内存等等的框架,然后将任务保存到数据库,在满足条件时从数据库取出并执行,从而达到任务的可靠性执行。

上一篇:解决文件夹如何取消git版本控制


下一篇:「RocketMQ技术专题」帮你梳理RocketMQ/Kafka的选择理由及二者PK