Как переопределить /swizzle метод частного класса во время выполнения objective-c?
Чтобы дать немного контекста, почему я спрашиваю об этом: в основном я хотел бы изменить расположение myLocationButton
карты google на iOS. Поэтому я сначала выбираю фактическую кнопку так:
@implementation GMSMapView (UIChanges)
- (UIButton *)myLocationButton
{
UIButton *myLocationButton;
for (myLocationButton in [settingView subviews])
{
if ([myLocationButton isMemberOfClass:[UIButton class]])
break;
}
return myLocationButton;
}
Затем я пытаюсь изменить его положение на экране с помощью NSLayoutConstraints (прямое изменение значений свойства frame
кнопки ничего не делает с картами Google 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. GMSUISettingsView-х реализация -layoutSubviews требует вызова super. '
проблема заключается в том, что GMSUISettingsView
не вызывает [super layoutSubviews]
..
Я видел такую ошибку раньше... Дело в том, что это происходит с общедоступными классами, такими как UITableViewCell
, в отличие от этого частного GMSUISettingsView
, который скрыт в китах SDK Google Maps для IOS. Если бы это было публично.. я мог бы легко swizzled метод layoutsubviews
внутри него, используя подход, похожий на этот ответ. Но это не публичный метод. Как я могу во время выполнения изменить его определение layoutsubviews
, чтобы обойти эту проблему? (предложения по достижению одной и той же цели с использованием более простых методов также приветствуются)
UPDATE
поэтому на основе обратной связи + немного больше исследований я сделал следующее:
@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.. поэтому мое приложение переходит в бесконечный рекурсивный цикл. этот ответ решил эту проблему с помощью категории.. но я не могу в этом случае b/c GMSUISettingsView
является частным классом..
другой способ задать тот же вопрос: как я могу сохранить ссылку на неизмененную версию GMSUISettingsView layoutSubviews
и использовать ее в _autolayout_replacementLayoutSubviews
, чтобы я не попадал в эту проблему с рекурсивным вызовом.
идеи?
Ответы
Ответ 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, "[email protected]:");
Method existing = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(layoutSubviews));
Method new = class_getInstanceMethod(NSClassFromString(@"GMSUISettingsView"), @selector(_autolayout_replacementLayoutSubviews));
method_exchangeImplementations(existing, new);
}
Ответ 2
ОБНОВЛЕНИЕ
Если вы хотите сохранить оригинальную реализацию, вы можете получить ее и вызвать ее внутри blockImp. Затем после этого вызовите [[aClass суперкласс] 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);
Я не тестировал это на устройстве/симуляторе, но что-то вроде этого должно сработать для вас. Я полагаю, вы знаете, что это не очень яркая идея манипулировать частными классами;)