How Apple Hooks Entire Frameworks
How Does the Main Thread Checker Work?
Introduction to the Main Thread Checker
- The main thread checker is a developer tool by Apple that monitors method calls in iOS applications, ensuring UI-related operations are executed on the main thread.
- It provides warnings when methods are called from incorrect threads, which is crucial for maintaining app stability and performance.
Challenges of Implementing Checks
- Implementing checks across Apple's extensive UIKit framework poses significant challenges due to the sheer number of methods involved.
- Any modifications must not affect production code; even minor changes could disrupt app performance globally.
Method Swizzling Explained
- Instead of altering existing methods, Apple uses method swizzling to inject functionality at runtime without modifying original implementations.
- This technique allows developers to add checks conditionally based on whether the main thread checker is enabled.
Practical Implementation of Swizzling
- A new Objective C method can be created to check if operations are running on the main thread, utilizing swizzling to redirect calls seamlessly.
- By importing Objective C runtime headers and using
method_exchangeImplementations, developers can swap method implementations dynamically.
Scaling Method Checks
- To handle multiple methods efficiently, a single routing function can be created that directs all relevant method calls through a common check.
- This approach avoids redundancy and maintains scalability while ensuring that all necessary checks are performed without impacting performance.
Main Thread Check Implementation
Refactoring Method Swizzling
- The implementation is refactored to move the main thread check out of the messages class, simplifying method routing.
- By using method swizzling, both
messages.helloandmessages.goodbyecan be routed to a shared main thread check without altering individual methods.
- The challenge arises in routing multiple function calls back to their original implementations while maintaining necessary context.
Objective-C Method Mechanics
- Objective-C methods are essentially C-style functions with two implicit parameters:
self(the object instance) and_cmd(the method name).
- This structure allows for easy swizzling since it updates how methods are looked up in memory based on these parameters.
Logging and Routing Back
- Updating log statements to include class and method names provides insight into which methods are being called during execution.
- A strategy is proposed where method names can be converted to strings for dynamic routing back to the correct implementation.
Storing Original Implementations
- To maintain access to original implementations after swizzling, a dictionary is created that maps class and method names to their respective implementations.
- A new function pointer type definition is introduced, allowing for proper invocation of original methods with the required parameters.
Runtime Method Swizzling
- After implementing the main thread checker, it’s noted that hard-coded information about methods could be avoided by dynamically retrieving them at runtime.
- Using
class_copyMethodList, all methods in a given class can be swizzled without needing specific code for each one.
Handling Parameters in Methods
Challenges with Parameterized Methods
- A significant issue arises when dealing with methods that require parameters; improper handling leads to crashes due to bad memory access.
Understanding Method Swizzling and Main Thread Checker
The Limitations of NSInvocation
- Objective C's NSInvocation can package parameter information for method forwarding, but it is significantly slower—about 100 times slower than standard method calls. This makes it impractical for routing UI calls, even in development tools.
Exploring Subclass Behavior with Method Swizzling
- A new subclass called
messages v2is created to override thesay hellomethod, which logs a message before calling its superclass implementation. This setup aims to ensure that bothmessagesandmessages v2are swizzled for main thread checks.
Debugging Method Calls in Swizzled Classes
- When invoking the overridden
say hello, the debugger shows that the main thread checker identifies a call on an instance ofmessages v2. It performs its check and looks up the original implementation.
Infinite Loop Issue with Superclass Calls
- Upon calling
super say hello, since both methods share the same name and object type, it leads back into the overridden method due to swizzling. This results in an infinite loop where only reminders to subscribe are displayed repeatedly.
Challenges with Shared Handlers
- The primary issue arises from insufficient data to route back correctly after swizzling methods. Knowing just the object type and method name isn't enough; specific context prior to swizzling is crucial.
Reverse Engineering Apple's Implementation
- To understand how Apple handles these issues, one must delve into assembly-level details. An earlier article discussed reverse engineering Apple's logic, noting changes due to different CPU architectures.
Identifying Main Thread Checker Functionality
- Triggering a UI-related error on a background thread reveals distinctive console output from the main thread checker. Searching this string in terminal leads to discovering its dynamic library (
lib main thread checker).
Runtime Behavior of Main Thread Checker Library
- Disabling the main thread checker shows that its library is not loaded during execution, indicating that this functionality operates entirely at runtime based on whether or not this library is active.
Analyzing Library Functions with IDA
- Using IDA disassembler reveals multiple tiny functions within
lib main thread checker, suggesting efficient handling mechanisms through numerous small operations rather than large monolithic functions.
Investigating Initialization Logic
- The initial function within the binary reads various settings from environment variables, including suppression files related to handling errors triggered by background threads accessing UI components.
Understanding Trampolines in UI Frameworks
Overview of UI Frameworks and Initialization
- The discussion begins with an overview of specific UI frameworks: Appit for Macs and UIKit Core for iOS, highlighting their roles in application development.
- A function called
initialize trampolinesis introduced, which takes a function as a parameter and saves it toregistered callback, indicating the setup process for handling main thread checks.
Main Thread Checks
- The transcript explains that the main thread check occurs within the
checker Cfunction. If not on the main thread, it triggers an assertion failure message related to UI API calls made from background threads.
- Following this, there’s a growth in data structure capacity before invoking
swizzle classes, which logs swizzling activities and retrieves methods from specified classes.
Method Swizzling Process
- The method swizzling process involves selecting which methods to swizzle while skipping potentially dangerous ones related to memory management (e.g., retain/release).
- The preparation phase includes calling
prepare swizzler, passing class information and method details to retrieve method implementations stored in memory.
Trampoline Functionality Explained
- An explanation of how trampolines are structured reveals they serve as small functions that facilitate transitions between different parts of code, enhancing modularity.
- Each trampoline is designed to handle its own mini-function rather than relying on a shared handler, allowing for more granular control over method execution.
Assembly Language Integration
- The concept of trampolines is further clarified by discussing their role in assembly language; they perform minimal computations before redirecting execution flow.
- An example illustrates how these trampolines operate behind the scenes when calling functions across different frameworks without affecting stack traces or complicating execution paths.
Creating Custom Trampolines
- Instructions on creating custom assembly files (
trampolines.s) are provided, emphasizing alignment requirements for ARM architecture during assembly.
- A label named
trampoline startis established as externally visible, setting up the groundwork for implementing custom trampoline functionality within projects.
Understanding Trampoline Handlers in Assembly
Overview of Trampoline Handlers
- The discussion begins with the concept of creating multiple trampoline handlers, suggesting the use of a repeat directive for efficiency, proposing to generate 100 instances.
- The speaker mentions disabling variable names in the disassembler for clarity and notes that standard assembly function setups typically involve storing two registers onto the stack.
Function Setup and Register Management
- Registers X29 and X30 are highlighted as crucial; X29 holds the caller's frame pointer while X30 stores the return address for functions.
- It is common practice to back up these registers at the start of a function and restore them before completion, ensuring proper execution flow.
Storing Registers and Preparing Function Calls
- A significant number of registers (X0 to X8, Q0 to Q7) are stored on the stack, indicating preparation for further operations or function calls.
- In C-based languages, parameters are passed through registers starting from X0. For example, calling an "add" function would involve setting values in these registers prior to invocation.
Parameter Passing in Assembly Functions
- The first parameter for a function call is derived from register X16, which contains addresses set by previous trampolines.
- The second parameter relates to register X30, which indicates where control should return after executing a function. This highlights how assembly manages control flow between functions.
Analyzing Trampoline C Functionality
- The analysis reveals that trampoline C takes two parameters: one being the trampoline address and another being the return address from its caller.
- The setup involves retrieving memory addresses using ARM instructions; this process includes loading symbols into specific registers like X8.
Conclusion on Assembly Practices
- A typical prologue structure is noted within trampoline C where several registers are saved initially. This sets up necessary conditions for executing subsequent operations effectively.
Understanding Trampoline Offsets and Callbacks
Calculating the Trampoline Offset
- The parameter represents the address of the trampoline being accessed. By subtracting the first trampoline's address from this, we save the offset into register x8. This effectively gives us the index of which trampoline was used.
- If arriving through different trampolines, offsets will be 0 for the first, 8 for the second, and so forth. This indicates how many bytes higher each subsequent trampoline is located.
Logical Shifts and Rounding Behavior
- A series of operations follows where we add seven to our offset before comparing it with zero. This may seem odd but aids in achieving correct rounding behavior when using a logical shift right (LSR) instruction later on.
- The LSR operation divides our number by two three times, effectively dividing it by eight overall—this is crucial for determining which trampoline index we are referencing based on its byte size.
Accessing Trampoline Data
- After calculating the index, we access a data array that holds information about functions swizzled earlier, including original method implementations and their associated names and classes. The fourth slot remains empty intentionally.
- In assembly code, we multiply our trampoline index by 32 (shifting left five times) to find corresponding bytes in this data array—a straightforward array access despite its complexity in assembly language representation.
Structuring Trampoline Data in Swift
- A new struct will be created to store trampoline data mirroring Apple's implementation: original method implementation, selector name, and class details are included while ensuring efficient lookup via an indexed shared array for each trampoline's data storage.
- In our assembly code context, a pointer to this stored data is loaded into register X19 before calling a registered callback function that checks if we're operating on the main thread or not—passing along necessary context about what method was invoked initially.
Contextual Parameters in Callbacks
- The registered callback receives two parameters: one being our previously moved trampoline data (in X0), while another parameter set up earlier contains the return address of whoever called this trampoline function initially (in X1). This dual parameter setup enhances error handling capabilities within callbacks.
- If a check determines that we're not on the main thread after invoking this callback function, both parameters are passed to an error handler function that assesses whether calls originated from Apple’s system frameworks—crucial for managing errors without overwhelming developers with unnecessary alerts from internal calls made by Apple methods themselves.
Understanding Trampolines in Objective-C Swizzling
Shared Callback and Class Wrapping
- The implementation involves creating a shared callback that transmits trampoline data along with the caller's address, enhancing accessibility by wrapping it in a class.
- This design allows different parts of the app to dictate the behavior of checks, promoting modularity and separation of concerns.
Function Checking and Context Management
- The code will check if execution is on the main thread while logging caller information for debugging purposes.
- After checking, the process retrieves the original method's pointer from trampoline data, ensuring correct routing back to the intended function.
Register Management During Execution
- The x0 register is used to return values; it points to the original function implementation after loading from trampoline data.
- Apple addresses context issues by using trampolines that provide additional context for swizzled functions, preventing collisions through indexed lookups rather than method names.
Parameter Handling in Assembly
- Registers (x0-x8 for parameters and Q registers for floating-point values) are backed up before calling other functions, allowing restoration post-execution without concern for parameter count or content.
- By restoring all registers to their original state before returning to the swizzled method, Apple ensures seamless operation as if no modifications were made.
Implementing New Swizzling Techniques
- A new swizzling implementation replaces old methods with trampolines while storing original implementations in a shared array.
- Each call updates pointers to ensure methods are replaced correctly with available trampolines while maintaining necessary metadata about each method.
Implementing a Main Thread Checker in iOS
Swizzling Methods for UI Kit
- The speaker discusses the ability to swizzle methods in subclasses, emphasizing that this design can hook into any method generically, demonstrating its flexibility beyond demo classes.
Expanding Swizzling to All UIKit Classes
- The approach involves looking up every class within the binary image of UIKit and applying the swizzle function universally across all classes, enhancing the main thread checker’s functionality.
Trampolines Requirement for Method Swizzling
- A significant number of trampolines (113,018) are required for effective swizzling of UIKit methods on a specific iOS version. This setup successfully identifies issues like background thread calls from Apple’s internal methods.
Custom Assembly Code Implementation
- The implementation relies on clever assembly code rather than special APIs exclusive to Apple. Other companies have adopted similar designs based on previous insights shared by the speaker.
Challenges with Trampoline Count
- A critical question arises regarding how many trampolines are necessary for developers creating open-source versions that must remain compatible with future UIKit updates without crashing apps due to outdated frameworks.
Balancing Size and Functionality
- Developers face challenges in determining an optimal trampoline count; too many could bloat app size significantly while too few might not cover all use cases.
Dynamic Memory Allocation Issues
- Creating an ideal library would allow dynamic growth without predefining trampoline numbers, but iOS's restrictions complicate runtime code creation.
Runtime Code Creation Techniques
- To create new functions at runtime on macOS (not typically allowed on iOS), one can use memory mapping techniques like
mmapto allocate writable memory pages for custom instructions.
Security Considerations in Memory Management
- While allocating memory, it is crucial to manage permissions correctly; writable and executable memory cannot coexist due to potential security risks.
This structured summary captures key concepts discussed in the transcript while providing timestamps for easy reference back to specific points in the video.
How to Create Functions at Runtime in Swift
Writing Bytes into Memory
- The process begins by assembling instructions into binary and opening it in a hex editor. The bytes are copied back into the code, specifically into allocated memory.
- Swift prompts for confirmation when treating a pointer as a function, which is defined as a C-style function that takes no arguments and returns an integer.
Executing the Function
- A crash occurs when attempting to run the function due to non-executable memory. To resolve this, the memory protections must be changed using
mprotectto make it readable and executable.
- After debugging, the expected bytes are confirmed in function memory, leading to successful execution of a runtime-created function that returns seven.
Challenges on iOS
- While creating trampolines at runtime is feasible on macOS, iOS has stricter memory protections preventing such actions without special entitlements from Apple.
- This limitation necessitates pre-defining trampolines within the binary rather than generating them dynamically during runtime.
Alternative Strategies for Trampolines
- An alternative approach involves remapping existing code instead of creating new code at runtime. This concept was highlighted in Landon Fuller’s blog regarding Objective-C features introduced in iOS 4.
- Modern operating systems utilize virtual memory allowing applications to have control over how their address space maps to physical RAM.
Memory Remapping Insights
- Virtual memory enables different views of the same underlying RAM, facilitating efficient data structures like circular buffers without exposing raw pointers directly.
- Although there are practical applications for memory remapping, using it for executable code can lead to unexpected behavior due to altered execution paths.
Debugging Assembly Functions
- For testing purposes, assembly instructions are temporarily replaced with a simple function returning the square root of seven.
- Stepping through this process reveals how control flows from assembly functions through trampolines before reaching library functions like square root.
Memory Remapping in iOS: Understanding the Process
Introduction to Memory Remapping
- The speaker discusses executing calculations and printing out a square root value, introducing the concept of memory remapping.
- Emphasizes the importance of aligning functions to a page boundary for easier mathematical operations, as remapping deals with entire memory pages.
VM Remap Function
- Introduces the
VM remapfunction, which allows creating new memory mappings within the current process. It requires several parameters but is straightforward for their use case.
- After running the code, it shows that two pointers point to different addresses in memory but reference identical data, effectively creating a runtime copy of a function.
Debugging Issues Encountered
- When attempting to run the newly created pointer as a function, issues arise; the debugger fails to recognize its context leading to crashes during execution.
- Highlights that on an actual iOS device (as opposed to a simulator), security measures prevent even reaching execution due to improved iOS security protocols.
Permissions and Objective-C Runtime Insights
- Discusses how despite modern restrictions, certain Objective-C features still work on physical devices. This includes insights from older implementations that remain valid today.
- Traces back through Apple's original implementation of related functions revealing similarities with their own codebase and confirming functionality despite changes over time.
Kernel Requirements and Workarounds
- Identifies kernel requirements mandating full text segment remaps rather than partial ones. This is crucial for understanding limitations in their approach.
- Explains two potential solutions: either remap entire binaries or create separate dynamic libraries for trampolines, allowing more efficient management without excessive resource usage.
Understanding Memory Mapping and Code Execution
Objective C Runtime Behavior
- The Objective C runtime differentiates behavior based on whether the app is a Steam app, which influences how trampolines are loaded.
- This approach may lead to complications when integrating dependencies, as it could require managing multiple frameworks or packages.
Exploring Alternative Solutions
- A proposed "secret third option" involves remapping not just memory but also files, leveraging existing binaries on the device.
- The goal is to locate the binary file containing specific functions (e.g., square 7 function) using the
DL addressfunction for path retrieval.
Memory Remapping Process
- The process includes opening the binary file and using a memory mapping call (
MAPAP) to allocate executable memory from that file.
- Determining the correct offset for mapping requires identifying where in the binary file the function's instructions reside.
Binary Analysis Techniques
- Using a disassembler allows verification of function locations within the binary; for instance, confirming that square 7 resides at hex 8000.
- Hardcoding offsets is unreliable; instead, parsing through binary files at runtime can yield necessary values, although it's cumbersome.
Ensuring Consistent Function Locations
- To guarantee consistent placement of code in binaries, a linker order file can be created to specify code arrangement during compilation.
- By ensuring that custom assembly code appears first in the binary, developers can reliably access it starting from hex 4000.
Executing Remapped Code
- After implementing these changes, executing remapped code does not trigger kernel restrictions as seen previously with direct memory mappings.
- This method resolves issues related to redundant data management and simplifies trampoline creation without needing multiple binaries.
Addressing Function Call Crashes
- Despite progress with remapping execution, crashes occur when calling other functions due to relative addressing in assembly instructions.
- Understanding how assembly instructions reference other parts of codebase is crucial; disassemblers simplify this by showing branch destinations.
Understanding Memory Addressing in Assembly
The Challenge of Relative Addressing
- The instruction translates to "branch to square root," indicating a jump to an address calculated by adding a branch offset to the instruction's address, which is at hex 404.
- When the instruction was assembled, it correctly pointed to the square root function. However, relocating it causes jumps into random memory locations, leading to crashes due to lack of context.
- The issue arises from needing assembly code that can call a shared Swift handler regardless of its memory location; relative instructions complicate this as they point incorrectly when moved.
Controlling Memory for Functionality
- To ensure functions point to useful memory, an assembly function is created that reads from an address offset (hex 4000) from its current location and returns that value.
- If the function resides at hex 14,000, control over both this and the target memory (hex 18,000) is necessary for valid data retrieval.
Remapping Functions and Data Pages
- By remapping functions with specific addresses in mind, we can allocate writable memory directly after executable code. This ensures data needed by functions is stored nearby.
- An assertion checks if the data pointer is correctly positioned after the function pointer. Writing an integer value confirms successful setup and retrieval.
Handling Unpredictability in Memory Allocation
- iOS may frequently trigger assertions due to dynamic allocation/deallocation of memory pages; neighboring pages might not be free or could be fragmented.
- To mitigate unpredictability, requesting two consecutive pages guarantees enough space for both executable code and associated data.
Finalizing Trampoline Code Setup
- A flag can enforce strict adherence to requested addresses during mapping; this allows overwriting existing content safely when necessary.
- Adjustments are made in code logic so that reading occurs from the correct page before executing functions—ensuring consistent operation across different loads in memory.
Trampoline Setup and Memory Management
Introduction to Trampolines
- The discussion begins with the creation of trampolines, focusing on remapping executable code into different memory areas.
- A new method called
allocate trampolinesis introduced, which will handle the setup for these trampolines.
Memory Allocation Process
- The process involves obtaining a file descriptor for the binary file containing trampolines and moving existing setup code to the class initializer.
- Two pages of writable memory are allocated; one page stores data while the other contains the trampoline handler and its implementations.
Calculating Trampoline Addresses
- Each trampoline's address is calculated using fixed offsets based on its index, ensuring proper placement in memory.
- The number of trampolines that can fit within a page is determined by dividing the total bytes available by the size of each trampoline (8 bytes).
Handling Remapped Pages
- To optimize space, it’s essential to calculate how many bytes are used by the handler before determining how many additional trampolines can fit.
- After calculating remaining bytes, this information helps derive how many trampolines can be accommodated in a given remapped page.
Finalizing Trampoline Management
- A label marking the end of all trampolines is added for better tracking within Swift, allowing easy access to total byte usage.
- When
allocate trampolinesis invoked, it reserves memory and returns pointers to newly created trampoline values ready for use.
Swizzling Methods with Trampolines
- Old management code is removed as a new property holds currently available trampolines. If none are available during swizzling, more will be allocated.
Testing and Debugging
- The next step involves testing this setup through swizzling example methods to observe behavior during execution.
- Internal calls like
obsc message sendare examined to understand how function implementations are located in memory.
Understanding the Main Thread Checker and Trampolines
Overview of the Main Thread Checker Functionality
- The main thread checker backs up all registers to avoid interference during function calls, then restores them post-execution.
- The implementation is located, leading to a branch into one of the trampolines, indicating successful swizzling with
message send.
Execution Flow in Trampolines
- Each trampoline's branch instruction points to a specific location within remapped code, ensuring continuity despite remapping.
- The assembly handler stores registers before branching to an external function, which may lead to garbage data if not handled correctly.
Addressing Remapping Challenges
- LLDB fails to label branches correctly when they point outside remapped areas, risking crashes due to invalid memory access.
- A writable data page allows for additional information storage; pointers can be set up for swift trampoline handlers.
Implementing Pointer Management
- By writing the address of the Swift trampoline handler just before a page of trampolines, it facilitates correct function calls across memory locations.
- Using bit manipulation on register X16 helps retrieve the starting address of a page for accurate pointer referencing.
Successful Navigation Through Memory
- After implementing changes, stepping through shows successful navigation from random memory areas back to the Swift handler.
- This method mirrors dynamic linking processes where values are loaded and referenced dynamically at runtime.
Final Considerations in Handler Logic
- The handler compares addresses between trampolines but risks nonsensical comparisons due to unrelated memory locations after remapping.
- Such discrepancies can lead to incorrect index calculations and potential crashes when looking up shared arrays.
Memory Management and Trampolines in Swift
Optimizing Index Calculation for Trampolines
- The discussion begins with the potential to improve index calculations by considering trampoline offsets within their respective pages or storing addresses of used trampolines for lookup.
- Instead of relying on a large shared array, each trampoline can store its own data in adjacent writable memory pages, simplifying data management.
Data Allocation Strategy
- Each trampoline is 8 bytes long, allowing for efficient mapping between executable and data memory. A proposal is made to allocate more data pages than code pages (e.g., 10 data pages for every code page).
- The need to calculate the exact number of bytes required for trampoline data storage is emphasized, suggesting the use of Swift's memory layout functionality to determine this dynamically.
Memory Layout and Pointer Management
- When creating trampoline values, pointers are calculated based on their positions in memory, ensuring that each has a dedicated space for its associated data.
- A computed property can be introduced in the trampoline structure to manage reading/writing operations seamlessly without exposing raw pointers.
Reading and Writing Trampoline Data
- The process involves calculating offsets within memory pages to access specific trampoline data efficiently while maintaining performance.
- By rounding down addresses to page sizes and computing offsets based on handler sizes, an effective method for accessing trampoline-related information is established.
Final Implementation Insights
- The transition from using a shared array to directly accessing allocated data pages marks a significant improvement in efficiency.
- Despite visual similarities in output after changes, the underlying mechanism allows dynamic self-replication of instructions within constraints imposed by the platform.
- The final question raised pertains to what additional functionalities can be integrated into this system as it currently supports swizzling all UI components effectively.
Swizzling Techniques in iOS Development
Introduction to Swizzling
- The discussion begins with the concept of swizzling, aiming to set a world record for the most swizzles. The speaker proposes moving logic down to a new method that can swizzle any image passed to it.
- Emphasizes the need for early swizzling during app launch, noting limitations in Swift and suggesting marking methods as Objective-C for better integration.
Performance Considerations
- Highlights performance issues related to Apple's implementation of method swizzling, specifically mentioning delays caused by acquiring locks across the Objective-C runtime.
- Introduces a private function called
class_replaceMethods_bulk, which allows bulk method swizzling, reducing overhead by minimizing lock acquisition.
Implementation Details
- Describes how to utilize the private function by defining a type alias that matches its signature and collecting necessary data for swizzling.
- Explains updating methods to return values needed for bulk processing while ensuring compatibility with existing implementations.
Testing and Debugging
- After implementing bulk swizzling, tests reveal significant improvements in speed but also lead to crashes due to changes in method definitions affecting low-level code expectations.
- Identifies specific classes causing exceptions and suggests excluding them from swizzling operations.
Final Adjustments and Results
- Discusses further debugging steps after encountering crashes related to class lookups, leading to decisions on what classes should not be swizzled.
- Concludes with successful execution of 800,000 calls post-adjustments, indicating effective implementation of the discussed techniques.
Understanding Method Swizzling in iOS Development
App Launch and Method Invocation
- The app launch process is discussed, revealing that even an empty app invokes a surprisingly high number of methods.
- Rotating an iPhone triggers approximately 100,000 method calls, highlighting the complexity involved in seemingly simple actions.
Dynamic Library Insights
- The speaker utilizes the
ipsswtool to extract dynamic library information from the dyld shared cache file, which contains around 4,000 different libraries/frameworks.
- A new function is created to load all frameworks listed from the cache. This involves logging each framework and attempting to open it.
Swizzling Methods and Performance Metrics
- After executing the loading function for several minutes, the app swizzles over 2.7 million methods, potentially setting a record for method swizzling in an application.
Conclusion and Acknowledgments
- The video concludes with gratitude towards viewers and mentions that the code is available on GitHub for further exploration.
- Special thanks are given to Landon Fuller for his influential blog post that contributed significantly to this project.