關於計時的相關知識點

注:本篇文章內容爲複製的,由於時間過久,文章地址忘記了,特注非原創


一、Timer TimerTask

參考Java中的Timer和TimerTask在Android中的用法

    在開發中我們有時會有這樣的需求,即在固定的每隔一段時間執行某一個任務。比如UI上的控件需要隨着時間改變,我們可以使用Java爲我們提供的計時器的工具類,即Timer和TimerTask。

Timer是一個普通的類,其中有幾個重要的方法;而TimerTask則是一個抽象類,其中有一個抽象方法run(),類似線程中的run()方法,我們使用Timer創建一個他的對象,然後使用這對象的schedule方法來完成這種間隔的操作。

//true 說明這個timer以daemon方式運行(優先級低,程序結束timer也自動結束)
java.util.Timer timer = new java.util.Timer(true);

TimerTask task = new TimerTask() {
   public void run() {
   //每次需要執行的代碼放到這裏面。   
   }   
};

//以下是幾種調度task的方法:

//time爲Date類型:在指定時間執行一次。
timer.schedule(task, time);

//firstTime爲Date類型,period爲long,表示從firstTime時刻開始,每隔period毫秒執行一次。
timer.schedule(task, firstTime, period);   

//delay 爲long類型:從現在起過delay毫秒執行一次。
timer.schedule(task, delay);

//delay爲long,period爲long:從現在起過delay毫秒以後,每隔period毫秒執行一次。
timer.schedule(task, delay, period);

//該任務每隔2秒更新主線程的UI(在主線程的TextView顯示最新的系統時間System.currentTimeMillis())。
package zhangphil.timertask;

import java.util.Timer;
import java.util.TimerTask;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.app.Activity;
import android.widget.TextView;

public class MainActivity extends Activity {

    private Timer timer;
    private TimerTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView tv = (TextView) findViewById(R.id.textView);

        final int WHAT = 102;
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                case WHAT:
                    tv.setText(msg.obj + "");
                    break;
                }
            }
        };

        task = new TimerTask() {
            @Override
            public void run() {
                Message message = new Message();
                message.what = WHAT;
                message.obj = System.currentTimeMillis();
                handler.sendMessage(message);
            }
        };

        timer = new Timer();
        // 參數:
        // 1000,延時1秒後執行。
        // 2000,每隔2秒執行1次task。
        timer.schedule(task, 1000, 2000);
    }

    @Override
    protected void onStop() {
        super.onStop();

        // 暫停
        // timer.cancel();
        // task.cancel();
    }
}

schedule方法有三個參數
第一個參數就是TimerTask類型的對象,我們實現TimerTask的run()方法就是要週期執行的一個任務;
第二個參數有兩種類型,第一種是long類型,表示多長時間後開始執行,另一種是Date類型,表示從那個時間後開始執行;
第三個參數就是執行的週期,爲long類型。

schedule方法還有一種兩個參數的執行重載,第一個參數仍然是TimerTask,第二個表示爲long的形式表示多長時間後執行一次,爲Date就表示某個時間後執行一次。

Timer就是一個線程,使用schedule方法完成對TimerTask的調度,多個TimerTask可以共用一個Timer,也就是說Timer對象調用一次schedule方法就是創建了一個線程,並且調用一次schedule後TimerTask是無限制的循環下去的,使用Timer的cancel()停止操作。當然同一個Timer執行一次cancel()方法後,所有Timer線程都被終止。

若要在TimerTask中更新主線程UI,鑑於Android編程模型不允許在非主線程中更新主線程UI,因此需要結合Android的Handler實現在Java的TimerTask中更新主線程UI。
二、ScheduledThreadPoolExecutor

    public static ExecutorService newScheduledThreadPool(int corePoolSize){
      return new ThreadPoolExecutor(corePoolSize,Integer.MAX_VALUE,0L,TimeUnit.SECONDS,new DelayedWorkQueue<Runnable>());
    }

    ScheduledThreadPoolExecutor核心線程數量固定,非核心線程數沒有限制。主要用於執行定時任務和具有固定週期的重複任務。

