1. 程式人生 > >多種方式實現Android定時任務,哪一款是你的FEEL?

多種方式實現Android定時任務,哪一款是你的FEEL?

前言

專案中總是會因為各種需求新增各種定時任務,所以就打算小結一下Android中如何實現定時任務,下面的解決方案的案例大部分都已在實際專案中實踐,特此列出供需要的朋友參考,如果有什麼使用不當或者存在什麼問題,歡迎留言指出!直接上乾貨!

解決方案

普通執行緒sleep的方式實現定時任務

建立一個thread,然後讓它在while迴圈裡一直執行著,通過sleep方法來達到定時任務的效果,這是最常見的,可以快速簡單地實現。但是這是java中的實現方式,不建議使用

    public class ThreadTask {  
    public static void main
(String[] args) { final long timeInterval = 1000; Runnable runnable = new Runnable() { public void run() { while (true) { System.out.println("execute task"); try { Thread.sleep(timeInterval); } catch
(InterruptedException e) { e.printStackTrace(); } } } }; Thread thread = new Thread(runnable); thread.start(); } }

Timer實現定時任務

和普通執行緒+sleep(long)+Handler的方式比,優勢在於

  • 可以控制TimerTask的啟動和取消
  • 第一次執行任務時可以指定delay的時間。

在實現時,Timer類排程任務,TimerTask則是通過在run()方法裡實現具體任務(然後通過Handler與執行緒協同工作,接收執行緒的訊息來更新主UI執行緒的內容)。

  • Timer例項可以排程多工,它是執行緒安全的。當Timer的構造器被呼叫時,它建立了一個執行緒,這個執行緒可以用來排程任務。
   /**
    * start Timer
    */
    protected synchronized void startPlayerTimer() {
        stopPlayerTimer();
        if (playTimer == null) {
            playTimer = new PlayerTimer();
            Timer m_musictask = new Timer();
            m_musictask.schedule(playTimer, 5000, 5000);
        }
    }

    /**
     * stop Timer
     */
    protected synchronized void stopPlayerTimer() {
        try {
            if (playTimer != null) {
                playTimer.cancel();
                playTimer = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class PlayerTimer extends TimerTask {

        public PlayerTimer() {
        }

        public void run() {
            //execute task
        }
    }

然而Timer是存在一些缺陷的

  • Timer在執行定時任務時只會建立一個執行緒,所以如果存在多個任務,且任務時間過長,超過了兩個任務的間隔時間,會發生一些缺陷
  • 如果Timer排程的某個TimerTask丟擲異常,Timer會停止所有任務的執行
  • Timer執行週期任務時依賴系統時間,修改系統時間容易導致任務被掛起(如果當前時間小於執行時間)

注意:

  • Android中的Timer和java中的Timer還是有區別的,但是大體呼叫方式差不多
  • Android中需要根據頁面的生命週期和顯隱來控制Timer的啟動和取消
    java中Timer:

    這裡寫圖片描述

    Android中的Timer:

    這裡寫圖片描述

ScheduledExecutorService實現定時任務

ScheduledExecutorService是從JDK1.5做為併發工具類被引進的,存在於java.util.concurrent,這是最理想的定時任務實現方式。
相比於上面兩個方法,它有以下好處:

  • 相比於Timer的單執行緒,它是通過執行緒池的方式來執行任務的,所以可以支援多個任務併發執行 ,而且彌補了上面所說的Timer的缺陷
  • 可以很靈活的去設定第一次執行任務delay時間
  • 提供了良好的約定,以便設定執行的時間間隔

簡例:

public class ScheduledExecutorServiceTask 
{  
    public static void main(String[] args) 
    {  
        final TimerTask task = new TimerTask()  
        {  
            @Override  
            public void run()  
            {  
               //execute task  
            }  
        };   
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);  
        pool.scheduleAtFixedRate(task, 0 , 1000, TimeUnit.MILLISECONDS);  

    }  
}

實際案例背景:

  • 應用中多個頁面涉及比賽資訊展示(包括比賽狀態,比賽結果),因此需要實時更新這些資料

思路分析:

  • 多頁面多介面重新整理
    • 就是每個需要重新整理的頁面基於自身需求基於特定介面定時重新整理
    • 每個頁面要維護一個定時器,然後基於頁面的生命週期和顯隱進行定時器的開啟和關閉(保證資源合理釋放)
    • 而且這裡的重新整理涉及到是重新整理區域性資料還是整體資料,重新整理整體資料效率會比較低,顯得非常笨重
  • 多頁面單介面重新整理
    • 介面給出一組需要實時進行重新整理的比賽資料資訊,客戶端基於id進行統一過濾匹配
    • 通過單例封裝統一定時重新整理回撥介面(注意記憶體洩露的問題,頁面銷燬時關閉ScheduledExecutorService )
    • 需要重新整理的item統一呼叫,入口唯一,方便維護管理,擴充套件性好
    • 區域性重新整理,效率高
public class PollingStateMachine implements INetCallback {

    private static volatile PollingStateMachine instance = null;
    private ScheduledExecutorService pool;
    public static final int TYPE_MATCH = 1;

    private Map matchMap = new HashMap<>();
    private List<WeakReference<View>> list = new ArrayList<>();
    private Handler handler;

    // private constructor suppresses
    private PollingStateMachine() {
        defineHandler();
        pool = Executors.newSingleThreadScheduledExecutor();
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                doTasks();
            }
        }, 0, 10, TimeUnit.SECONDS);
    }

    private void doTasks() {
        ThreadPoolUtils.execute(new PollRunnable(this));
    }

    public static PollingStateMachine getInstance() {
        // if already inited, no need to get lock everytime
        if (instance == null) {
            synchronized (PollingStateMachine.class) {
                if (instance == null) {
                    instance = new PollingStateMachine();
                }
            }
        }
        return instance;
    }
    public <VIEW extends View> void subscibeMatch(VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
        subscibe(TYPE_MATCH,view,onViewRefreshStatus);
    }

    private <VIEW extends View> void subscibe(int type, VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
        view.setTag(onViewRefreshStatus);
        if (type == TYPE_MATCH) {
            onViewRefreshStatus.update(view, matchMap);
        } 
        for (WeakReference<View> viewSoftReference : list) {
            View textView = viewSoftReference.get();
            if (textView == view) {
                return;
            }
        }
        WeakReference<View> viewSoftReference = new WeakReference<View>(view);
        list.add(viewSoftReference);
    }

    public void updateView(final int type) {
        Iterator<WeakReference<View>> iterator = list.iterator();
        while (iterator.hasNext()) {
            WeakReference<View> next = iterator.next();
            final View view = next.get();
            if (view == null) {
                iterator.remove();
                continue;
            }
            Object tag = view.getTag();
            if (tag == null || !(tag instanceof OnViewRefreshStatus)) {
                continue;
            }
            final OnViewRefreshStatus onViewRefreshStatus = (OnViewRefreshStatus) tag;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (type == TYPE_MATCH) {
                        onViewRefreshStatus.update(view, matchMap);
                    } 
                }
            });

        }
    }

    public void clear() {
        pool.shutdown();
        instance = null;
    }

    private Handler defineHandler() {
        if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
            handler = new Handler();
        }
        return handler;
    }


    @Override
    public void onNetCallback(int type, Map msg) {
        if (type == TYPE_MATCH) {
            matchMap=msg;
        } 
        updateView(type);
    }
}

