Browse Source

Proper implementation of DispatcherImpl for iOS

pull/10923/head
Max Katz 4 years ago
parent
commit
d0be3f2d7b
  1. 139
      src/iOS/Avalonia.iOS/DispatcherImpl.cs
  2. 47
      src/iOS/Avalonia.iOS/Interop.cs
  3. 3
      src/iOS/Avalonia.iOS/Platform.cs
  4. 38
      src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

139
src/iOS/Avalonia.iOS/DispatcherImpl.cs

@ -0,0 +1,139 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Threading;
using Avalonia.Threading;
using CoreFoundation;
using Foundation;
using ObjCRuntime;
using CFIndex = System.IntPtr;
namespace Avalonia.iOS;
internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
{
// CFRunLoopTimerSetNextFireDate docs recommend to "create a repeating timer with an initial
// firing time in the distant future (or the initial firing time) and a very large repeat
// interval—on the order of decades or more"
private const double DistantFutureInterval = (double)50*365*24*3600;
internal static readonly DispatcherImpl Instance = new();
private readonly Stopwatch _clock = Stopwatch.StartNew();
private readonly Action _checkSignaledAction;
private readonly Action _wakeUpLoopAction;
private readonly IntPtr _timer;
private Thread? _loopThread;
private bool _backgroundProcessingRequested, _signaled;
private DispatcherImpl()
{
_checkSignaledAction = CheckSignaled;
_wakeUpLoopAction = () =>
{
// This is needed to wakeup the loop if we are called from inside of BeforeWait hook
};
var observerBlock = new BlockLiteral();
observerBlock.SetupBlock((Interop.CFRunLoopObserverCallback)ObserverCallback, null);
var observer = Interop.CFRunLoopObserverCreateWithHandler(IntPtr.Zero,
Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources |
Interop.CFOptionFlags.kCFRunLoopBeforeWaiting,
true,
0,
ref observerBlock);
Interop.CFRunLoopAddObserver(CFRunLoop.Main.Handle, observer, Interop.kCFRunLoopCommonModes);
var timerBlock = new BlockLiteral();
timerBlock.SetupBlock((Interop.CFRunLoopTimerCallback)TimerCallback, null);
_timer = Interop.CFRunLoopTimerCreateWithHandler(IntPtr.Zero,
Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval,
DistantFutureInterval, 0, 0, ref timerBlock);
}
public event Action? Signaled;
public event Action? Timer;
public event Action? ReadyForBackgroundProcessing;
public bool CurrentThreadIsLoopThread
{
get
{
if (_loopThread != null)
return Thread.CurrentThread == _loopThread;
if (!NSThread.IsMain)
return false;
_loopThread = Thread.CurrentThread;
return true;
}
}
public void Signal()
{
lock (this) {
if(_signaled)
return;
_signaled = true;
DispatchQueue.MainQueue.DispatchAsync(_checkSignaledAction);
CFRunLoop.Main.WakeUp();
}
}
public void UpdateTimer(long? dueTimeInMs)
{
var ms = dueTimeInMs == null ? -1 : (int)Math.Min(int.MaxValue - 10, Math.Max(1, dueTimeInMs.Value - Now));
var interval = ms < 0 ? DistantFutureInterval : ((double)ms / 1000);
Interop.CFRunLoopTimerSetTolerance(_timer, 0);
Interop.CFRunLoopTimerSetNextFireDate(_timer, Interop.CFAbsoluteTimeGetCurrent() + interval);
}
public long Now => _clock.ElapsedMilliseconds;
public void RequestBackgroundProcessing()
{
if(_backgroundProcessingRequested)
return;
_backgroundProcessingRequested = true;
DispatchQueue.MainQueue.DispatchAsync(_wakeUpLoopAction);
}
private void CheckSignaled()
{
bool signaled;
lock (this)
{
signaled = _signaled;
_signaled = false;
}
if (signaled)
{
Signaled?.Invoke();
}
}
[MonoPInvokeCallback(typeof(Interop.CFRunLoopObserverCallback))]
private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity)
{
if (activity == Interop.CFOptionFlags.kCFRunLoopBeforeWaiting)
{
bool triggerProcessing;
lock (Instance)
{
triggerProcessing = Instance._backgroundProcessingRequested;
Instance._backgroundProcessingRequested = false;
}
if (triggerProcessing) Instance.ReadyForBackgroundProcessing?.Invoke();
}
Instance.CheckSignaled();
}
[MonoPInvokeCallback(typeof(Interop.CFRunLoopTimerCallback))]
private static void TimerCallback(IntPtr timer)
{
Instance.Timer?.Invoke();
}
}

47
src/iOS/Avalonia.iOS/Interop.cs

@ -0,0 +1,47 @@
using System;
using System.Runtime.InteropServices;
using CoreFoundation;
using Foundation;
using ObjCRuntime;
namespace Avalonia.iOS;
// TODO: use LibraryImport in NET7
internal class Interop
{
internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
internal static NativeHandle kCFRunLoopCommonModes = CFString.CreateNative("kCFRunLoopCommonModes");
[Flags]
public enum CFOptionFlags : ulong
{
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopBeforeWaiting = (1UL << 5)
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void CFRunLoopTimerCallback(IntPtr timer);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopObserverCreateWithHandler(IntPtr allocator, CFOptionFlags activities, bool repeats, int index, ref BlockLiteral block);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopAddObserver(IntPtr loop, IntPtr observer, IntPtr mode);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopTimerCreateWithHandler(IntPtr allocator, double firstDate, double interval,
CFOptionFlags flags, int order, ref BlockLiteral block);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFRunLoopTimerSetTolerance(IntPtr timer, double tolerance);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFRunLoopTimerSetNextFireDate(IntPtr timer, double fireDate);
[DllImport(CoreFoundationLibrary)]
internal static extern double CFAbsoluteTimeGetCurrent();
}

3
src/iOS/Avalonia.iOS/Platform.cs

@ -7,6 +7,7 @@ using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia
{
@ -44,7 +45,7 @@ namespace Avalonia.iOS
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IRenderLoop>().ToSingleton<RenderLoop>()
.Bind<IRenderTimer>().ToConstant(Timer)
.Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface())
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor(

38
src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs

@ -1,38 +0,0 @@
using System;
using System.Threading;
using Avalonia.Platform;
using Avalonia.Threading;
using CoreFoundation;
using Foundation;
namespace Avalonia.iOS
{
class PlatformThreadingInterface : IPlatformThreadingInterface
{
private bool _signaled;
public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface();
public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread;
public event Action<DispatcherPriority?> Signaled;
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
=> NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick());
public void Signal(DispatcherPriority prio)
{
lock (this)
{
if(_signaled)
return;
_signaled = true;
}
DispatchQueue.MainQueue.DispatchAsync(() =>
{
lock (this)
_signaled = false;
Signaled?.Invoke(null);
});
}
}
}
Loading…
Cancel
Save