參考Java 併發專題 : Timer的缺陷 用ScheduledExecutorService替代
以前在項目中也經常使用定時器,比如每隔一段時間清理項目中的一些垃圾文件,每個一段時間進行數據清洗;然而Timer是存在一些缺陷的,因爲Timer在執行定時任務時只會創建一個線程,所以如果存在多個任務,且任務時間過長,超過了兩個任務的間隔時間,會發生一些缺陷.

1.定義了兩個任務,預計是第一個任務1s後執行,第二個任務3s後執行

package com.zhy.concurrency.timer;

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest
{
    private static long start;

    public static void main(String[] args) throws Exception
    {

        TimerTask task1 = new TimerTask()
        {
            @Override
            public void run()
            {

                System.out.println("task1 invoked ! "
                        + (System.currentTimeMillis() - start));
                try
                {
                    Thread.sleep(3000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }

            }
        };
        TimerTask task2 = new TimerTask()
        {
            @Override
            public void run()
            {
                System.out.println("task2 invoked ! "
                        + (System.currentTimeMillis() - start));
            }
        };
        Timer timer = new Timer();
        start = System.currentTimeMillis();
        timer.schedule(task1, 1000);
        timer.schedule(task2, 3000);

    }
}

運行結果:

task1 invoked ! 1000
task2 invoked ! 4000

task2實際上是4後才執行,正因爲Timer內部是一個線程,而任務1所需的時間超過了兩個任務間的間隔導致。下面使用ScheduledThreadPool解決這個問題:

package com.zhy.concurrency.timer;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExecutorTest
{
    private static long start;

    public static void main(String[] args)
    {
        /**
         * 使用工廠方法初始化一個ScheduledThreadPool
         */
        ScheduledExecutorService newScheduledThreadPool = Executors
                .newScheduledThreadPool(2);

        TimerTask task1 = new TimerTask()
        {
            @Override
            public void run()
            {
                try
                {

                    System.out.println("task1 invoked ! "
                            + (System.currentTimeMillis() - start));
                    Thread.sleep(3000);
                } catch (Exception e)
                {
                    e.printStackTrace();
                }

            }
        };

        TimerTask task2 = new TimerTask()
        {
            @Override
            public void run()
            {
                System.out.println("task2 invoked ! "
                        + (System.currentTimeMillis() - start));
            }
        };
        start = System.currentTimeMillis();
        newScheduledThreadPool.schedule(task1, 1000, TimeUnit.MILLISECONDS);
        newScheduledThreadPool.schedule(task2, 3000, TimeUnit.MILLISECONDS);
    }
}

2.如果TimerTask拋出RuntimeException,Timer會停止所有任務的運行:

package com.zhy.concurrency.timer;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;


public class ScheduledThreadPoolDemo01
{


    public static void main(String[] args) throws InterruptedException
    {

        final TimerTask task1 = new TimerTask()
        {

            @Override
            public void run()
            {
                throw new RuntimeException();
            }
        };

        final TimerTask task2 = new TimerTask()
        {

            @Override
            public void run()
            {
                System.out.println("task2 invoked!");
            }
        };

        Timer timer = new Timer();
        timer.schedule(task1, 100);
        timer.scheduleAtFixedRate(task2, new Date(), 1000);



    }
}

上面有兩個任務,任務1拋出一個運行時的異常,任務2週期性的執行某個操作,輸出結果:

task2 invoked!
Exception in thread "Timer-0" java.lang.RuntimeException
    at com.zhy.concurrency.timer.ScheduledThreadPoolDemo01$1.run(ScheduledThreadPoolDemo01.java:24)
    at java.util.TimerThread.mainLoop(Timer.java:512)
    at java.util.TimerThread.run(Timer.java:462)

由於任務1的一次,任務2也停止運行了。。。下面使用ScheduledExecutorService解決這個問題:

package com.zhy.concurrency.timer;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


public class ScheduledThreadPoolDemo01
{


    public static void main(String[] args) throws InterruptedException
    {

        final TimerTask task1 = new TimerTask()
        {

            @Override
            public void run()
            {
                throw new RuntimeException();
            }
        };

        final TimerTask task2 = new TimerTask()
        {

            @Override
            public void run()
            {
                System.out.println("task2 invoked!");
            }
        };



        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        pool.schedule(task1, 100, TimeUnit.MILLISECONDS);
        pool.scheduleAtFixedRate(task2, 0 , 1000, TimeUnit.MILLISECONDS);

    }
}

代碼基本一致,但是ScheduledExecutorService可以保證,task1出現異常時,不影響task2的運行:

task2 invoked!
task2 invoked!
task2 invoked!
task2 invoked!
task2 invoked!

三、AlarmManager

    Java的Timer類可以用來計劃需要循環執行的任務。簡單的說,一個Timer內部封裝裝了“一個Thread”和“一個TimerTask隊列”,這個隊列按照一定的方式將任務排隊處理。封裝的Thread在Timer的構造方法調用時被啓動,這個Thread的run方法按照條件去循環這個TimerTask隊列,然後調用TimerTask的run方法。
    但是,如果CPU進入了休眠狀態,那麼這個thread將會因爲失去CPU時間片而阻塞,從而造成我們需要的定時任務失效。上述定時任務失效的場景分析:假設定時任務的條件是到了時間xx:yy才能執行,但由於cpu休眠造成線程阻塞的關係,當前系統時間超過了這個時間,即便CPU從終端中恢復了,那麼由於條件不滿足,定時任務在這一次自然就失效了。
    解決方案是:它需要用WakeLock讓CPU 保持喚醒狀態。那麼問題就來了,這樣會大量消耗手機電量(CPU喚醒和屏幕喚醒不是同一概念),大大減短手機待機時間。這種方式不能滿足我們的需求。
    注:TimerTask實現Runnable接口,但它的run方法只是簡單的在Timer中封裝的Thread中被調用,並未將其放在其它線程中執行。也就是說timer是單線程執行的,那麼問題來了,爲何要這麼費勁的封裝一個Runnable接口又不進行多線程調用?我的答案是,老外只是想要表示它是可執行的方法。
    關於休眠,可以參考
    Android 關於休眠引發的幾個血案
    Android手機休眠後時間不準確的解決方案
    AlarmManager是Android 系統封裝的用於管理RTC的模塊,RTC(Real Time Clock) 是一個獨立的硬件時鐘,可以在 CPU 休眠時正常運行,在預設的時間到達時,通過中斷喚醒CPU。這意味着,如果我們用 AlarmManager 來定時執行任務,CPU 可以正常的休眠,只有在需要運行任務時醒來一段很短的時間。

以下參考Android基礎入門教程——10.5 AlarmManager(鬧鐘服務)

核心流程如下:

    AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
    獲得系統提供的AlarmManager服務的對象
    Intent設置要啓動的組件:
    Intent intent = new Intent(MainActivity.this, ClockActivity.class);
    PendingIntent對象設置動作,啓動的是Activity還是Service,又或者是廣播!
    PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);
    調用AlarmManager的set( )方法設置單次鬧鐘的鬧鐘類型,啓動時間以及PendingIntent對象!
    alarmManager.set(AlarmManager.RTC_WAKEUP,c.getTimeInMillis(), pi);

