Skip to content

Update the GULObjectSwizzler to handle NSProxy objects #2053

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

Merged
merged 11 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions GoogleUtilities/Example/Tests/Swizzler/GULObjectSwizzlerTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,28 @@
// limitations under the License.

#import <XCTest/XCTest.h>
#import <objc/runtime.h>

#import <GoogleUtilities/GULObjectSwizzler.h>
#import <GoogleUtilities/GULSwizzledObject.h>
#import <GoogleUtilities/GULProxy.h>

@interface GULObjectSwizzlerTest : XCTestCase

@end

@implementation GULObjectSwizzlerTest

/** Used as a donor method to add a method that doesn't exist on the superclass. */
- (NSString *)donorDescription {
return @"SwizzledDonorDescription";
}

/** Used as a donor method to add a method that exists on superclass. */
- (NSString *)description {
return @"SwizzledDescription";
}

/** Exists just as a donor method. */
- (void)donorMethod {
}
Expand Down Expand Up @@ -235,4 +247,48 @@ - (void)testSetGetAssociatedObjectWithoutProperAssociation {
XCTAssertEqualObjects(returnedObject, associatedObject);
}

- (void)testSwizzleProxiedObject {
NSObject *object = [[NSObject alloc] init];
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];
GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];

XCTAssertNoThrow([swizzler swizzle]);

XCTAssertNotEqual(object_getClass(proxyObject), [GULProxy class]);
XCTAssertTrue([object_getClass(proxyObject) isSubclassOfClass:[GULProxy class]]);

XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_objectSwizzler)]);
XCTAssertNoThrow([proxyObject performSelector:@selector(gul_objectSwizzler)]);

XCTAssertTrue([proxyObject respondsToSelector:@selector(gul_class)]);
XCTAssertNoThrow([proxyObject performSelector:@selector(gul_class)]);
}

- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenOverridingMethod {
NSObject *object = [[NSObject alloc] init];
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];

GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
[swizzler copySelector:@selector(description)
fromClass:[GULObjectSwizzlerTest class]
isClassSelector:NO];
[swizzler swizzle];

XCTAssertEqual([proxyObject performSelector:@selector(description)], @"SwizzledDescription");
}

- (void)testSwizzleProxiedObjectInvokesInjectedMethodWhenAddingMethod {
NSObject *object = [[NSObject alloc] init];
GULProxy *proxyObject = [GULProxy proxyWithDelegate:object];

GULObjectSwizzler *swizzler = [[GULObjectSwizzler alloc] initWithObject:proxyObject];
[swizzler copySelector:@selector(donorDescription)
fromClass:[GULObjectSwizzlerTest class]
isClassSelector:NO];
[swizzler swizzle];

XCTAssertEqual([proxyObject performSelector:@selector(donorDescription)],
@"SwizzledDonorDescription");
}

