“DependencyObject-Context”例外についての仮説。

注:個人的なメモです。8割憶測です。

System.ArgumentException: 指定された DependencyObject は、この Freezable のコンテキストではありません。

この例外は、Krile Mystique のレンダリング時に不定期に発生するもので、スタックトレースを見ても

場所 System.Windows.Freezable.RemoveContextInformation(DependencyObjectcontext, DependencyProperty property)
  場所 System.Windows.Freezable.RemoveInheritanceContext(DependencyObjectcontext, DependencyProperty property)
  場所 System.Windows.DependencyObject.RemoveSelfAsInheritanceContext(DependencyObject doValue, DependencyProperty dp)
  場所 System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType)
  場所 System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
  場所 System.Windows.DependencyObject.SetCurrentValue(DependencyProperty dp, Object value)
  場所 System.Windows.Controls.Image.MeasureArrangeHelper(Size inputSize)
  場所 System.Windows.Controls.Image.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 Mystique.Views.CustomPanels.FillPanel.MeasureOverride(Size availableSize)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV)
  場所 System.Windows.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV)
  場所 System.Windows.Controls.Grid.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
  場所 System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Border.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Control.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
  場所 System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Border.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.Control.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.Controls.VirtualizingStackPanel.MeasureOverride(Size constraint)
  場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
  場所 System.Windows.UIElement.Measure(Size availableSize)
  場所 System.Windows.ContextLayoutManager.UpdateLayout()
  場所 System.Windows.ContextLayoutManager.UpdateLayoutCallback(Object arg)
  場所 System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
  場所 System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
  場所 System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
  場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
  場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.DispatcherOperation.InvokeImpl()
  場所 System.Threading.ExecutionContext.runTryCode(Object userData)
  場所 System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
  場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
  場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
  場所 System.Windows.Threading.DispatcherOperation.Invoke()
  場所 System.Windows.Threading.Dispatcher.ProcessQueue()
  場所 System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
  場所 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
  場所 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
  場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
  場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
  場所 System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
  場所 MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
  場所 MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
  場所 System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
  場所 System.Windows.Application.RunInternal(Window window)
  場所 System.Windows.Application.Run()
  場所 Mystique.App.Main()

と、要するにどうしたらいいんだろうかと頭を抱えてしまう代物だった。

最終的に内部で利用している画像キャッシュに起因するものだと分かったのだが、最近またこの例外が再燃した。これらの顛末と、そこからの僕の勝手な推測をメモっておく。

初期の問題

Krileの画像キャッシュは、平たく言えば Dictionary<Uri, BitmapImage> となっている。BitmapImageはFreezeしてしまえばスレッドセーフとして扱えるので、バックグラウンドスレッドで画像の取得とフリーズを行いこれをキャッシュに格納し、必要に応じてBitmapImageを取り出すという動作をしていた。(参照: http://msdn.microsoft.com/ja-jp/library/system.windows.freezable(v=VS.80).aspx )

画像の取得では当初は単に

return cache[uri];

としていたのだが、これではこの例外が不定期に送出されていた。そこで、

var img = cache[uri].Clone();
img.Freeze();
return img;

という実装に変更したところ、同様の例外はぱったり送出されなくなった。

というわけで、クローンしないとこの例外が飛ぶということを理解した。

最近の問題

画像の表示処理が処理のボトルネックとなっていることが分かったので、画像の表示処理を高速化したいと考えた。

種々の対策を講じたのだが、先述したClone部分にもメスを入れた。

http://msdn.microsoft.com/ja-jp/library/system.windows.freezable.getasfrozen.aspx

によると、

Freezable を固定できることを検証するには、このメソッドを呼び出す前に CanFreeze プロパティを確認する必要があります。 このメソッドを使用するのは、Clone を使用してコピーを作成してから、Freeze メソッドを使用して、作成したコピーを固定するのと同じです。

GetAsFrozen メソッドおよび GetCurrentValueAsFrozen メソッドでは、既に固定されている Freezable サブオブジェクトを複製せずに、それらを参照によってコピーするだけなので、コピー パフォーマンスが向上します。

とのことで、まさにうってつけだと感じ、先述した実装を

return (BitmapImage)cache[url].GetAsFrozen();

に変更した。

すると、また例外が送出されるようになった。

つまり

まず、初期の問題から考える。

初期の問題は、「Cloneしていない」ことによって例外が送出されていたと思われる。

また、この例外は常に

System.Windows.Freezable.RemoveContextInformation(DependencyObject context, DependencyProperty property)

から送出されている。

メソッドの具体的な動作が分からないが、このFreezableに付加していたContextInformationを取り除こうとしていることが分かる。そして、そんなコンテキストは無いということで例外が飛んだのだろう。完全に異常系で処理されているので、「本来コンテキストはあって然るべきだが、それが失われている」状態になっているということが分かる。

この手の問題で一番ありがちなのがスレッドセーフ関連だが、スタックトレースを見れば分かるとおり、このメソッドはディスパッチャスレッドから呼ばれているため、マルチスレッド絡みの問題は生じない。

残る可能性としては、Krileの構造に由来する例外だということ。Krileの構造上、BitmapImageのImageへの束縛と解放が短時間に大量に行われるという動作があり、これが悪影響を及ぼしているのではないか。

BitmapImageはCloneされず、同じURIに対しては同じインスタンスを返すようにしていたので、大量のImageから一つのインスタンスへの参照が張られている状態になる。この状態で動作している時に何らかの理由でContextInformationが破損した状態になり、これにより例外が送出、と考えると辻褄が合う。

子は複数の親を持つ場合に想定外の動作をする、ということなのだろうか?

とりあえずこれ以上は内部の実装の問題になってしまうので、「BitmapImageに限っては」複数の親に所持される場合、個々の親に対してCloneしてやる必要があるようだ。
※他のFreezableやDependencyObjectなどについては不明

最近の問題は明らかにCloneからGetAsFrozenメソッドへ切り替えたことが問題であるが、これは

GetAsFrozen メソッドおよび GetCurrentValueAsFrozen メソッドでは、既に固定されている Freezable サブオブジェクトを複製せずに、それらを参照によってコピーするだけなので、コピー パフォーマンスが向上します。

という点によるものだろう。複製によって例外送出を回避したのだから、「複製せず」なら例外が飛ぶ。

そんな感じのメモ。ImageCacheStorageを触るときは気を付ける。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です