//10秒鐘後執行一個任務
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);

相關方法:

    set(int type,long startTime,PendingIntent pi):一次性鬧鐘
    setRepeating(int type,long startTime,long intervalTime,PendingIntent pi):
    重複性鬧鐘,和3有區別,3鬧鐘間隔時間不固定
    setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi):
    重複性鬧鐘,時間不固定
    cancel(PendingIntent pi):取消AlarmManager的定時服務
    getNextAlarmClock():得到下一個鬧鐘,返回值AlarmManager.AlarmClockInfo
    setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
    和set方法類似,這個鬧鐘運行在系統處於低電模式時有效
    setExact(int type, long triggerAtMillis, PendingIntent operation):
    在規定的時間精確的執行鬧鐘,比set方法設置的精度更高
    setTime(long millis):設置系統牆上的時間
    setTimeZone(String timeZone):設置系統持續的默認時區
    setWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation):
    設置一個鬧鐘在給定的時間窗觸發。類似於set,該方法允許應用程序精確地控制操作系統調 整鬧鐘觸發時間的程度。

關鍵參數講解:

    Type(鬧鐘類型):
    有五個可選值:
        AlarmManager.ELAPSED_REALTIME:
        鬧鐘在手機睡眠狀態下不可用,該狀態下鬧鐘使用相對時間(相對於系統啓動開始),狀態值爲3;
        AlarmManager.ELAPSED_REALTIME_WAKEUP
        鬧鐘在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鐘也使用相對時間,狀態值爲2;
        AlarmManager.RTC
        鬧鐘在睡眠狀態下不可用,該狀態下鬧鐘使用絕對時間,即當前系統時間,狀態值爲1;
        AlarmManager.RTC_WAKEUP
        表示鬧鐘在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鐘使用絕對時間,狀態值爲0;
        AlarmManager.POWER_OFF_WAKEUP
        表示鬧鐘在手機關機狀態下也能正常進行提示功能,所以是5個狀態中用的最多的狀態之一,該狀態下鬧鐘也是用絕對時間,狀態值爲4;不過本狀態好像受SDK版本影響,某些版本並不支持;
    startTime:鬧鐘的第一次執行時間,以毫秒爲單位,可以自定義時間,不過一般使用當前時間。 需要注意的是,本屬性與第一個屬性(type)密切相關,如果第一個參數對應的鬧鐘使用的是相對時間 (ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那麼本屬性就得使用相對時間相對於系統啓動時間來說),比如當前時間就表示爲:SystemClock.elapsedRealtime(); 如果第一個參數對應的鬧鐘使用的是絕對時間(RTC、RTC_WAKEUP、POWER_OFF_WAKEUP), 那麼本屬性就得使用絕對時間,比如當前時間就表示 爲:System.currentTimeMillis()。
    intervalTime:表示兩次鬧鐘執行的間隔時間,也是以毫秒爲單位.
    PendingIntent:綁定了鬧鐘的執行動作,比如發送一個廣播、給出提示等等。
    PendingIntent是Intent的封裝類。需要注意的是,
        如果是通過啓動服務來實現鬧鐘提示的話,PendingIntent對象的獲取就應該採用Pending.getService (Context c,int i,Intent intent,int j)方法;
        如果是通過廣播來實現鬧鐘提示的話,PendingIntent對象的獲取就應該採用 PendingIntent.getBroadcast (Context c,int i,Intent intent,int j)方法;
        如果是採用Activity的方式來實現鬧鐘提示的話,PendingIntent對象的獲取就應該採用
        PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。

