JobService源码探究之 onStartJob()里如何优雅地处理耗时逻辑?

首先我们要思考如下两个问题。


思考一



如果我们在onStartJob()里处理耗时逻辑,导致onStartJob()没有及时返回给JobSchedulerContext。
最终结果是怎么样?


是ANR?
还是因为超时,该Job可能被强制停止和销毁?


思考二



如果onStartJob()里起了新线程处理耗时逻辑,但是返回值返回了false,那么系统还会销毁Job吗?

如果会的话,新线程是否会导致内存泄漏?


针对思考一,我们对DEMO的JobService的代码做下修改。

让UI线程睡眠60s。


public class Helpers {
    ...
    public static void doHardWork(JobService job, JobParameters params) {
        ...
        try {
            Log.w(TAG, "Helpers doHardWork() starting sleep");
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.w(TAG, "Helpers doHardWork() sleep finished");
    }
}


运行下DEMO,查看效果。


02-06 10:46:55.384 16428 16428 W Ellison : MainActivity onClick_Schedule()
02-06 10:46:55.384 16428 16428 W Ellison : Helpers schedule()
02-06 10:46:55.388 16428 16428 W Ellison : EllisonsJobService onCreate()
02-06 10:46:55.395 16428 16428 W Ellison : EllisonsJobService onStartJob()
02-06 10:46:55.395 16428 16428 W Ellison : Helpers doHardWork()
02-06 10:46:55.395 16428 16428 W Ellison : Helpers doHardWork() starting sleep

线程刚开始休眠了,同时,我们点击job finished button。
看看会有什么现象。


结果DEMO发生了ANR。

adb logcat -s ActivityManager取下ANR的log。


02-06 10:47:25.511  1433  1527 E ActivityManager: ANR in com.example.timeapidemo (com.example.timeapidemo/.MainActivity)
02-06 10:47:25.511  1433  1527 E ActivityManager: PID: 16428
02-06 10:47:25.511  1433  1527 E ActivityManager: Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.  Wait queue length: 2.  Wait queue head age: 14676.9ms.)
02-06 10:47:25.511  1433  1527 E ActivityManager: Load: 9.26 / 8.6 / 8.39
02-06 10:47:25.511  1433  1527 E ActivityManager: CPU usage from 73968ms to -1ms ago (2018-02-06 10:46:06.894 to 2018-02-06 10:47:20.863):
02-06 10:47:25.511  1433  1527 E ActivityManager:   7.9% 8123/com.github.shadowsocks: 5% user + 2.9% kernel / faults: 27 minor


发现button响应时间过长,导致了ANR的发生。
这个时候我们点击ANR对话框上的wait,等待button继续响应。
等待一段时间后,我们继续查看log。


02-06 10:47:55.396 16428 16428 W Ellison : Helpers doHardWork() sleep finished
02-06 10:47:55.413 16428 16428 W Ellison : EllisonsJobService destroyed.★
02-06 10:47:55.416 16428 16428 W Ellison : MainActivity onClick_Finished()
02-06 10:47:55.416 16428 16428 W Ellison : Helpers jobFinished()


发现60s后UI线程睡眠结束后,竟然自行销毁了。
我们点击的job finished button的响应还没来得及处理,job就已经结束了。


那这个现象到底是不是因为我们点击了job finished button导致的或者ANR导致的呢。


这时候我们再次点击schdule job的button,让job跑起来,但这次我们默默等待UI线程睡眠结束。
再次收集下log。

02-06 10:55:21.893 16428 16428 W Ellison : MainActivity onClick_Schedule()
02-06 10:55:21.893 16428 16428 W Ellison : Helpers schedule()
02-06 10:55:21.900 16428 16428 W Ellison : EllisonsJobService onCreate()
02-06 10:55:21.902 16428 16428 W Ellison : EllisonsJobService onStartJob()
02-06 10:55:21.902 16428 16428 W Ellison : Helpers doHardWork()
02-06 10:55:21.902 16428 16428 W Ellison : Helpers doHardWork() starting sleep
02-06 10:56:21.903 16428 16428 W Ellison : Helpers doHardWork() sleep finished
02-06 10:56:21.909 16428 16428 W Ellison : EllisonsJobService destroyed.

发现还是一样的结果,UI线程睡眠结束后,我们的Job被自行销毁了。


这里留个疑问,思考下为什么线程睡眠一段时间后job被自行销毁了。待会儿我们探究这个处理的缘由。


根据以上的尝试,我们已经可以得出一些阶段性的结论。


onStartJob()里直接执行耗时逻辑的话,如果这时候操作UI可能会导致ANR。
如果不操作UI,耗时逻辑执行完成后,Job将被销毁。



为了防止UI线程的耗时逻辑造成ANR或者Job被销毁,那我们在doHardWork里新起个线程,把睡眠逻辑放到线程里。
代码如下。


