1. 程式人生 > >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被自動停止和銷燬?


下次我們從原始碼層面探究為這兩個疑問。