In this article, we’ll continue to look at some of the things we might overlook in daily development, but the internal implementation actually involves running Loop as a support.

GCD

At the bottom of Run Loop and GCD each side uses the other. First let’s take a look at the GCD used in the process of reading the source code of run loop. We learned from GCD that dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue) can be used to build timers and it is more accurate than NSTimer.

There are two dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source dispatch_source Are used to wake up the current Run loop when the triggering time of the most recent timer in the _timers array of run loop mode is reached, There is also a place where dispatch_source is directly used in the __CFRunLoopRun function to build a timer to time the run loop, which is triggered when the run loop times out.

There is a platform constraint on whether to use dispatch_source or MK_TIMER to build a timer in run Loop mode. The source code uses two macros to distinguish USE_DISPATCH_SOURCE_FOR_TIMERS from USE_DISPATCH_SOURCE_FOR_TIMERS.

#if DEPLOYMENT_TARGET_MACOSX

MacOS supports both dispatch_source and MK_TIMER to build timers
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1

#else

// Other platforms only support MK_TIMER
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1

#endif
Copy the code

Here we can search USE_DISPATCH_SOURCE_FOR_TIMERS globally and see that it and USE_MK_TIMER_TOO are almost always used before and after 🦶, Both wake up the Run loop for the same target CFRunLoopTimerRef.

This is not to say that dispatch_source is not supported on platforms other than macOS. This is just a different way of waking up the current Run loop in run Loop mode when a CFRunLoopTimerRef in the _timers array reaches trigger time. Then in the __CFRunLoopRun function we see that all platforms use dispatch_source to build timers for the run loop run time.

(An aside: CFRunLoopTimerRef has always been what we call a timer, But in fact, its triggering execution completely depends on _timerPort or _timerSource in run Loop mode to wake up the current run loop, and then determine the source of the current run loop to wake up in the current run loop. If it’s a timer, it executes a CFRunLoopTimerRef callback event and updates the last time it was executed. So CFRunLoopTimerRef is called a timer but it relies on someone else to do the timing. It just has a value to record its next trigger time.

Let’s take a look at where Run Loop is used in GCD.

When dispatch_async(dispatch_get_main_queue(), block) is called, libDispatch sends a message to the main thread’s Run loop, which wakes up and takes the block from the message, This block is executed in the __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ callback. But this logic is limited to dispatches to the main thread, dispatches to other threads are still handled by libDispatch. Why doesn’t the child thread have this logic to interact with the GCD? There are two reasons:

  1. The main thread Run loop is the main thread event manager. The run loop is responsible for when the run loop handles what events. All tasks assigned to the main thread must be queued to the main thread run loop. For example: UI operations can only be performed on the main thread. Operating on the UI outside the main thread causes a lot of UI clutter and UI update delays.
  2. Child threads do not accept GCD interactions. The child thread may not have run loop enabled.

The __CFRunLoopRun function flow is already clear. For example, at the beginning of the function, it checks whether the current queue is the main thread, obtains the port of the main queue and assigns the value to dispatchPort, and then in the run loop, it checks that the awakening source is dispatchPort. Execute the task added to the main queue (_dispatch_MAIN_queue_drain).

.else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH(a); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);// TSD sets __CFTSDKeyIsInGCDMainQ to 6.
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6.NULL);
            
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif

            Static void _dispatch_main_queue_drain(dispatch_queue_main_t dq) is called internally to process tasks in the main queue
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0.NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true; }...Copy the code

Now that we’re done with the interuse of GCD and Run Loop, let’s take a look at the on-screen FPS.

FPS

FPS (Frames Per Second) is the definition in the field of graphics. FPS refers to the number of Frames transmitted Per Second of an image, generally speaking, the number of Frames of an animation or video. FPS measures the amount of information used to save and display dynamic video. The more frames per second, the smoother the displayed action will be. The maximum frame rate of iPhone screen is 60 frames per second. Some of the reasons for the screen stalling: iOS’s tricks for keeping the interface smooth

YYFPSLabel under YYKit provides a scheme to monitor FPS by adding a CADisplayLink object to the NSRunLoopCommonModes of the run loop of the main thread. The number of screen refreshes per second is then counted in the CADisplayLink object’s callback function. If you are not familiar with CADisplayLink, you can take a look at the following CADisplayLink documentation, and then we will take a look at the source code of YYFPSLabel.

CADisplayLink

