AndroidでUIメソッド呼び出しの共通時刻を取得する

概要

UIスレッドから呼び出される処理全体で初期化やバケツリレー無しで共通時刻を用意したいなーという話。
下記のメソッド1~Nすべてでボタン押下時刻を共通時間として取得するとか。

ボタン押下 → onClick()実行 → メソッド1呼び出し → メソッド2呼び出し … メソッドN 呼び出し

本題

Android アプリ開発では、時間情報として currentTimeMillis()uptimeMillis() を扱うことがあります。

これらのメソッドは現在時刻を返すので、同一スレッド内で連続で呼び出しても異なる時刻が返されます。*1

連続した処理で同じ時刻を扱いたい場合、どこかで時刻を取得して、それをメソッドの呼び出し階層でバケツリレーする方法で対応することが可能です。

しかし、含まれるメソッド数が大きい場合や、自身は時刻情報を利用しないのにバケツリレーのためだけに引数を受け取るメソッドが多数あるような場合は面倒です。そのような場合は独自にスレッドを生成して ThreadLocal にスレッド生成直後の時刻を設定するという方法も考えられます。

しかし、UIスレッドの場合、既にスレッドが存在しているし、描画開始タイミングもアプリ側で制御できません。

そこで、main looper が message の dispatch を開始するタイミングで時刻を設定する方法を試してみました。

object MainLooperMessageDispatchUptimeMillisManager {
    private var isAvailable = false
        private set
    
    var mainLooperMessageDispatchUptimeMillis: Long = -1
        get() {
            assertMainThread()
            return field
        }
        private set
    
    init {
        assertMainThread()
        Looper.getMainLooper().setMessageLogging {
            // 開始タイミングのみ時刻を記録する。
            // 最初に実行されるのは、init{} が実行された後で最初に main looper が message を dispatch した時。
            // init{} が実行された時の main looper の dispatch 終了タイミングでは呼び出しが無い点に注意。
            isAvailable = !isAvailable
            if (isAvailable) mainLooperMessageDispatchUptimeMillis = SystemClock.uptimeMillis()
        }
    }
    
    private fun assertMainThread() {
        if (Thread.currentThread() != Looper.getMainLooper().thread) throw IllegalStateException("Current thread is not main thread.")
    }
}


これを Application#onCreate()などで一度評価することによって初期化します。*2

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 式として評価して初期化する
        MainLooperMessageDispatchUptimeMillisManager
    }
}


使うときは static import しておくと便利。

import mypackage.MainLooperMessageDispatchUptimeMillisManager.mainLooperMessageDispatchUptimeMillis as uptimeMillis


呼び出し例

val handler1 = Handler()
val handler2 = Handler()

fun f1(handler: String, name: String) { println("f1(): $handler: $name: $uptimeMillis"); Thread.sleep(10) }
fun f2(handler: String, name: String) { println("f2(): $handler: $name: $uptimeMillis"); Thread.sleep(10) }

handler1.postDelayed(100) { f1("handler1", "a1"); f2("handler1", "a2") }
handler1.postDelayed(100) { f1("handler1", "b1"); f2("handler1", "b2") }
handler2.postDelayed(100) { f1("handler2", "c1"); f2("handler2", "c2") }
handler2.postDelayed(100) { f1("handler2", "d1"); f2("handler2", "d2") }

実行結果

f1(): handler1: a1: 844721417 ← 同一スレッドでは時間が共通となる。
f2(): handler1: a2: 844721417 ← 同一スレッドでは時間が共通となる。
f1(): handler1: b1: 844721438 ← main looper から dispatch されるタイミングが異なるので上記の処理と時刻がずれる。
f2(): handler1: b2: 844721438
f1(): handler2: c1: 844721459 ← 同一 handler でも時刻が異なるのだから別 handler でも当然そうなる。
f2(): handler2: c2: 844721459
f1(): handler2: d1: 844721480
f2(): handler2: d2: 844721480


Looper#setMessageLogging(android.util.Printer) はどこからでもセットできてしまう点は注意すべきですね。

*1:ミリ秒精度で一致することはありますが。

*2:生成と初期化を分けてもいいし方法は何でもいいですw