[Xamarin.Forms] Device.StartTimerを使ってタイマー処理を行う

2017.03.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Device.StartTimerを使えば簡単にタイマー処理を行える

Xamarin.FormsにはDeviceという現在のデバイスやプラットフォームとやりとりを行うためのユーティリティクラスがあります。 当該クラスに用意されているStartTimerというstaticメソッドを使うことで簡単にタイマー処理を行うことができます。

Device.StartTimerを使った例

先に実行結果を載せておきます。 iOSは10.2のシミュレーター、Androidは実機(Androidバージョン6.0.1のNexus 5)で動かしています。

 iOS  Android
iOS_device_start_timer Android_device_start_timer

ボタンをタップしたらラベルのカウントを1ずつインクリメントし、10回行ったらリセットしています。

検証環境

以下の環境で動作を確認しています。

  • Xamarin Studio Community バージョン 6.2(build 1821)
  • Xamarin.Forms バージョン 2.3.3.193

ソースコード

XAMLにはLabelとButtonを配置しています。

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:TimerSample" x:Class="TimerSample.TimerSamplePage">
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <Label x:Name="label" Text="0" HorizontalTextAlignment="Center"/>
        <Button x:Name="button" Text="Start Timer" Clicked="Handle_Clicked"/>
    </StackLayout>
</ContentPage>

コードビハインドはこちらです。
Device.StartTimerの第1引数はTimeSpan型で処理の実行間隔を指定します。 第2引数の型はFunc<bool>で処理を指定します。戻り値としてtrueを返すと繰り返し処理が行われます。falseを返すとタイマーは停止します。

private int _count;

private void Handle_Clicked(object sender, System.EventArgs e)
{
    this.button.IsEnabled = false;

    Device.StartTimer(
        TimeSpan.FromSeconds(1),
        () =>
        {
            // カウントをインクリメント
            _count++;

            // タイマーを繰り返すかどうかの判定
            var keepRecurring = _count < 10;
            if (!keepRecurring)
            {
                // タイマー終了時の処理
                this.button.IsEnabled = true;
                _count = 0;
            }

            // カウントをラベルのテキストに設定
            this.label.Text = $"{_count}";
            return keepRecurring;
        });
}

Device.StartTimerの実装を見てみる

簡単に使えて便利なDevice.StartTimerですが、どのような実装になっているのか、どのスレッドで処理が行われるのか気になったので調べてみました。

Xamarin.Formsのソースコードはgithubで公開されているので誰でも見ることができます。 StartTimerメソッドはXamarin.Forms.Core/Device.csに定義されています。

public static void StartTimer(TimeSpan interval, Func<bool> callback)
{
  PlatformServices.StartTimer(interval, callback);
}

PlatformServicesのStartTimerを実行していました。 じゃあ、このPlatformServicesって何よ?てことで定義を見てみます。

[EditorBrowsable(EditorBrowsableState.Never)]
public static IPlatformServices PlatformServices
{
  get
  {
    if (s_platformServices == null)
      throw new InvalidOperationException("You MUST call Xamarin.Forms.Init(); prior to using it.");
    return s_platformServices;
  }
  set { s_platformServices = value; }
}

IPlatformServices型のプロパティとして定義されていました。 ちなみに、s_platformServicesはIPlatformServices型のprivateなstaticフィールドです。 そして、getterにおいてs_platformServicesがnullの場合にExceptionが投げられています。メッセージには「"You MUST call Xamarin.Forms.Init(); prior to using it."」とあるので、Xamarin.Formsの初期化の際にPlatformServicesに値が設定されるようです。

iOS

Xamarin.Formsの初期化処理は各プラットフォームで行っているので、IPlatformServicesの実装クラスもプラットフォーム毎に存在します。 iOSではIOSPlatformServicesクラスが該当し、以下のようにStartTimerメソッドの実装がありました。

public void StartTimer(TimeSpan interval, Func<bool> callback)
{
  NSTimer timer = NSTimer.CreateRepeatingTimer(interval, t =>
  {
    if (!callback())
      t.Invalidate();
  });
  NSRunLoop.Main.AddTimer(timer, NSRunLoopMode.Common);
}

NSTimer.CreateRepeatingTimerで繰り返し処理を行うNSTimerを生成し、メインスレッドのrun loopにCommonモードで登録しています。 このことからタイマーで実行する処理はメインスレッド上で行われることがわかります。 また、Commonモードで登録しているので、ScrollViewのスクロール中にタイマー処理が止まってしまうこともありません。

Android

AndroidではAndroidPlatformServicesがIPlatformServicesの実装クラスです。 以下のようにStartTimerメソッドの実装がありました。

public void StartTimer(TimeSpan interval, Func<bool> callback)
{
  var handler = new Handler(Looper.MainLooper);
  handler.PostDelayed(() =>
  {
    if (callback())
      StartTimer(interval, callback);

    handler.Dispose();
    handler = null;
  }, (long)interval.TotalMilliseconds);
}

new Handler(Looper.MainLooper)でメインスレッドのLooperに対するHandlerを生成しています。 そして、指定したinterval経過後に処理をLooperのキューに入れています。 このことからタイマーで実行する処理はメインスレッド上で行われることがわかります。

まとめ

今回はDevice.StartTimerを使ってタイマー処理を行う方法をご紹介しました。 また、Device.StartTimerの実装の中身を見ることで、メインスレッド上で処理が行われることが確認できました。 iOSはNSTimerというタイマーそのものを扱っているのに対し、AndroidはLooperとHandlerの仕組みを使って「タイマーのようなもの」を実装している点がプラットフォームの差があっておもしろかったです。

参考記事

Xamarin.Forms.Device Class