CADisplayLink is a timer with the same refresh rate as the screen (but the implementation is more complicated, unlike NSTimer, which actually operates on a Source). If a long task is executed between screen refreshes, one frame will be skipped (similar to NSTimer) and the interface will feel stuck. Even a frame stalling is noticeable to the user when swiping a TableView quickly. Facebook’s open source AsyncDisplayLink is designed to solve this problem, and it also uses Run Loop internally. Let’s start by looking at the CADisplayLink documentation.

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

CADisplayLink represents a class bound to the timer that displays vsync. (where CA stands for CoreAnimation acronym, coreAnimation.h is a QuartzCore framework file containing all the header files of QuartzCore framework)

/** Class representing a timer bound to the display vsync. **/

API_AVAILABLE(ios(3.1), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos)
@interface CADisplayLink : NSObject {
@private
  void *_impl;
}
Copy the code

In the application initializes a new display link object using displayLinkWithTarget: selector: function, this function provides a target object and a screen update to invoke sel. To synchronize the run loop with the display link, add the display link object to the specified mode of the specified run loop using the addToRunLoop:forMode: function.

Once the display link is associated with the Run loop, sel on target is called when screen content needs to be updated. Target can read the timestamp property of display link to retrieve the timestamp of the last frame displayed. For example, an application that plays movies might use TIMESTAMP to calculate the next video frame to display. An application that performs its own animation might use timestamp to determine where and how to display an object in the next frame.

The Duration property provides the time interval between frames in maximumFramesPerSecond (maximum number of frames that can be displayed on the screen per second: 60). To calculate the actual frame duration between two frames, use targetTimestamp -timestamp (that is, the timestamp of the next frame minus the timestamp of the last frame). You can use the actual frame Duration in your application to calculate the frame rate of the display, the approximate display time for the next frame, and adjust the drawing behavior to prepare the next frame in time for display.

An application can disable notification (stopping the sel function that calls back to target) by setting the paused property to YES. In addition, if your application is unable to provide frames within the time provided, you may need to choose a slower frame rate. An application with a slow but consistent frame rate will feel smoother to the user than an application that jumps. You can define the number of frames per second by setting the preferredFramesPerSecond property.

When your application completes display link, it should call the invalidate function to remove it from all run loops and disassociate it from target.

CADisplayLink should not be subclassed.

displayLinkWithTarget:selector:

Returns a newly created display Link object.

// Create a new display link object for main display.
// it will call the method called 'sel' on 'target', which has the signature of '(void)selector:(CADisplayLink *)sender'.
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
Copy the code

Target: The object to be notified when the screen should update. Sel: method called at target.

The selector to be called on target must be a method with the following signature:

- (void) selector:(CADisplayLink *)sender;
Copy the code

The sender is displayLinkWithTarget: selector: the returned display link object.

The newly created display link object retains target.

addToRunLoop:forMode:

Register the display link object into the run loop.

// Add the display link object to the given mode of the given run loop. Unless paused, it will trigger each vsync until it is removed.
// Each display link object can only be added to one run loop, but can be added in multiple modes at once.
// When added to the run loop, the display link object will be implicitly retained by the run loop.
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
Copy the code

Runloop: Run loop associated with the display Link object. Mode: Adds display link to the mode of the run loop. You can specify custom modes or use one of the modes listed in NSRunLoop.

You can associate display link with multiple input modes. When the run loop is executed in the mode you specify, display Link notifies target when a new frame is required. (that is, the sel function of the target when the display link object is created.)

Run loop will retain the display link object. When the display link object is no longer used, it should be removed from all run loops by calling the invalidate function of display link. (This is very similar to NSTimer, for example, when they create their objects they all hold the target passed in, and when they are added to the run loop they are all held by the Run loop, The invalidate function is called when they are no longer needed, presumably to remove them from the run loop’s _commonModeItems collection, or the Run Loop mode’s _timers or _sources0 collection.)

removeFromRunLoop:forMode:

Removes the display Link object from the run loop of the given mode.

// Remove the display link object from the given mode of the run loop. When removed from the last registered mode, the display Link object is implicitly freed.
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
Copy the code

Runloop: runloop associated with display link. Mode: display link Run loop mode.

If the display link is no longer associated with any run loop mode, the run loop releases the display link.

invalidate

Remove the display link from all Run loop modes.

// Remove the display link object from all run loop modes (or release the display link object if it is implicitly reserved),
// Then release the target passed in when the display link object is built.
- (void)invalidate;
Copy the code

