I'm having some problems debugging what appears to be a KVO-related EXC_BAD_ACCESS exception on 10.6.6.
I've set up an observer on the position property of a CALayer subclass. This particular layer is the content layer for a custom CALayer scroll layer. Basically, when the user drags the content layer around, I would like some KVO notifications to occur on the position changes for the layer so that I can update some custom CALayer scrollers.
The above all works fine.
Now, I've added a rubber-band effect à la iOS, so that when the user scrolls passed the limits of the content area, some resistance is encountered, and when the user lets go of the mouse, the content layer snaps back to the limit position in both x and y dimensions. Vertical snap-back works like a charm. However, for some bizarre reason, any amount of horizontal snap-back cause the above exception to occur.
What I don't understand is why precisely the same code path (vertical snap-back vs. horizontal-snap back) would cause these two situations to behave differently? Here is the stack trace:
0 0x7fff810bbc2b in CALayerTransactionFlagsLocation_
1 0x7fff810bbe00 in CALayerMark
2 0x7fff810bd394 in propertyDidChange
3 0x7fff810bcbff in endChange
4 0x7fff810bc9f3 in -[CALayer setPosition:]
5 0x100038578 in -[MRContextualScrollLayer scrollDidEnd] at MRContextualScrollLayer.m:280
...
scrollDidEnd is the method that is called when the user let's go of the mouse, and a snap-back calculation is performed to determine how to reposition the content layer for the snap-back effect.
I've tried turning on NSZombieEnabled by setting the environment variable in the arguments of the executable in Xcode, but it has no effect, i.e., no additional information is logged. This leads me to believe that the problem is not a memory over-release issue, where an object is being accessed that shouldn't be.
The final line in the scrollDidEnd method that results in the KVO notification getting posted (which then results in the crash) is simply:
_contentLayer.position = CGPointMake(_contentLayer.position.x + snapBackOffset.x,
_contentLayer.position.y + snapBackOffset.y);
Does anyone have any ideas or helpful hints?
Discovery!
Setting a breakpoint in the -dealloc method and stepping through, I see in Xcode that
#3 0x7fff810bf352 in CA::Transaction::commit
is getting messaged just before the crash. This makes sense, since I'm disabling and re-enabling CA animation in the scroller update code using the pattern:
// Disable animation temporarily.
[CATransaction flush];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
//
// Update position of CA scrollers.
//
// Re-enable animation.
[CATransaction commit];
If I remove this CATransaction animation disabling logic, everything works fine.
It would appear that explicit transactions (such as the wrapper pattern above) are not supported within the context of implicit CALayer transactions tied to KVO, even through the docs say that nested transactions are supported. The added element of KVO here is what causes the problem.
From the CATransaction documentation:
CATransaction is the Core Animation mechanism for batching multiple layer-tree operations into atomic updates to the render tree. Every modification to a layer tree must be part of a transaction. Nested transactions are supported.
Core Animation supports two types of transactions: implicit transactions and explicit transactions. Implicit transactions are created automatically when the layer tree is modified by a thread without an active transaction and are committed automatically when the thread's run-loop next iterates. Explicit transactions occur when the the application sends the CATransaction class a begin message before modifying the layer tree, and a commit message afterwards.
In short, if you want to have explicit nested transactions behave correctly within the context of KVO, make sure the transaction that initiated the KVO is not implicit. Alternatively, ensure the nested transaction is implicit as well.