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

简介: 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被自动停止和销毁?

相关文章
|
3月前
|
存储
hyengine 优化问题之代码耗时如何解决
hyengine 优化问题之代码耗时如何解决
|
4月前
|
缓存 Java
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
|
4月前
|
运维 中间件 数据库
浅析JAVA日志中的性能实践与原理解释问题之元信息打印会导致性能急剧下降问题如何解决
浅析JAVA日志中的性能实践与原理解释问题之元信息打印会导致性能急剧下降问题如何解决
|
4月前
|
存储 Java
浅析JAVA日志中的性能实践与原理解释问题之测试日志内容大小对系统性能的影响问题如何解决
浅析JAVA日志中的性能实践与原理解释问题之测试日志内容大小对系统性能的影响问题如何解决
110 0
|
6月前
|
分布式计算 并行计算 算法
图计算中的性能优化有哪些方法?请举例说明。
图计算中的性能优化有哪些方法?请举例说明。
53 0
|
Java 编译器 应用服务中间件
代码开发优化细节
带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50% 。
209 2
代码开发优化细节
|
测试技术
代码为啥不能过度优化
代码为啥不能过度优化
77 0
|
开发工具
微信小游戏开发实战5-重复执行和逻辑循环的区别
本篇主要内容包括了解帧的概念,以及理解重复执行和逻辑循环这两种循环积木块之间的区别。 如果你没有任何的游戏开发经验,欢迎阅读我的“人人都能做游戏”系列教程,它会手把手的教你做出自己的第一个小游戏。
112 0
|
缓存 Java 编译器
图解JVM整体结构、执行流程以及2种架构模型,你学会了吗?
HotSpot VM 是目前市面上高性能虚拟机的代表作之一。 方法区和堆:多线程共享 虚拟机栈、本地方法栈、程序计数器:每个线程独有一份 执行引擎:包含三部分:解释器,及时编译器(后端编译器),垃圾回收器 它采用解释器与即时编译器并存的架构。 在今天,Java 程序的运行性能早已脱胎换骨,已经达到了可以和 C/C++ 程序一较高下的地步。
|
前端开发
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)