public class Helpers {
    ...
    public static void doHardWork(JobService job, JobParameters params) {
        ...
        new Thread (new Runnable() {
            public void run() {
                try {
                    Log.w(TAG, "Helpers doHardWork() starting sleep");
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.w(TAG, "Helpers doHardWork() sleep finished");
            }
        }).start();
    }
}


DEMO运行起来后,我们点击schedule button让Job跑起来。
同时我们不断点击enqueue button,这个button里具体没做什么实际处理。只是判断一下能否及时响应。


02-06 11:14:56.065 22200 22200 W Ellison : MainActivity onClick_Schedule()
02-06 11:14:56.066 22200 22200 W Ellison : Helpers schedule()
02-06 11:14:56.079 22200 22200 W Ellison : EllisonsJobService onCreate()
02-06 11:14:56.082 22200 22200 W Ellison : EllisonsJobService onStartJob()
02-06 11:14:56.082 22200 22200 W Ellison : Helpers doHardWork()
02-06 11:14:56.083 22200 22223 W Ellison : Helpers doHardWork() starting sleep
02-06 11:15:02.982 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:02.983 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:03.213 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:03.214 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:03.441 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:03.442 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:03.672 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:03.672 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:03.902 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:03.902 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:04.142 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:04.142 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:04.385 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:04.385 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:04.653 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:04.653 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:04.894 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:04.894 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:05.135 22200 22200 W Ellison : MainActivity onClick_Enqueue()
02-06 11:15:05.135 22200 22200 W Ellison : Helpers enqueueJob()
02-06 11:15:56.086 22200 22223 W Ellison : Helpers doHardWork() sleep finished


上面的log看出,Job里的新线程睡眠的过程中不管点击多少次enqueue button,UI都能及时响应。不会发生ANR。


等待一段时间后,新线程睡眠结束后,Job并没有被销毁。


那新线程里执行的耗时逻辑是不是无限长呢?我们现在不知道答案,但估摸着肯定不是无限长。


我们把新线程的睡眠时间调成10min,就是600000ms。
再运行下DEMO。看下log。


public class Helpers {
    ...
    public static void doHardWork(JobService job, JobParameters params) {
        ...
        new Thread (new Runnable() {
            public void run() {
                try {
                    Log.w(TAG, "Helpers doHardWork() starting sleep");
                    Thread.sleep(600000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.w(TAG, "Helpers doHardWork() sleep finished");
            }
        }).start();
    }
}

02-06 11:50:44.957 23218 23218 W Ellison : MainActivity onClick_Schedule()
02-06 11:50:44.958 23218 23218 W Ellison : Helpers schedule()
02-06 11:50:44.974 23218 23218 W Ellison : EllisonsJobService onCreate()
02-06 11:50:44.981 23218 23218 W Ellison : EllisonsJobService onStartJob()
02-06 11:50:44.981 23218 23218 W Ellison : Helpers doHardWork()
02-06 11:50:44.982 23218 23244 W Ellison : Helpers doHardWork() starting sleep
02-06 12:00:44.985 23218 23244 W Ellison : Helpers doHardWork() sleep finished
02-06 12:00:44.987 23218 23218 W Ellison : EllisonsJobService stopped andandroid.app.job.JobParameters@8b7dbc2 reason:3 ★
02-06 12:00:45.001 23218 23218 W Ellison : EllisonsJobService destroyed.


发现耗时逻辑刚处理完毕,还没等到我们自行finish job,Job就被强制停止了。
而且★显示被停止的数值为3,其定义在JobParameters中。


    public static final int REASON_TIMEOUT = 3;

我们猜测JobScheduler察觉我们的Job后台执行了较长时间还没有自行调用jobFinished方法。
系统自动停止并销毁了我们的Job。


如果我们把休眠时间加上1s,就是休眠10min1s。看下log。


02-06 12:11:17.963 W/Ellison (23876): MainActivity onClick_Schedule()
02-06 12:11:17.963 W/Ellison (23876): Helpers schedule()
02-06 12:11:17.988 W/Ellison (23876): EllisonsJobService onCreate()
02-06 12:11:17.992 W/Ellison (23876): EllisonsJobService onStartJob()
02-06 12:11:17.992 W/Ellison (23876): Helpers doHardWork()
02-06 12:11:17.993 W/Ellison (23876): Helpers doHardWork() starting sleep

02-06 12:21:17.994 I/JobServiceContext( 1433): Client timed out while executing (no jobFinished received), sending onStop: 5673577 #u0a174/0 com.example.timeapidemo/.EllisonsJobService
02-06 12:21:17.995 W/Ellison (23876): EllisonsJobService stopped andandroid.app.job.JobParameters@5284e28 reason:3
02-06 12:21:17.998 W/Ellison (23876): EllisonsJobService destroyed.

02-06 12:21:18.994 W/Ellison (23876): Helpers doHardWork() sleep finished


发现还没等后台Job执行完休眠处理,Job就被停止和销毁了。
等JobService销毁1s后,后台线程才完成了休眠。
而且停止的原因一样,也是TIMEOUT。【查看源码我们知道Job执行的超时限制就是10min】


到这里,我们又可以得出一个结论。
就是onStartJob()里开启的工作线程存在10min的超时限制,不可以无休止地执行耗时逻辑。
10min一到,不论工作线程是否结束,Job都将被强制停止和销毁。



同时,我们不禁要引发思考,JobScheduler社么设计的证据在哪?这么设计的理由是什么?我们暂且把它当作疑问2。


上面还有一个思考二。



如果onStartJob()里起了新线程处理耗时逻辑,但是返回值返回了false,那么系统还会销毁Job吗?
如果会的话,新线程是否会导致内存泄漏?


我们修改下代码。将onStartJob的返回值改为false。
doHardWork里的耗时逻辑改回到休眠6s。


public class EllisonsJobService extends JobService {
    ...
    @Override
    public boolean onStartJob(JobParameters params) {
        Log.w(TAG, "EllisonsJobService onStartJob()");
        Helpers.doHardWork(this, params);

        return false;
    }
}

public class Helpers {
    ...
    public static void doHardWork(JobService job, JobParameters params) {
        ...
        new Thread (new Runnable() {
            public void run() {
                try {
                    Log.w(TAG, "Helpers doHardWork() starting sleep");
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.w(TAG, "Helpers doHardWork() sleep finished");
            }
        }).start();
    }
}


再次运行DEMO,收集log。


02-06 12:41:18.034 24541 24541 W Ellison : MainActivity onClick_Schedule()
02-06 12:41:18.035 24541 24541 W Ellison : Helpers schedule()
02-06 12:41:18.045 24541 24541 W Ellison : EllisonsJobService onCreate()
02-06 12:41:18.047 24541 24541 W Ellison : EllisonsJobService onStartJob()
02-06 12:41:18.047 24541 24541 W Ellison : Helpers doHardWork()
02-06 12:41:18.048 24541 24575 W Ellison : Helpers doHardWork() starting sleep
02-06 12:41:18.051 24541 24541 W Ellison : EllisonsJobService destroyed.
02-06 12:41:24.051 24541 24575 W Ellison : Helpers doHardWork() sleep finished


发现虽然后台任务还在继续,Job就被强制销毁了。
如果我们不在JobService的onDestroy()里释放掉线程的话,会造成内存泄漏。


话说回来,起后台任务的同时告诉系统任务已经完成了,这是一种逻辑上就说不通的处理。
也就是说多度探讨的意义并不大。
我们只要知道这样的写法Job会被立即销毁,同时造成内存泄漏。


根据以上的两个思考的验证,我们得出了不少结论,可以适当做些总结。


总结一

onStartJob()返回false的话,无论后台任务是否完成,该JobService都将被强制销毁。


总结二

onStartJob()里直接执行耗时逻辑的话,如果操作了UI会导致ANR。
如果不操作UI,等耗时逻辑完了后,该JobService会被强制停止和销毁。


总结三

onStartJob()里新建工作线程执行后台逻辑的话,可以解决同时操作UI造成ANR的问题。


总结四

onStartJob()新建工作线程执行后台逻辑的时间存在10min的限制,即便任务没有完成JobService也会被强制停止和销毁。


回到我们的标题上来,如何优雅地在onStartJob()里执行任务逻辑?


根据上面的总结,我们可以得到如下启发。
按照使用场景的不同,执行任务逻辑的方式也不同。


◆后台执行简单的任务的场景

onStartJob()里直接执行该任务并返回false,通知JobScheduler可以立即销毁我的Job。
比如:发送IDLE状态变化的广播


◆后台执行耗时任务的场景

onStartJob()里新建工作线程执行耗时逻辑并返回true,通知JobScheduler我还在执行任务,不要销毁我的Job。
等后台线程完成后自行调用jobFinished()通知JobScheduler可以立即销毁我的Job。
比如:简单的网络请求


◆后台执行无法预估处理时间的耗时任务的场景

为了防止后台的任务超时,除了在onStartJob()里启动工作线程执行耗时逻辑并返回true外,还需要在onStopJob()里加入
如下逻辑。
1.结束我们的后台线程,回收资源等等
2.保存本次任务的状态和临时文件
3.返回true,让系统再度启动我们的任务。
4.当任务再度启动后,读取上次任务的状态和临时文件继续完成未完的处理
比如:耗时的下载任务


上面还残留着两个疑问。


疑问一

为什么onStartJob()直接执行耗时逻辑后,即便自己没有finish该Job,但是Job还是会被自动销毁?


疑问二

为什么onStartJob()里开启新线程执行的耗时逻辑超过10min,但是Job被自动停止和销毁?


下次我们从源码层面探究为这两个疑问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值