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

相关文章
|
Java API 调度
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
1377 0
|
安全 Java Android开发
Rockchip系列之客制化GPIO接口jni+service接口访问(4)
Rockchip系列之客制化GPIO接口jni+service接口访问(4)
225 0
|
域名解析 SEO 搜索推荐
网络基础知识之————A记录和CNAME记录的区别
1、什么是域名解析? 域名解析就是国际域名或者国内域名以及中文域名等域名申请后做的到IP地址的转换过程。IP地址是网路上标识您站点的数字地址,为了简单好记,采用域名来代替ip地址标识站点地址。域名的解析工作由DNS服务器完成。
11740 1
|
人工智能 自然语言处理 机器人
智能语音机器人底层系统设计逻辑机器人源码系统逻辑
简介: — 1 —智能客服背景智能语音客服机器人是在传统的客服系统基础上,集成了语音识别、语义理解、知识图谱、深度学习等多项智能交互技术,能准确理解用户的意图或提问,再根据丰富的内容和海量知识图谱,给予用户满意的回答。目前已广泛应用于金融、保险、汽车、房产、电商、政府等多个领域。
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
640 58
|
负载均衡 算法 前端开发
Spring Cloud Alibaba-负载均衡
Spring Cloud Alibaba-负载均衡
Spring Cloud Alibaba-负载均衡
|
XML 数据格式 Python
Python技巧:将HTML实体代码转换为文本的方法
在选择方法时,考虑到实际的应用场景和需求是很重要的。通常,使用标准库的 `html`模块就足以满足大多数基本需求。对于复杂的HTML文档处理,则可能需要 `BeautifulSoup`。而在特殊场合,或者为了最大限度的控制和定制化,可以考虑正则表达式。
616 12
|
网络协议 Windows
电脑ip在哪里查看?windows系统查看ip地址的8种方法
在Windows系统中,有多种方法可以查看电脑的IP地址。
7344 2
|
SQL 移动开发 前端开发
基于jeecg-boot的任务甘特图显示
基于jeecg-boot的任务甘特图显示
304 0
|
Web App开发 Android开发 ice
【Android App】给App集成WebRTC实现视频发送和接受实战(附源码和演示 超详细)
【Android App】给App集成WebRTC实现视频发送和接受实战(附源码和演示 超详细)
1771 2