@end
8 changes: 6 additions & 2 deletions GoogleUtilities/ISASwizzler/GULObjectSwizzler.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ - (instancetype)initWithObject:(id)object {
__strong id swizzledObject = object;
if (swizzledObject) {
_swizzledObject = swizzledObject;
_originalClass = [swizzledObject class];
_originalClass = object_getClass(object);
NSString *newClassName = [NSString
stringWithFormat:@"fir_%p_%@", swizzledObject, NSStringFromClass(_originalClass)];
_generatedClass = objc_allocateClassPair(_originalClass, newClassName.UTF8String, 0);
Expand Down Expand Up @@ -134,7 +134,7 @@ - (void)swizzle {

[GULSwizzledObject copyDonorSelectorsUsingObjectSwizzler:self];

NSAssert(_originalClass == [_swizzledObject class],
NSAssert(_originalClass == object_getClass(swizzledObject),
@"The original class is not the reported class now.");
NSAssert(class_getInstanceSize(_originalClass) == class_getInstanceSize(_generatedClass),
@"The instance size of the generated class must be equal to the original class.");
Expand All @@ -154,4 +154,8 @@ - (void)dealloc {
objc_disposeClassPair(_generatedClass);
}

- (BOOL)isSwizzlingProxyObject {
return [_swizzledObject isProxy];
}

@end
19 changes: 19 additions & 0 deletions GoogleUtilities/ISASwizzler/GULSwizzledObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#import <objc/runtime.h>

#import "Private/GULSwizzledObject.h"
#import "Private/GULObjectSwizzler.h"

Expand All @@ -26,6 +28,17 @@ @implementation GULSwizzledObject
+ (void)copyDonorSelectorsUsingObjectSwizzler:(GULObjectSwizzler *)objectSwizzler {
[objectSwizzler copySelector:@selector(gul_objectSwizzler) fromClass:self isClassSelector:NO];
[objectSwizzler copySelector:@selector(gul_class) fromClass:self isClassSelector:NO];

// This is needed because NSProxy objects ususally override
// -[NSObjectProtocol respondsToSelector:] and ask this question to the underlying object.
// Since we don't swizzle the underlying object but swizzle the proxy, when someone calls
// -[NSObjectProtocol respondsToSelector:] on the proxy, the answer ends up being NO even if we
// added new methods to the subclass through ISA Swizzling. To solve that, we override
// -[NSObjectProtocol respondsToSelector:] in such a way that takes into account the fact that
// we've added new methods.
if ([objectSwizzler isSwizzlingProxyObject]) {
[objectSwizzler copySelector:@selector(respondsToSelector:) fromClass:self isClassSelector:NO];
}
}

- (instancetype)init {
Expand All @@ -43,4 +56,10 @@ - (Class)gul_class {
return [[self gul_objectSwizzler] generatedClass];
}

// Only added to a class when we detect it is a proxy.
- (BOOL)respondsToSelector:(SEL)aSelector {
Class selfClass = object_getClass(self);
return [selfClass instancesRespondToSelector:aSelector] || [super respondsToSelector:aSelector];
}

@end
3 changes: 3 additions & 0 deletions GoogleUtilities/ISASwizzler/Private/GULObjectSwizzler.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ typedef OBJC_ENUM(uintptr_t, GUL_ASSOCIATION){
* the class pair. */
- (void)swizzle;

/** @return The value of -[objectBeingSwizzled isProxy] */
- (BOOL)isSwizzlingProxyObject;

@end

NS_ASSUME_NONNULL_END
24 changes: 24 additions & 0 deletions GoogleUtilities/SwizzlerTestHelpers/GULProxy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>

/** An example NSProxy that could be used to wrap an object that we have to ISA Swizzle. */
@interface GULProxy : NSProxy

+ (instancetype)proxyWithDelegate:(id)delegate;

@end
94 changes: 94 additions & 0 deletions GoogleUtilities/SwizzlerTestHelpers/GULProxy.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "GULProxy.h"

@interface GULProxy ()

@property(nonatomic, strong) id delegateObject;

@end

@implementation GULProxy

- (instancetype)initWithDelegate:(id)delegate {
_delegateObject = delegate;
return self;
}

+ (instancetype)proxyWithDelegate:(id)delegate {
return [[GULProxy alloc] initWithDelegate:delegate];
}

- (id)forwardingTargetForSelector:(SEL)selector {
return _delegateObject;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
if (_delegateObject != nil) {
[invocation setTarget:_delegateObject];
[invocation invoke];
}
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [_delegateObject instanceMethodSignatureForSelector:selector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
return [_delegateObject respondsToSelector:aSelector];
}

- (BOOL)isEqual:(id)object {
return [_delegateObject isEqual:object];
}

- (NSUInteger)hash {
return [_delegateObject hash];
}

- (Class)superclass {
return [_delegateObject superclass];
}

- (Class)class {
return [_delegateObject class];
}

- (BOOL)isKindOfClass:(Class)aClass {
return [_delegateObject isKindOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass {
return [_delegateObject isMemberOfClass:aClass];
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_delegateObject conformsToProtocol:aProtocol];
}

- (BOOL)isProxy {
return YES;
}

- (NSString *)description {
return [_delegateObject description];
}
- (NSString *)debugDescription {
return [_delegateObject debugDescription];
}

@end