首先我们要思考如下两个问题。
思考一
如果我们在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被自动停止和销毁?