Removing the display link from all Run loop modes causes it to be released by the Run loop. Display link also releases target.

Invalidate is thread-safe, meaning that the invalidate function can be called from a thread “separate” from the thread running display Link.

duration

(Read Only) Screen refresh Updates the interval between frames. (The Duration property provides the time interval for each frame below the maximum screen refresh rate (maximumFramesPerSecond 60), which can be used to get the frame rate in an application.)

@property(readonly, nonatomic) CFTimeInterval duration;
Copy the code

Duration is undefined until target’s sel is called at least once. (An application can calculate the amount of time required to render each frame by multiplying Duration by frameInterval.)

Duration provides the time between frames, the time between each screen refresh. Duration is only a approximate time. If the CPU is busy with other calculations, there is no guarantee that the screen will be drawn at the same frequency, thus skipping several callbacks.

frameInterval

(deprecated) Number of frames that must pass before display Link notifies target again.

@property(nonatomic) NSInteger frameInterval;
Copy the code

The default value is 1, which causes the application to be notified with the refresh rate displayed. If the value is set to a value greater than 1, display Link notifies the application at a fraction of the native refresh rate. For example, setting the interval to 2 causes the display link to fire every other frame, providing half the frame rate.

Setting this value to less than 1 results in undefined behavior.

timestamp

The timestamp associated with the last frame displayed. This property returns the timestamp of the last screen refresh. For example, a video playback application can calculate the next frame by obtaining the time stamp of the previous frame.

// The current time and duration of the display frame associated with the most recent target call. Time is represented using normal Core Animation conventions, which convert Mach Host time to seconds.
@property(readonly, nonatomic) CFTimeInterval timestamp;
Copy the code

Target should use the value of this property to calculate what should be displayed in the next frame.

preferredFramesPerSecond

The preferred frame rate for the display link callback.

// Define the callback rate this display link wants, in frames per second. If it is set to zero (the actual default is used), the native default frame rate (60) is used.
// display link will do its best to try to call back to the value of the preferredFramesPerSecond set.
@property(nonatomic) NSInteger preferredFramesPerSecond API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));
Copy the code

When you specify the preferred frame rate for display Link, it looks at the capabilities of the hardware and what other tasks your game or app might be doing, Notify the target at the closest possible rate (that is, call target’s sel function back and forth with a value as close to preferredFrame perSecond as possible). The actual frame rate selected is usually a factor in the maximum screen refresh rate to provide a consistent frame rate. For example, if the screen’s maximum refresh rate is 60 frames per second, this is also the maximum frame rate that display Link sets to the actual frame rate. However, if you require a lower frame rate, display Link may choose 30, 20, or 15 frames per second or some other rate as the actual frame rate. Try to choose a frame rate that your application can maintain all the time.

The default value is 0. When this value is 0, the preferred Frame Rate uses the maximum refresh rate of the display (60 frames per second), as shown in the maximumFramesPerSecond property. For more information, see Setting Refresh Rates on ProMotion and Traditional Displays.

If an operation on an object cannot be provided within a particular frame rate, it can be resolved by lowering the frame rate. An app that has a steady but slow frame rate is much smoother than a frame-hopping app. You can use preferredFrame esperSecond to set the refresh times per second. The preferredFrame esperSecond defaults to the screen’s maximum frame rate, which is currently 60.

The actual screen frame rate will differ somewhat from the preferred frame rate, resulting from the interaction between the set value and the screen’s maximum frame rate. The rules are as follows: if the screen’s maximum frame rate is 60, the actual frame rate can only be 15, 20, 30, or 60. If the value is greater than 60, the actual screen frame rate is 60. If the value is between 26 and 35, the actual frame rate is 30. If set to 0, the highest frame rate is used.

maximumFramesPerSecond

MaximumFramesPerSecond is a read-only property of UIScreen that represents the maximum number of frames per second that the screen can display.

@property(readonly) NSInteger maximumFramesPerSecond;
Copy the code

On iOS, the maximum number of frames per second is usually 60. For tvOS devices, this value may vary depending on the hardware features of the connected screen or the resolution chosen by the user on the Apple TV.

paused

Paused is a Boolean value that indicates whether the display link to target notification (sel callback) has been paused.

/* When true the object is prevented from firing. Initial state is false. */
@property(getter=isPaused, nonatomic) BOOL paused;
Copy the code

The default value is NO. If the value is YES, the display link does not send notification (callback SEL) to target.

Paused is thread-safe, which means that it can be set from a separate thread from the one running display Link.

targetTimestamp

(read-only) Property added after iOS 10.0, timestamp associated with the next frame displayed.

@property(readonly, nonatomic) CFTimeInterval targetTimestamp
API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));
Copy the code

You can use Target TIMESTAMP to cancel or suspend long-running processes that may exceed the available time between frames in order to maintain a consistent frame rate. (targetTimestamp and timestamp seem to correspond, with one representing the timestamp of the next frame and one representing the timestamp of the previous frame.)

That’s the end of the CADisplayLink document. We create a CADisplayLink object and add it to the main thread of the current thread.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)displayLinkAction:(CADisplayLink *)displayLink {
    NSLog(@"📧 📧 % @", displayLink);
    NSLog(@"duration: %lf timestamp: %lf targetTimestamp: %lf frameInterval: %d preferredFramesPerSecond: %d maximumFramesPerSecond: %d", displayLink.duration, displayLink.timestamp, displayLink.targetTimestamp, displayLink.frameInterval, displayLink.preferredFramesPerSecond, UIScreen.mainScreen.maximumFramesPerSecond);
}
// Console print:📧 📧 < CADisplayLink:0x6000008ec2c0>
duration: 0.016667 timestamp: 366093.060335 targetTimestamp: 366093.077002 frameInterval: 1 preferredFramesPerSecond: 0 maximumFramesPerSecond: 60
Copy the code

Print the CADisplayLink object properties, and you can see that the duration is the familiar 0.016667 seconds (16.7 milliseconds), targetTimestamp – timestamp is about 16.7 milliseconds, PreferredFramesPerSecond has a value of 0, which is actually the screen’s maximum refresh rate of 60 frames per second. On the iPhone, maximumFramesPerSecond is 60.

Make a breakpoint inside the displayLinkAction function above and print the current function call stack when it enters the breakpoint:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001007b3b1e Simple_iOS`-[ViewController displayLinkAction:](self=0x00007fc4ab601df0, _cmd="displayLinkAction:", displayLink=0x00006000013cc090) at ViewController.m:382:27
    frame #1: 0x00007fff2afeb266 QuartzCore`CA::Display::DisplayLink::dispatch_items(unsigned long long.unsigned long long.unsigned long long) + 640
    frame #2: 0x00007fff2b0c3e03 QuartzCore`display_timer_callback(__CFMachPort*, void*, long.void*) + 299
    frame #3: 0x00007fff23b9503d CoreFoundation`__CFMachPortPerform + 157
    frame #4: 0x00007fff23bd4bc9 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41 // ⬅️ executes the source1 callback
    frame #5: 0x00007fff23bd4228 CoreFoundation`__CFRunLoopDoSource1 + 472
    frame #6: 0x00007fff23bced64 CoreFoundation`__CFRunLoopRun + 2516
    frame #7: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
    frame #8: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
    frame #9: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
    frame #10: 0x00000001007b486d Simple_iOS`main(argc=1, argv=0x00007ffeef44bd60) at main.m:76:12
    frame #11: 0x00007fff5227ec25 libdyld.dylib`start + 1
(lldb)
Copy the code

See that CADisplayLink’s callback function is executed via the source1 callback. Then print the run loop for the current thread and you can see that a callback function is created which is _ZL22display_timer_callbackP12__CFMachPortPvlS1_ source1.

.0 : <CFRunLoopSource 0x600003b11140 [0x7fff80617cb0]>{signalled = No, valid = Yes, order = - 1, context = <CFMachPort 0x6000039146e0 [0x7fff80617cb0]>{valid = Yes, port = 6507, source = 0x600003b11140, callout = _ZL22display_timer_callbackP12__CFMachPortPvlS1_ (0x7fff2b0c3cd8), context = <CFMachPort context 0x6000035200d0>}}...Copy the code

CADisplayLink is internally driven by Source1.

The above part is the study of CADisplayLink part, let’s look at the concrete implementation of YYFPSLabel.

YYFPSLabel Frame rate monitoring

YYFPSLabel

#import "YYFPSLabel.h"
//#import <YYKit/YYKit.h>
#import "YYText.h"
#import "YYWeakProxy.h"

#define kSize CGSizeMake(55, 20)

