Last active
June 12, 2023 06:23
-
-
Save Adlai-Holler/91a3ba2388b6ada50647db97c23d2f02 to your computer and use it in GitHub Desktop.
How to observe CoreAnimation transaction commits as activities. Covers run-loop commits and UIKit-direct-flush commits, but not CADisplayLink scroll view commits.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Don't use this in production! | |
* | |
* Screenshot of it working: https://user-images.githubusercontent.com/2466893/27601666-f65159f0-5b24-11e7-969d-fe86103c21de.png | |
*/ | |
/** | |
* This is real, private CA API. Valid as of iOS 10. | |
*/ | |
typedef enum { | |
kCATransactionPhasePreLayout, | |
kCATransactionPhasePreCommit, | |
kCATransactionPhasePostCommit, | |
} CATransactionPhase; | |
@interface CATransaction (Private) | |
+ (void)addCommitHandler:(void(^)(void))block forPhase:(CATransactionPhase)phase; | |
@end | |
@implementation CATransaction (ASDKSwizzles) | |
+ (void)asdk_flush | |
{ | |
as_activity_scope(as_activity_create("CA transaction flush", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); | |
[self asdk_flush]; | |
} | |
@end | |
+ (void)registerCATransactionObservers | |
{ | |
// Swizzle +[CATransaction flush] with our +asdk_flush method which embeds an activity. | |
// | |
// UIKit sometimes calls [CATransaction flush] to manually commit the implicit transaction. | |
// See __UIApplicationFlushRunLoopCATransactionIfTooLate. | |
// In addition there is a manual flush at application launch time. | |
// Unfortunately, the run-loop-triggered commit does not call +flush, it calls directly into C++ | |
// so there's no easy way to embed an activity that way. | |
Method origFlush = class_getClassMethod([CATransaction class], @selector(flush)); | |
Method swizFlush = class_getClassMethod([CATransaction class], @selector(asdk_flush)); | |
method_exchangeImplementations(origFlush, swizFlush); | |
/** | |
* To use modern activity tracing (the deprecated API doesn't work fully), we have to have control of the execution | |
* scope of the activity i.e. we have to have a stack-based state struct that survives the entire activity. | |
* | |
* So we add a run loop observer just before CA's run loop observer, and from inside that observer, scope the activity | |
* and spin the run loop | |
*/ | |
static CFIndex const kCARunLoopObserverOrder = 2000000; | |
auto o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, kCARunLoopObserverOrder - 1, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { | |
auto commitActivity = as_activity_create("CA transaction commit", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT); | |
auto layoutActivity = as_activity_create("CA transaction commit - layout", rootActivity, OS_ACTIVITY_FLAG_DEFAULT); | |
/** | |
* NOTE: As of iOS 10, the API for actually getting activity tracing working is very restrictive. | |
* Even if you store the os_activity_scope_state at a lower level in the same stack, you cannot | |
* enter that scope, then return, then do some work, then leave the activity scope. So having os_activity_scope_enter | |
* inside of [CATransaction addCommitHandler:] blocks in order to separate out the commit-layout and commit-postlayout | |
* phases isn't an option. At the time of this writing, however, you can leave a scope that was started at a lower | |
* level in the stack and things will actually work so that's what we do. | |
*/ | |
__block os_activity_scope_state_s commitScopeState = {}; | |
__block os_activity_scope_state_s layoutScopeState = {}; | |
// After the layout, leave the layout scope. | |
[CATransaction addCommitHandler:^{ | |
as_activity_scope_leave(&layoutScopeState); | |
} forPhase:kCATransactionPhasePreCommit]; | |
// After the commit, leave the commit scope. | |
[CATransaction addCommitHandler:^{ | |
as_activity_scope_leave(&commitScopeState); | |
} forPhase:kCATransactionPhasePostCommit]; | |
/** | |
* We need to run CA's observer _inside_ this frame for the activity to apply, so we turn the run loop once. | |
*/ | |
as_activity_scope_enter(commitActivity, &commitScopeState); | |
as_activity_scope_enter(layoutActivity, &layoutScopeState); | |
CFRunLoopRunInMode(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()), 0, true); | |
}); | |
CFRunLoopAddObserver(CFRunLoopGetCurrent(), o, kCFRunLoopCommonModes); | |
CFRelease(o); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment