-
Notifications
You must be signed in to change notification settings - Fork 123
Refactor ForEachAppDelegateClass for iOS into a new function that swizzles [UIApplication setDelegate:] to obtain App Delegate classes. #1737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This commit refactors the `ForEachAppDelegateClass` function in `app/src/util_ios.mm`. Instead of scanning all runtime classes to find UIApplicationDelegate implementers, it now relies on method swizzling. The `[UIApplication setDelegate:]` method is swizzled at startup. When `setDelegate:` is called, the class of the actual application delegate is captured and stored globally (static to the .mm file). `ForEachAppDelegateClass` now uses this stored class. If it's called before `setDelegate:` has been invoked, the block passed to `ForEachAppDelegateClass` is queued. This queued block is then executed once the delegate is set via the swizzled `setDelegate:` method. This approach is more efficient and directly targets the actual App Delegate class used by the application. Key changes: - Added `Firebase_setDelegate` C function as the swizzled implementation. - Introduced `UIApplication(FirebaseAppDelegateSwizzling)` category with a `+load` method to perform the swizzling. - Uses `method_setImplementation` for swizzling and stores the original IMP. - Global static variables `g_app_delegate_class`, `g_original_setDelegate_imp`, and `g_pending_app_delegate_block` manage the state within `util_ios.mm`. - Modified `ForEachAppDelegateClass` to use the new mechanism and queue blocks if the delegate is not yet known.
✅ Integration test succeeded!Requested by @jonsimantov on commit f9e05f8 |
I replaced some logging calls with NSLog for better stability during early app startup. I also removed some unnecessary comments and unused include statements to keep things clean.
Modified `util_ios.mm` to support queueing multiple blocks (up to 8, defined by `MAX_PENDING_APP_DELEGATE_BLOCKS`) if `ForEachAppDelegateClass` is called before `[UIApplication setDelegate:]` is invoked. Changes include: - Replaced single pending block storage with a C array of block pointers and a counter (`g_pending_app_delegate_blocks` and `g_pending_block_count`). - `ForEachAppDelegateClass` now adds blocks to this array if the app delegate is not yet known. If the array is full, an error is logged and the block is discarded. - `Firebase_setDelegate` (the swizzled method) now iterates through all pending blocks in the array. If a valid delegate is being set, it executes each pending block. If the delegate is being set to nil, it clears all pending blocks. The array count is reset in both cases. - Added `#define MAX_PENDING_APP_DELEGATE_BLOCKS 8` for configurability.
- I removed extraneous developmental comments from app/src/util_ios.mm for better code clarity. - I updated a call site of firebase::util::RunOnAppDelegate (formerly ForEachAppDelegateClass) in messaging/src/ios/messaging.mm to use the new function name.
- I updated the documentation for RunOnAppDelegateClasses (formerly RunOnAppDelegate) in app/src/util_ios.h to accurately reflect its new behavior. - I renamed RunOnAppDelegate to RunOnAppDelegateClasses in all relevant locations (declaration, definition, internal logs, and call sites in invites and messaging modules) for clarity. - I removed the specified extraneous code comments from app/src/util_ios.mm and app/src/invites/ios/invites_ios_startup.mm.
I modified `ClassMethodImplementationCache::ReplaceOrAddMethod` in app/src/util_ios.mm to prevent re-swizzling a method if it's already swizzled with the target implementation. This is done by checking if the current method IMP is identical to the incoming IMP; if so, the function returns early. This resolves a recursive call issue observed when App Delegate hooks were applied multiple times to the same effective class via different GUL-proxied delegate instances. I also included a final cleanup of specified iterative code comments.
Modified `Firebase_setDelegate` in `app/src/util_ios.mm` to prevent redundant processing for delegate classes that are subclasses of already seen delegates. - When `setDelegate:` is called with a `newClass`: - It now first iterates through the superclasses of `newClass`. If any superclass is found in the `g_seen_delegate_classes` list, `newClass` is considered handled, and no further processing (adding to seen list or running pending blocks for it) occurs. - If no superclass is seen, it checks if `newClass` itself is already seen. If so, it's skipped. - If `newClass` is genuinely new (neither itself nor any superclass already seen), it's added to `g_seen_delegate_classes`, and all blocks from `g_pending_app_delegate_blocks` are executed for it. - This addresses potential issues with third-party libraries (like GUL) that might set their own delegate subclasses, ensuring our hooks and blocks run appropriately. - Includes cleanup of minor iterative comments.
This commit addresses several items after an accidental reset: 1. **Restores Source Code Logic:** * Re-implements the correct logic for `RunOnAppDelegateClasses` (formerly ForEachAppDelegateClass) and the swizzled `Firebase_setDelegate` in `app/src/util_ios.mm`. * `Firebase_setDelegate` now correctly tracks multiple unique delegate classes seen, includes a superclass check to prevent redundant processing for subclasses of already-seen delegates, and executes persistent pending blocks for genuinely new delegate classes. * `RunOnAppDelegateClasses` executes blocks for all currently known unique delegates and queues the block for future new delegate classes. * Ensures `ClassMethodImplementationCache` is in its state prior to the reverted idempotency fix attempt. * All associated constants, global variables, function declarations (in `util_ios.h`), and call sites (in `invites` and `messaging` modules) are correctly restored/updated. * Logging uses `NSLog` and iterative comments have been cleaned. 2. **Integrates Learnings into `Jules.md`:** * Reverts the previous commit that added a task-specific learnings section. * Integrates key insights from this refactoring task directly into the most appropriate existing sections of `Jules.md`, covering robust swizzling, callback lifecycle, naming, logging safety, and agent interaction patterns. This commit aims to bring the branch to the desired functional state with updated documentation.
@@ -305,6 +305,7 @@ API documentation. | |||
as mentioned in `CONTRIBUTING.md`. | |||
* **Formatting**: Use `python3 scripts/format_code.py -git_diff -verbose` to | |||
format your code before committing. | |||
* **Naming Precision for Dynamic Systems**: Function names should precisely reflect their behavior, especially in systems with dynamic or asynchronous interactions. For example, a function that processes a list of items should be named differently from one that operates on a single, specific item captured asynchronously. Regularly re-evaluate function names as requirements evolve to maintain clarity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please word wrap this Markdown file to 80 characters, and put a self-referential note about this file that it should always be wrapped at 80 characters.
@@ -397,6 +399,10 @@ API documentation. | |||
integration, it can occasionally be a factor to consider when debugging app | |||
delegate behavior or integrating with other libraries that also perform | |||
swizzling. | |||
When implementing or interacting with swizzling, especially for App Delegate methods like `[UIApplication setDelegate:]`: | |||
* Be highly aware that `setDelegate:` can be called multiple times with different delegate class instances, including proxy classes from other libraries (e.g., GUL - Google Utilities). Swizzling logic must be robust against being invoked multiple times for the same effective method on the same class or on classes in a hierarchy. An idempotency check (i.e., if the method's current IMP is already the target swizzled IMP, do nothing more for that specific swizzle attempt) in any swizzling utility can prevent issues like recursion. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this idempotency check was removed during the iteration process, do you think we should add it back in?
This commit refactors the
ForEachAppDelegateClass
function inapp/src/util_ios.mm
. Instead of scanning all runtime classes to find UIApplicationDelegate implementers, it now relies on method swizzling.The
[UIApplication setDelegate:]
method is swizzled at startup. WhensetDelegate:
is called, the class of the actual application delegate is captured and stored globally (static to the .mm file).ForEachAppDelegateClass
now uses this stored class. If it's called beforesetDelegate:
has been invoked, the block passed toForEachAppDelegateClass
is queued. This queued block is then executed once the delegate is set via the swizzledsetDelegate:
method.This approach is more efficient and directly targets the actual App Delegate class used by the application.
Key changes:
Firebase_setDelegate
C function as the swizzled implementation.UIApplication(FirebaseAppDelegateSwizzling)
category with a+load
method to perform the swizzling.method_setImplementation
for swizzling and stores the original IMP.g_app_delegate_class
,g_original_setDelegate_imp
, andg_pending_app_delegate_block
manage the state withinutil_ios.mm
.ForEachAppDelegateClass
to use the new mechanism and queue blocks if the delegate is not yet known.Description
Testing
Type of Change
Place an
x
the applicable box:Notes
Release Notes
section ofrelease_build_files/readme.md
.