需要重新整理的item呼叫

PollingStateMachine.getInstance().subscibeMatch(tvScore, new OnViewRefreshStatus<ScoreItem, TextView>(matchViewItem.getMatchID()) {
            @Override
            public void OnViewRefreshStatus(TextView view, ScoreItem scoreItem) {
                //重新整理處理
                }
        });

網路資料回撥介面

public interface INetCallback {

    void onNetCallback(int type,Map msg);

}

重新整理回撥介面:

public abstract class OnViewRefreshStatus<VALUE, VIEW extends View> {
    private static final String TAG = OnViewRefreshStatus.class.getSimpleName();
    private long key;

    public OnViewRefreshStatus(long key) {
        this.key = key;
    }

    public long getKey() {
        return key;
    }

    public void update(final VIEW view, Map<Long, VALUE> map) {
        final VALUE value = map.get(key);

        if (value == null) {
            return;
        }
        OnViewRefreshStatus(view, value);

    }


    public abstract void OnViewRefreshStatus(VIEW view, VALUE value);

}

Handler實現定時任務

通過Handler延遲傳送訊息的形式實現定時任務。

  • 這裡通過一個定時傳送socket心跳包的案例來介紹如何通過Handler完成定時任務

案例背景

  • 由於移動裝置的網路的複雜性,經常會出現網路斷開,如果沒有心跳包的檢測, 客戶端只會在需要傳送資料的時候才知道自己已經斷線,會延誤,甚至丟失伺服器傳送過來的資料。 所以需要提供心跳檢測

