问题
To give a bit of context of why I'm asking this: basically I would like to change the location of the myLocationButton of the google map on iOS. So I first fetch the actual button like so:
@implementation GMSMapView (UIChanges)
- (UIButton *)myLocationButton
{
UIButton *myLocationButton;
for (myLocationButton in [settingView subviews])
{
if ([myLocationButton isMemberOfClass:[UIButton class]])
break;
}
return myLocationButton;
}
Then I try to change it's position in the screen using NSLayoutConstraints (directly changing values of the frame property of the button does nothing with google maps SDK 1.8+):
UIButton *myLocationButton = [mapView_ myLocationButton];
[myLocationButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[myLocationButton constraintRightEqualTo:[myLocationButton superview] constant:0];
[myLocationButton constraintTopEqualTo:[myLocationButton superview] constant:50];
where constraintRightEqualTo is defined in a category as:
- (void)constraintRightEqualTo:(UIView *)toView constant:(CGFloat)constant
{
NSLayoutConstraint *cn = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:toView
attribute:NSLayoutAttributeRight
multiplier:1 constant:constant];
[toView addConstraint:cn];
}
so far so good? ok.
Now this works just fine in iOS8.. however when I run this in iOS 7 it crashes with this famous error:
-[TPMURequestStatusNotificationManager makeActionButtonResponsive]:810 - makeActionButtonResponsive 2014-10-08 16:03:20.775 SmartTaxi[13009:60b] * Assertion failure in -[GMSUISettingsView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794 2014-10-08 16:03:20.779 SmartTaxi[13009:60b] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. GMSUISettingsView's implementation of -layoutSubviews needs to call super.'
the problem is that GMSUISettingsView is not calling [super layoutSubviews]..
I've seen this kind of error before.. the thing is that it happens with public classes such as UITableViewCell, as opposed to this private one GMSUISettingsView which is hidden in the guts of the google maps SDK for iOS. Had it been public.. i could have easily swizzled the method layoutsubviews inside of it using an approach similar to this answer. But it's not a public method. How can I in runtime change the definition of it's layoutsubviews to get around this problem? (suggestions to accomplish the same goal using simpler methods are also welcome)
UPDATE
so based on feedback + a bit more research I did the following:
@interface AttackerClass : UIView @end
@implementation AttackerClass
- (void)_autolayout_replacementLayoutSubviews
{
struct objc_super superTarget;
superTarget.receiver = self;
superTarget.super_class = class_getSuperclass(object_getClass(self));
objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
NSLog(@":: calling send super")
// PROBLEM: recursive call.. how do I call the *original*
// GMSUISettingsView implementation of layoutSubivews here?
// replacing this with _autolayout_replacementLayoutSubviews will
// throw an error b/c GMSUISettingsView doesn't have that method defined
objc_msgSend(self, @selector(layoutSubviews));
objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
}
@end
Method attackerMethod = class_getInstanceMethod([AttackerClass class], @selector(_autolayout_replacementLayoutSubviews));
Method victimMethod = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));
method_exchangeImplementations(victimMethod, attackerMethod);
The problem with this approach is that anytime GMSUISettingsView calls layoutSubviews.. it is in effect calling _autolayout_replacementLayoutSubviews.. which then recursively calls GMSUISettingsView layoutsubviews.. so my app goes into an infinite recursive loop. this answer solved that problem by using a category.. but I can't in this case b/c GMSUISettingsView is a private class..
another way of asking the same question: how can i retain a reference to the unaltered version of GMSUISettingsView's layoutSubviews and use it in _autolayout_replacementLayoutSubviews so that I don't fall into this recursive call problem.
ideas?
回答1:
this did it.. i'm not sure if this counts as an actual answer since i just got around the problem by simply calling [self layoutIfNeeded] instead of [self layoutSubviews]
void _autolayout_replacementLayoutSubviews(id self, SEL _cmd)
{
// calling super
struct objc_super superTarget;
superTarget.receiver = self;
superTarget.super_class = class_getSuperclass(object_getClass(self));
objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
// to get around calling layoutSubviews and having
// a recursive call
[self layoutIfNeeded];
objc_msgSendSuper(&superTarget, @selector(layoutSubviews));
}
- (void)replaceGMSUISettingsViewImplementation
{
class_addMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews), (IMP)_autolayout_replacementLayoutSubviews, "v@:");
Method existing = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));
Method new = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews));
method_exchangeImplementations(existing, new);
}
回答2:
UPDATE
If you want to preserve original implementation you can get it and call it inside the blockImp. Then after that call [[aClass superclass] layoutSubviews] or call it with function pointer. Note that all this code need some error check and exception prevents.
//get method encoding
const char * encoding = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews)));
void(^oldImplementation)(id aClass, SEL aSelector) = imp_getBlock(method_getImplementation(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews))));
id blockImp = ^void(id aClass, SEL aSelector)
{
//old imlpementation
oldImplementation(aClass, aSelector);
//calling [super layoutSubviews]
IMP aImp = [[aClass superclass] methodForSelector:@selector(layoutSubviews)];
id (*func)(id, SEL) = (void *)aImp;
func([aClass superclass], @selector(layoutSubviews));
};
IMP newImp = imp_implementationWithBlock(blockImp);
class_replaceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews), newImp, encoding);
ORIGINAL ANSWER
I'm sorry but I didn't fully understand do you want to completely override 'layoutsubviews' or to have original implementation and change just part of it. If it's the first you could use class_replaceMethod instead of swizzling. It will have performance hits though! Something like this should do the trick:
//get method encoding
const char * encoding = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews)));
id blockImp = ^void(id aClass, SEL aSelector)
{
//what you want to happen in layout subviews
};
IMP newImp = imp_implementationWithBlock(blockImp);
class_replaceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews), newImp, encoding);
I've not tested this on device/simulator but something like this should work for you. I assume you know it's not a really bright idea to manipulate private classes ;)
来源:https://stackoverflow.com/questions/26258071/how-to-override-swizzle-a-method-of-a-private-class-in-runtime-objective-c