如果這三種方法錯用了的話,雖然不會報錯,但是看不到鬧鐘提示效果。

下面是一個每隔一小時就會在後臺執行定時任務的服務。

public class LongRunningService extends Service{
   public IBinder onBind(Intent intent){
      return null;
   }

   public int onStartCommand(Intent intent, int flags, int startId){
      new Thread(new Runnable(){
         public void run(){
            Log.d("LongRunningService","executed at"+new Date().toString());
         }
      }).start();
   }
   AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
   int anHour = 60 * 60 * 1000;
   long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
   Intent i = new Intent(this,AlarmReceiver.class);
   PendingIntent pi = PendingIntent.getBroadcast(this,0,i,0);
   manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pi);
   return super.onStartCommand(intent,flags,startId);
}

public class AlarmReceiver extends BroadcastReceiver{
   public void onReceive(Context context, Intent intent){
      Intent i = new Intent(context, LongRunningService.class);
      context.startService(i);
   }
}

public class MainActivity extends Activity{
   protected void onCreate(Bundle savedInstanceState){
      super.onCreate(savedInstanceState);
      Intent intent = new Intent(this,LongRunningService);
      startService(intent);
   }
}

四、CountDownTimer

參考
Android實現倒計時之使用CountDownTimer
[Android] CountDownTimer 簡單用法與源碼解析

    在開發中會經常用到倒計時這個功能,包括給手機發送驗證碼等等,之前我的做法都是使用Handler + Timer +TimerTask來實現,現在發現了這個類,果斷拋棄之前的做法,相信還是有很多人和我一樣一開始不知道Android已經幫我們封裝好了一個叫CountDownTimer的類。CountDownTimer timer = new CountDownTimer(10000,1000);以毫秒爲單位,第一個參數是指從開始調用start()方法到倒計時完成的時候onFinish()方法被調用這段時間的毫秒數,也就是倒計時總的時間;第二個參數表示間隔多少毫秒調用一次 onTick方法,例如間隔1000毫秒。在調用的時候直接使用timer.start();

