Handler機制(4)-Handler常見應用場景和常見問題分析

主目錄見:Android高級進階知識(這是總目錄索引)
[written by Ticoo]

Handler應用場景

根據前幾篇的分析,根據實際的開發,我們可以總結出以下Handler的使用場景

最簡單的消息發送

主線程使用Handler, 主線程里或子線程里發送消息,或延遲發送消息的方式更新UI
如,
啟動應用時Splash頁面的延遲2,3秒后,跳轉到主頁面
加載完頁面的各個控件后,再加載線程下載圖片,最后更新圖片等等

 private static final int WHAT_UPDATE_ICON = 1;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case WHAT_UPDATE_ICON:
                    Log.e(Tag, "receive message:" + msg.obj);
                    break;
            }
        }
    };
    
    Message msg = handler.obtainMessage(WHAT_UPDATE_ICON);
    msg.obj = "update the imageview";
    handler.sendMessage(msg);

使用消息的時候,盡量使用 obtainMessage 的方式來獲取Message,避免多次創建Message對象,消耗內存,效率低下。

結合HandlerThread處理耗時任務

結合HandlerThread,串行的處理單個耗時任務,如單任務下載

class DownloadOneByOne extends HandlerThread {
    public DownloadOneByOne() {
        super(DownloadOneByOne.class.getSimpleName());
    }

    @Override
    protected void onLooperPrepared() {
        super.onLooperPrepared();
        // 初始化下載組件
    }
}

private HandlerThread mHandlerThread;

private Handler downloadHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        String url = (String) msg.obj;
        // 使用下載組件開始下載
        
    }
};

public void initHandler() {
    // 初始化Handler
    mHandlerThread = new DownloadOneByOne();
    mHandlerThread.start();
    
    downloadHandler = new Handler(mHandlerThread.getLooper());
}

private void sendDownloadTask(String downloadUrl) {
    // 發送下載任務
    Message msg = downloadHandler.obtainMessage(WHAT_DOWNLOAD_TASK);
    msg.obj = downloadUrl;
    downloadHandler.sendMessage(msg);
}

倒計時View的簡易實現

通過Handler我們還可以快速簡易,并且不占用太多性能的實現一個簡易的倒計時View。

public class CountDownView extends AppCompatTextView {
    /**
     * 總時間
     */
    private long seconds;
    /**
     * 當前分鐘
     */
    private long minutes;
    /**
     * 當前秒數
     */
    private int second = 60;

    private static final int SECONDS_PER_MINUTE = 60;
    private static final int MILLS_PER_SECOND = 1000;
    private static final int MILLS_PER_MINUTE = SECONDS_PER_MINUTE * 1000;

    private static final int WHAT_DONE = 2;
    private static final int WHAT_TICK = 1;

    private int marginEnd;

    private StringBuilder content = new StringBuilder();

    public CountDownView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
        int size = (int) (MeasureSpec.getSize(widthMeasureSpec) / deviceProfile.inv.numColumns);
        marginEnd = marginEnd == 0 ? (size - deviceProfile.iconSizePx) / 2 : marginEnd;

        setMarginEnd(marginEnd);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private void setMarginEnd(int marginEnd) {
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
        layoutParams.setMarginEnd(marginEnd);
        layoutParams.resolveLayoutDirection(layoutParams.getLayoutDirection());
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (handler.hasMessages(WHAT_TICK)) {
            handler.removeMessages(WHAT_TICK);
        }
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case WHAT_DONE:
                    setVisibility(View.GONE);
                    break;
                default:
                    setText(content.toString());
                    handler.post(runnable);
                    break;
            }
        }
    };

    /***
     * 設置倒計時
     * @param millis
     */
    public void setCountDownMills(long millis) {
        seconds = (long) Math.floor(millis / MILLS_PER_SECOND);
        minutes = (long) Math.floor(millis / MILLS_PER_MINUTE) - 1;
        // start after one second
        handler.postDelayed(runnable, MILLS_PER_SECOND);
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (seconds <= 0) {
                handler.sendEmptyMessage(WHAT_DONE);
                return;
            }
            seconds--;
            if (second <= 0) {
                second = SECONDS_PER_MINUTE;
                minutes = (long) Math.floor(seconds / SECONDS_PER_MINUTE);
            }
            second--;
            content.delete(0, content.length());

            appendZeroWhenLower10(minutes);
            content.append(":");
            appendZeroWhenLower10(second);

            if (handler.hasMessages(WHAT_TICK)) {
                handler.removeMessages(WHAT_TICK);
            }
            handler.sendEmptyMessageDelayed(WHAT_TICK, MILLS_PER_SECOND);
        }
    };

    private StringBuilder appendZeroWhenLower10(long value) {
        if (value < 10) {
            content.append("0").append(value);
        } else {
            content.append(value);
        }
        return content;
    }
}