@implementation YYFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
    UIFont *_font;
    UIFont *_subFont;
    
    NSTimeInterval _llll;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (frame.size.width == 0 && frame.size.height == 0) {
        frame.size = kSize;
    }
    self = [super initWithFrame:frame];
    
    self.layer.cornerRadius = 5;
    self.clipsToBounds = YES;
    self.textAlignment = NSTextAlignmentCenter;
    self.userInteractionEnabled = NO;
    self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
    
    _font = [UIFont fontWithName:@"Menlo" size:14];
    if (_font) {
        _subFont = [UIFont fontWithName:@"Menlo" size:4];
    } else {
        _font = [UIFont fontWithName:@"Courier" size:14];
        _subFont = [UIFont fontWithName:@"Courier" size:4];
    }
    
    // Create a CADisplayLink object and add it to the NSRunLoopCommonModes of main RunLoop.
    // Because CADisplayLink object will retain target, so we use [YYWeakProxy proxyWithTarget:self] as the intermediate bridge,
    // Self assigns the weak attribute _target to YYWeakProxy, that is, self is weakly referenced by YYWeakProxy,
    / / and re-written YYWeakProxy forwardingTargetForSelector: function, direct return _target object to receive the treatment to YYWeakProxy message,
    // Pass CADisplayLink's callback function tick: to YYFPSLabel for processing.
    
    // (self holds _link, _link holds YYWeakProxy, YYWeakProxy weakly references self, thus breaking the original reference loop)
    
    _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
    return self;
}

- (void)dealloc {
    // Make sure to call CADisplayLink's invalidate function when destroying
    [_link invalidate];
}

- (CGSize)sizeThatFits:(CGSize)size {
    return kSize;
}

- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
    
        // When the tick function is first called, _lastTime records the timestamp of the first frame
        _lastTime = link.timestamp;
        
        return;
    }
    
    // Count the number of tick calls
    _count++;
    
    // timestamp is the timestamp of the current frame, minus the timestamp of the last statistical frame rate, when the interval is greater than or equal to 1 second before the statistical frame rate,
    // The frame rate is measured once a second (there is no need to count the frame rate too often)
    NSTimeInterval delta = link.timestamp - _lastTime;
    
    // If the time is greater than or equal to 1 second, calculate the frame rate and refresh the frame rate displayed on YYFPSLabel once
    if (delta < 1) return;
    
    // Update _lastTime to the timestamp of the current frame
    _lastTime = link.timestamp;
    
    // The number of tick calls divided by the interval is the current frame rate
    float fps = _count / delta;
    
    // The number of tick calls is cleared to 0 (start the next frame rate count)
    _count = 0;
    
    CGFloat progress = fps / 60.0;
    UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
    
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS", (int)round(fps)]];
    [text yy_setColor:color range:NSMakeRange(0, text.length - 3)];
    [text yy_setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3.3)];
    text.yy_font = _font;
    [text yy_setFont:_subFont range:NSMakeRange(text.length - 4.1)];
    
    self.attributedText = text;
}

@end
Copy the code

The tick function internally uses the timestamp of each frame recorded in the CADisplayLink object to calculate the frame rate per second, which is enough to monitor the sliding frame rate in daily development.

Refer to the link

Reference link :🔗

  • Runloop source
  • Run Loops official documentation
  • Complete guide to iOS RunLoop
  • IOS source code parsing: Runloop underlying data structure
  • IOS source code: Runloop operating principle
  • Understand RunLoop in depth
  • IOS Basics – Dive into RunLoop
  • A core runloop source code analysis
  • NSRunLoop
  • Get to the bottom of iOS – Understand RunLoop in depth
  • RunLoop summary and interview
  • Runloop- Actually develop the application scenario you want to use
  • RunLoop source code read
  • do {… } The role of while (0) in the macro definition
  • CFRunLoop (cF-1151.16)
  • Operating system big-endian mode and small-endian mode
  • CFBag
  • Mach_absolute_time use
  • IOS probe mach_absolute_time
  • IOS multithreading — RunLoop and GCD, AutoreleasePool You should know about iOS multithreading, NSThread, GCD, NSOperation, and RunLoop
  • Mach primitives: Everything is message-mediated
  • Operating system dual mode and interrupt mechanism and timer concept
  • RunLoop life cycle –(9)
  • From the NSRunLoop
  • When will runloop and Autorelase objects and Autorelease pools be released
  • Memory management: Autoreleasepool and Runloop
  • Objective-c AutoreleasePool association with Runloop
  • IOS development – Custom input Source in Runloop
  • IOHIDFamily
  • Summary of iOS caton monitoring scheme
  • IOS tips for keeping the interface smooth