【问题标题】:how to override/swizzle a method of a private class in runtime objective-c?如何在运行时objective-c中覆盖/调整私有类的方法?
【发布时间】:2014-10-08 13:26:00
【问题描述】:

为了说明我问这个问题的原因:基本上我想更改 iOS 上谷歌地图的myLocationButton 的位置。所以我首先像这样获取实际的按钮:

@implementation GMSMapView (UIChanges)

- (UIButton *)myLocationButton
{
    UIButton *myLocationButton;
    for (myLocationButton in [settingView subviews])
    {
        if ([myLocationButton isMemberOfClass:[UIButton class]])
            break;
    }
    return myLocationButton;
}

然后我尝试使用 NSLayoutConstraints 更改它在屏幕中的位置(直接更改按钮的 frame 属性的值对 google maps SDK 1.8+ 没有任何作用):

UIButton *myLocationButton = [mapView_ myLocationButton];
[myLocationButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[myLocationButton constraintRightEqualTo:[myLocationButton superview] constant:0];
[myLocationButton constraintTopEqualTo:[myLocationButton superview] constant:50];

其中constraintRightEqualTo 在一个类别中定义为:

- (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];
}

到目前为止这么好?好的。

现在这在 iOS8 中工作得很好。但是当我在 iOS 7 中运行它时,它会因这个著名的错误而崩溃:

-[TPMURequestStatusNotificationManager makeActionButtonResponsive]:810 - makeActionButtonResponsive 2014-10-08 16:03:20.775 SmartTaxi[13009:60b] * 中的断言失败 -[GMSUISettingsView layoutSublayersOfLayer:],/SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794 2014-10-08 16:03:20.779 SmartTaxi[13009:60b] * 由于未捕获而终止应用程序 异常'NSInternalInconsistencyException',原因:'自动布局 执行 -layoutSubviews 后仍然需要。 GMUISettingsView 的 -layoutSubviews 的实现需要调用 super.'

问题是GMSUISettingsView 没有调用[super layoutSubviews]..

我以前见过这种错误。问题是它发生在诸如UITableViewCell 之类的公共类上,而不是隐藏在谷歌地图SDK 中的私有类GMSUISettingsView IOS。如果它是公开的.. 我可以很容易地在其中使用swizzled 方法layoutsubviews 使用类似于this 答案的方法。但这不是公共方法。如何在运行时更改它的定义 layoutsubviews 来解决这个问题? (也欢迎使用更简单的方法实现相同目标的建议)


更新

因此,根据反馈 + 更多研究,我做了以下工作:

@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);

这种方法的问题是任何时候GMSUISettingsView 调用layoutSubviews.. 它实际上调用_autolayout_replacementLayoutSubviews.. 然后递归调用GMSUISettingsView layoutsubviews.. 所以我的应用程序进入一个无限递归循环。 this answer 通过使用类别解决了这个问题.. 但在这种情况下我不能 b/c GMSUISettingsView 是一个私有类..

问同样问题的另一种方式:我怎样才能保留对GMSUISettingsView's layoutSubviews 未更改版本的引用并在_autolayout_replacementLayoutSubviews 中使用它,这样我就不会陷入这个递归调用问题。

想法?

【问题讨论】:

  • 第一段代码有问题。它总是会返回 something 并且 something 并不总是 UIButtonnil,这是我想象的预期语义。更不用说未初始化的变量和使用isMemberOfClass而不是isKindOfClass
  • @Droppy 为了论证..假设它没有错误并且它可靠地返回了我所期望的
  • 如果您只需将layoutSubviews 发送到对象super 类,您就不能使用objc_msgSendSuper 来代替swizzling 吗?
  • @BradAllred 告诉我更多?你能给我看一个使用objc_msgSendSuper的代码示例吗?
  • 该方法似乎在 iOS 运行时库中不可用.. 仅在 osx 运行时

标签: ios objective-c iphone google-maps-sdk-ios objective-c-runtime


【解决方案1】:

这样做了.. 我不确定这是否算作实际答案,因为我只是通过简单地调用 [self layoutIfNeeded] 而不是 [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);
}

【讨论】:

  • 使用objc_msgSend@selector(_autolayout_replacementLayoutSubviews) 发送到self。 (而不是调用[self layoutIfNeeded]。)这具有[self _autolayout_replacementLayoutSubviews] 的效果,您已经将其确定为所需的行为。
【解决方案2】:

更新

如果您想保留原始实现,您可以获取它并在 blockImp 中调用它。然后在调用 [[aClass superclass] layoutSubviews] 或使用函数指针调用它之后。请注意,所有这些代码都需要一些错误检查和异常预防。

//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);

原始答案

很抱歉,我不完全明白您是想完全覆盖“layoutsubviews”还是拥有原始实现并仅更改其中的一部分。如果是第一个,您可以使用 class_replaceMethod 而不是 swizzling。不过,它会有性能上的打击!像这样的东西应该可以解决问题:

//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);

我没有在设备/模拟器上测试过这个,但是这样的东西应该适合你。我假设您知道操纵私有课程并不是一个非常好的主意;)

【讨论】:

  • 如果操纵私人课程不是一个好主意,那么请提出一种更好的方法来解决我在上面遇到的问题而不操纵私人课程
  • 你如何确保blockImp 仍然像原来的layoutSubviews 那样做......而且还添加了[super layoutsubviews]?
  • aSelector 上面返回这个aSelector SEL "(\xae\xff\xbf/\xa1\xfa\x03 /4\x13\xe4\xfa\xa9\x01(" 0xbfffae08.. 所以调用oldImplementation(aClass, aSelector); 崩溃了..
  • 嘿,看看我更新的问题.. 现在我陷入了递归调用
猜你喜欢
  • 1970-01-01
  • 2015-08-03
  • 2012-01-29
  • 1970-01-01
  • 2023-04-09
  • 2018-07-08
  • 2011-07-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多