//共有4個方法,前兩個抽象方法需要重寫
public abstract void onTick(long millisUntilFinished);//固定間隔調用
public abstract void onFinish();//倒計時完成時被調用
public synchronized final void cancel();//取消倒計時,當再次啓動會重新開始倒計時
public synchronized final CountDownTimer start();//啓動倒計時
//該類的成員變量與成員函數均較少,功能實現的關鍵部分在於 mHandler,下面看 mHandler 的源碼:
private Handler mHandler = new Handler() {

  @Override public void handleMessage(Message msg) {

    synchronized (CountDownTimer.this) {
      if (mCancelled) {
        return;
      }

      final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();

      if (millisLeft <= 0) {
        onFinish();
      } else if (millisLeft < mCountdownInterval) {
        // no tick, just delay until done
        sendMessageDelayed(obtainMessage(MSG), millisLeft);
      } else {
        long lastTickStart = SystemClock.elapsedRealtime();
        onTick(millisLeft);

        // take into account user's onTick taking time to execute
        long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();

        // special case: user's onTick took more than interval to
        // complete, skip to next interval
        while (delay < 0) delay += mCountdownInterval;

        sendMessageDelayed(obtainMessage(MSG), delay);
      }
    }
  }
};


內部流程


看一個使用例子:

package com.per.countdowntimer;

import android.app.Activity;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.widget.TextView;


public class MainActivity extends Activity {
    private TextView mTvShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvShow = (TextView) findViewById(R.id.show);
    }

    /**
     * 取消倒計時
     * @param v
     */
    public void oncancel(View v) {
        timer.cancel();
    }

    /**
     * 開始倒計時
     * @param v
     */
    public void restart(View v) {
        timer.start();
    }

    private CountDownTimer timer = new CountDownTimer(10000, 1000) {

        @Override
        public void onTick(long millisUntilFinished) {
            mTvShow.setText((millisUntilFinished / 1000) + "秒後可重發");
        }

        @Override
        public void onFinish() {
            mTvShow.setEnabled(true);
            mTvShow.setText("獲取驗證碼");
        }
    };
}

五、new Handler().postDelayed()

該方法就是利用我們常說的消息處理器。該方法原理就是在主線程中創建一個Handler消息處理器,然後利用其中的一個postDelayed(Runnable r, long delayMillis)方法,該方法第一個參數需要傳入一個Runnable接口,並實現run()方法,第二個參數就是延遲多少時間將run()方法中的代碼通過一個消息發送給消息隊列,然後在主線程中執行這個消息中的代碼,即是run方法中的代碼,從而實現在主線程中更新界面UI。
貼代碼吧:

new Handler().postDelayed(new Runnable() {//在當前線程(也即主線程中)開啓一個消息處理器,並在3秒後在主線程中執行,從而來更新UI
    @Override
    public void run() {
        //有關更新UI的代碼
    }
}, 3000);//3秒後發送


發佈了54 篇原創文章 · 獲贊 22 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章