下面的程式碼只是用來說明大體實現流程

  • WebSocket初始化成功後,就準備傳送心跳包
  • 每隔30s傳送一次心跳
  • 建立的時候初始化Handler,銷燬的時候移除Handler訊息佇列
private static final long HEART_BEAT_RATE = 30 * 1000;//目前心跳檢測頻率為30s
private Handler mHandler;
private Runnable heartBeatRunnable = new Runnable() {
        @Override
        public void run() {
            // excute task
            mHandler.postDelayed(this, HEART_BEAT_RATE);
        }
    };
public void onCreate() {
     //初始化Handler
    }

//初始化成功後,就準備傳送心跳包
public void onConnected() {
        mHandler.removeCallbacks(heartBeatRunnable);
        mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);
    }

public void onDestroy() {
        mHandler.removeCallbacks(heartBeatRunnable)
    }

注意:這裡開啟的runnable會在這個handler所依附執行緒中執行,如果handler是在UI執行緒中建立的,那麼postDelayed的runnable自然也依附在主執行緒中。

AlarmManager實現精確定時操作

我們使用Timer或者handler的時候會發現,delay時間並沒有那麼準。如果我們需要一個嚴格準時的定時操作,那麼就要用到AlarmManager,AlarmManager物件配合Intent使用,可以定時的開啟一個Activity,傳送一個BroadCast,或者開啟一個Service.

案例背景

  • 在比賽開始前半個小時本地定時推送關注比賽的提醒資訊

AndroidManifest.xml中宣告一個全域性廣播接收器

  <receiver
            android:name=".receiver.AlarmReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="${applicationId}.BROADCAST_ALARM" />
            </intent-filter>
        </receiver>

接收到action為IntentConst.Action.matchRemind的廣播就展示比賽
提醒的Notification

public class AlarmReceiver extends BroadcastReceiver {
    private static final String TAG = "AlarmReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null) {
            return;
        }
        String action = intent.getAction();
        if (TextUtils.equals(action, IntentConst.Action.matchRemind)) {
            //通知比賽開始
            long matchID = intent.getLongExtra(Net.Param.ID, 0);
            showNotificationRemindMe(context, matchID);
        }

    }
}

AlarmUtil提供設定鬧鈴和取消鬧鈴的兩個方法

public class AlarmUtil {
    private static final String TAG = "AlarmUtil";

    public static void controlAlarm(Context context, long startTime,long matchId, Intent nextIntent) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (int) matchId, nextIntent,
                PendingIntent.FLAG_ONE_SHOT);
        alarmManager.set(AlarmManager.RTC_WAKEUP, startTime, pendingIntent);
    }

    public static void cancelAlarm(Context context, String action,long matchId) {
        Intent intent = new Intent(action);
        PendingIntent sender = PendingIntent.getBroadcast(
                context, (int) matchId, intent, 0);

        // And cancel the alarm.
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(sender);
    }


}

關注比賽的時候,會設定intent的action為IntentConst.Action.matchRemind,會根據比賽的開始時間提前半小時設定鬧鈴時間,取消關注比賽同時取消鬧鈴

   if (isSetAlarm) {
            long start_tm = matchInfo.getStart_tm();
            long warmTime = start_tm - 30 * 60;

            Intent intent = new Intent(IntentConst.Action.matchRemind);
            intent.putExtra(Net.Param.ID, matchInfo.getId());
            AlarmUtil.controlAlarm(context, warmTime * 1000,matchInfo.getId(), intent);

            Gson gson = new Gson();
            String json = gson.toJson(matchInfo);
            SportDao.getInstance(context).insertMatchFollow(eventID, matchInfo.getId(), json, matchInfo.getType());
        } else {
            AlarmUtil.cancelAlarm(context, IntentConst.Action.matchRemind,matchInfo.getId());
            SportDao.getInstance(context).deleteMatchFollowByID(matchInfo.getId());
        }