結合IntentService的使用

使用IntentService處理耗時的任務相對比較簡單,我們來個有難度的,結合AlarmManager的調度,息屏喚醒IntentService定時處理任務的案例來講
當我們的應用進入后臺activity棧的時候,注冊并啟動AlarmManager任務

    private static final String ACTION_WAKE_UP = "com.doze.cpu.wakeup";

  public static void registerAlarm(Context context, int wakeType) {
    type = wakeType;
    if (alarmManager == null)
        alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

    if (operation != null) alarmManager.cancel(operation);

    schedule(context);
}
    
private static void schedule(Context context) {
    Intent intent = new Intent();
    intent.setAction(ACTION_WAKE_UP);
    operation = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    switch (type) {
        case 0:
            AlarmUtils.setRTCWakeup(alarmManager, AlarmUtils.DEFAULT_TRIGGER_AT_MILLIS, operation);
            break;
        case 1:
            AlarmUtils.setElapsedWakeup(alarmManager, AlarmUtils.DEFAULT_TRIGGER_AT_MILLIS, operation);
            break;
    }
}

定時5分鐘發送一個Action為com.doze.cpu.wakeup的廣播,我們的廣播需要繼承 WakefulBroadcastReceiver, 在onReceive里,調用startWakefulService方法,會創建一個1分鐘的WakeLock,喚醒cpu處理我們的任務,我們的任務在IntentService處理最好不過了,處理完就銷毀,不會有多余的占用

public class WakeCPUReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

        Intent wakefulIntent = new Intent(context, WorkService.class);
        startWakefulService(context, wakefulIntent);
        schedule(context);
    }

startWakefulService的源碼

public static ComponentName startWakefulService(Context context, Intent intent) {
        synchronized (mActiveWakeLocks) {
            int id = mNextId;
            mNextId++;
            if (mNextId <= 0) {
                mNextId = 1;
            }

            intent.putExtra(EXTRA_WAKE_LOCK_ID, id);
            ComponentName comp = context.startService(intent);
            if (comp == null) {
                return null;
            }

            PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
            PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    "wake:" + comp.flattenToShortString());
            wl.setReferenceCounted(false);
            wl.acquire(60*1000);
            mActiveWakeLocks.put(id, wl);
            return comp;
        }
    }

在IntentService里,我們在onHandleIntent處理我們的任務后,再調用
WakefulBroadcastReceiver的靜態方法completeWakefulIntent,釋放WakeLock,減少電量的消耗

public class WorkService extends IntentService {
    ...
    @Override
    protected void onHandleIntent(Intent intent) {
        Log.e(WakeCPUReceiver.TAG, "WorkService is working");
        // TODO 處理我們的任務
        WakeCPUReceiver.completeWakefulIntent(intent);
    }
}

completeWakefulIntent源碼

public static boolean completeWakefulIntent(Intent intent) {
        final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0);
        if (id == 0) {
            return false;
        }
        synchronized (mActiveWakeLocks) {
            PowerManager.WakeLock wl = mActiveWakeLocks.get(id);
            if (wl != null) {
                wl.release();
                mActiveWakeLocks.remove(id);
                return true;
            }
            // We return true whether or not we actually found the wake lock
            // the return code is defined to indicate whether the Intent contained
            // an identifier for a wake lock that it was supposed to match.
            // We just log a warning here if there is no wake lock found, which could
            // happen for example if this function is called twice on the same
            // intent or the process is killed and restarted before processing the intent.
            Log.w("WakefulBroadcastReceiver", "No active wake lock id #" + id);
            return true;
        }
    }

Handler的溢出問題

雖然Handler很好用,但由于它可以延遲發送消息,在我們延遲啟動其他組件,或者使用Activity的引用調用一些方法時,如果在延遲的過程中,Activity finish掉了,這時候就會拋出溢出的異常了。
所以,我們在onDestroy的時候,記得 調用removeCallbacks,removeMessages等移除消息的方法來解決這個問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容