- PVSM.RU - https://www.pvsm.ru -
Изучая основы разработки под Android мне пришлось познакомится с таким замечательным языком, как Java. Читая очередной раздел гугловского GetStarted я наткнулся на такую конструкцию:
Button.OnClickListener mTakePicSOnClickListener =
new Button.OnClickListener() {
@Override
public void onClick(View v) {
}
};
Объявление представителя OnClickListener и переопределения у него метода onClick (поправьте меня Java программисты). Хм, подумал я, а круто бы эту фичу поиметь в Objective-C, а именно возможность переопределять реализацию метода у объекта(конкретного объекта, а не реализацию метода для всех объектов класса) да еще и через блоки в runtime (!) и забыл о этом всем… пока не оказался в полупустом автобусе в дождливую погоду. Времени было много и я решил поразмыслить над тем, что же тут можно сделать.
Зачем это нужно было? Изначально хотелось уметь делать так:
tableView1.delegate = [[NSObject new] override:@selector(tableView:didDeselectRowAtIndexPath:) imp:^void(NSIndexPath* ip){
NSLog(@"selected row %i", ip.row);
}]
tableView2.delegate = [[NSObject new] override:@selector(tableView:didDeselectRowAtIndexPath:) imp:^void(NSIndexPath* ip){
NSLog(@"selected row %i", ip.row);
}]
Обратите внимание, что предполагается изменение именно делегата и добавление/предопределение методов у него. А tableView остается оригинальным, без каких либо изменений.
Тем самым местом я чувствовал, что это вполне реализуемо благодаря богатому внутреннему миру Objective-C Runtime.
И да, то самое место меня не подвело.
Начнем с примеров. О реализации и подводных камнях я расскажу чуть ниже
UIViewController* vc = [[UIViewController new] overrideMethod:@selector(viewWillAppear:)
blockImp:^id(UIViewController* selfVC, BOOL anim) {
selfVC.view.backgroundColor=[UIColor redColor]; //изменение цвета view
}];
[self presentViewController:vc animated:YES completion:^{ }]; //отобразим vc
//Объявляем ds (объяснения по MMProxy ниже)
MMProxy *ds = [MMProxy proxyWithMMObject];
//массив с данными для отображения
NSArray *arr=@[@"one",@"two",@"three", @"four",@"five"];
//переопределяем метод делегата (возвращающие количество секций, ячеек и сами ячейки)
//ВАЖНО: параметр isRequired указывает является ли этот метод обязательным для протокола (@required)
//или же нет (@option)
//Этот параметр должен быть корректным
[ds addMethod:@selector(numberOfSectionsInTableView:)
fromProtocol:@protocol(UITableViewDataSource)
isRequired:NO
blockImp:^NSUInteger(id object, UITableView* tb) {
return 1;
}];
[ds addMethod:@selector(tableView:numberOfRowsInSection:)
fromProtocol:@protocol(UITableViewDataSource)
isRequired:YES
blockImp:^NSUInteger (id object, UITableView* tb) {
return [arr count];
}];
[ds addMethod:@selector(tableView:cellForRowAtIndexPath:)
fromProtocol:@protocol(UITableViewDataSource)
isRequired:YES
blockImp:^id(id obj, UITableView* tb, NSIndexPath* indexPath) {
static NSString *TableIdentifier = @"SimpleTableItem";
UITableViewCell *cell = [tb dequeueReusableCellWithIdentifier:TableIdentifier];
if (cell == nil)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TableIdentifier];
cell.textLabel.text=arr[indexPath.row];
return cell;
}];
self.tableView.dataSource=(id<UITableViewDataSource>)ds;
[self.tableView reloadData];
//UIOnClickListener уже объявлен. Дефолтная реализация метода onClick: ничего не делать
//-(IBAction)onClick:(id)sender{};
[button1 addTarget:[[UIOnClickListener new] overrideMethod:@selector(onClick:)
blockImp:^void(id obj,id sender){
NSLog(@"bump 1");
}]
action:@selector(onClick:)
forControlEvents:UIControlEventTouchUpInside];
[button2 addTarget:[[UIOnClickListener new] overrideMethod:@selector(onClick:)
blockImp:^void(id obj,id sender){
NSLog(@"bump 2");
}]
action:@selector(onClick:)
forControlEvents:UIControlEventTouchUpInside];
Итак, у меня была идея и план ее воплощения:
Глупец, как же я был наивен.
Тут наверное стоит сделать небольшое отступление и рассказать немного о методах. Давайте разберемся, что из себя представляет метод в Objective-C:
Так как планировалось изменение реализации метода, то в первую очередь нужно было разобраться с IMP. Но к моему сожалению документация от Apple была ну уж слишком поверхностна. Все что удалось найти по IMP на этой странице [1]:
An Objective-C method is simply a C function that take at least two arguments—self and _cmd. You can add a function to a class as a method using the function class_addMethod. Therefore, given the following function:
и небольшой пример IMP для метода вида -(void) method; Но что делать с методами у который есть параметры (или возвращающие что-то) было не ясно. И самое главное! С-функция это хорошо, но хотелось бы использовать блоки, как с ними быть? Эти два вопроса меня привели к этой замечательной статье [2] и я получил ответы на все свои вопросы. Начиная с ios 4 Runtime Api позволяет получить IMP прямо из блока. Кстати, документации по Runtime API от Apple о этом ни слова. Приведу описание по IMP и примеры из этой же статьи:
-(void)doSomething:
void(*doSomethingIMP)(id s, SEL _c);
void(^doSomethingBLOCK)(id s);
-(void)doSomethingWith:(int)x;
void(*doSomethingWithIMP)(id s, SEL _c, int x);
void(^doSomethingWithBLOCK)(id s, int x);
-(int)doToThis:(NSString*)n withThat:(double)d;
int(*doToThis_withThatIMP)(id s, SEL _c, NSString *n, double d);
int(^doToThis_withThatBLOCK)(id s, NSString *n, double d);
На тот момент я мог получить информацию о методе, создать IMP из блока и заменить старую IMP на новую из блока и все это благодаря runtime. Вот оно, думал я, как все просто и почему никто этого еще не сделал? И только тут я осознал свою ошибку… Покажу на примере:
NSString *str1= @"";
[str1 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}];
NSString *str2= @"";
[str2 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 2;}];
[str1 intValue]; //возвращало бы 2, а не 1
[str2 intValue]; //возвращало бы 2
Не то что бы это и плохо… переопределение реализации метода для всех экземпляров класса с помощью блока это круто, но не то что мне нужно было изначально.
И я начал все сначала, сделав «ход конем». Решено было вместо переопределения реализации старого метода генерировать новый, но с уникальным именем. И, перехватив отсылку сообщения объекту, заменять команду/селектор метода. В роли уникального имени селектора была выбрана пара <адрес>_<старый селектор>. Т.е. предыдущий код должен был преобразоваться в это:
//переменная 0x0000042
NSString *str1= @"";
//генерируем метод вида 0x0000042_intValue
[str1 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}];
//переменная 0x0000043
NSString *str2= @"";
//генерируем метод вида 0x0000043_intValue
[str2 overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 2;}];
[str1 0x0000042_intValue]; //возвращает 1
[str2 0x0000043_intValue]; //возвращает 2
Конечно же без заморочек для пользователя. Планировалось все это сделать через механизм перехватики сообщений. Скорей всего это моя ошибка, но я не знал и сейчас не знаю как можно это реализовать по другому, может у вас есть какие-то идеи?
В Objective-С возможно перехватить посылку сообщения объекту в следующих случаях:
Как вы видите перехватить любое сообщение, отсылаемого объекту NSObject невозможно (или я просто не нашел?).
Использование обертки из NSProxy могло помочь, но оно:
1) не так элегантно
NSString *str1= @"";
NSProxy *proxy=[NSProxy proxyWithObject:str1];
[proxy overrideMethod:@selector(intValue) imp:^int(NSString* selfStr){ return 1;}];
[proxy intValue]; //возвращает 1
2) Иногда не работает. Вот так не получится сделать, так как applicationDidBecomeActive будет отослан делегату, а не обертке из прокси. Зачем так делать? это совсем другой вопрос…
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSProxy *p=[NSProxy proxyWithObject:self];
// Override point for customization after application launch.
[p overrideMethod:@selector(applicationDidBecomeActive:)
blockImp:^void(id obj,UIApplication* app)
{
UIAlertView *alv=[[UIAlertView alloc] initWithTitle:@"ss" message:nil delegate:nil cancelButtonTitle:@"fuuu" otherButtonTitles:nil];
[alv show];
[alv release];
}];
return YES;
}
И я решил реализовать переопределение реализации метода через оба механизма:
1) через NSProxy. Как более правильное, но не являющееся универсальным решением
2) через костыль, но универсальный, для любого представителя NSObject.
А так как NSObject и NSProxy это root классы, то теоретически я реализовал предопределение реализации метода для любого objective-c класса в runtime. На практике это оказалось не совсем так.
Итак, кратко попробую описать как все работает при использовании NSProxy:
Как работает все при использовании костыля для NSObject
Использование подобного костыля черевато проблемами. Так, к примеру, если пользовательский класс переопределит метод forwardInvocation то для него override уже не будет работать корректно. Ну и самое главное, уничтожение старого метода и работа с forwardInvocation это ну очень серьезный удар по производительности. Причем для всех экземпляров класса. Попробую объяснить: если мы у класса NSString переопределим таким образом intValue для одного объекта, то этот класс уже никогда не будет таким как прежде. Теперь при посылке сообщения intValue всем представителям NSString будет вызываться уже mm_old_intValue, причем через механизм forwardInvocation.
Кстати, насчет удаления. К сожалению в Objective-C 2.0 убрали возможность удалять методы. По этому пришлось сделать еще один костыль. Под удалением я подразумеваю замену IMP у удаляемого метода на IMP не существующего метода. Что-то такое
IMP forw=class_getMethodImplementation(clas, @selector(methodThatDoesNotExist:iHope:::::));
IMP oldImpl= method_setImplementation(method,forw);
Или, что эквивалентно, использовать _objc_msgForward.
IMP oldImpl= method_setImplementation(method,(IMP)_objc_msgForward);
Да, теперь все выглядит не так радужно, как было в примерах. Но зато каким весельем можно заниматься, переопределяя методы у viewController'ов, AppDelegatov, просто делегатов и прочих объектов. И все это в две строчки кода. А методы то бывают разные: и черные, и белые, и публичные, и прива. Ой, что-то меня понесло.
Хотя есть еще несколько ограничений, о которых я не упомянул раньше:
Возможно еще что-то, о чем я не знаю. Подобные манипуляции на грани фола, они как Восток — дело тонкое.
Есть два возможных сценария изменения реализации:
Используя обертку MMProxy. Для этого нужно инициализировать MMProxy с интересующим нас объектом и вызвать метод для переопределения. Посылать сообщения следует прокси, а не объекту.
Пример:
id object;
MMProxy *p= [MMProxy proxyWithObject:object];
[object overrideMethod:@selector(onClick:) blockImp:^void(id obj,id sender){ }] ;
[p onClick:nil]
Для делегатов я рекомендую использовать прокси, созданный методом proxyWithMMObject
MMProxy *ds = [MMProxy proxyWithMMObject];
[ds addMethod:@selector(numberOfSectionsInTableView:)
fromProtocol:@protocol(UITableViewDataSource)
isRequired:NO
blockImp:^NSUInteger(id object, UITableView* tb) {
return 1;
}];
Второй способ реализован через категорию, а по этому нужные вам методы можно вызвать у любого объекта, унаследованного от NSObject. О ограничениях я уже говорил.
Оба варианта реализуют протокол со следующими методами:
-(id) overrideMethod: (SEL)sel blockImp:(id)block;
позволяет изменить реализацию метода. Если метод по заданному селектору у класса не обнаружется (или возникнут какие-либо еще проблемы), то будет сгенерировано исключение
-(id) addMethod: (SEL)sel fromProtocol:(Protocol*)p isRequired:(BOOL)isReq blockImp:(id)block;
позволяет добавить метод к объекту в случае если у класса нет такого метода. Аналогично, если возникнут какие-либо проблемы — будет сгенерировано исключение.
Отдельно стоит обсудить область применения и вообще нужность такого подхода. Как по мне так да, это нужно. Это еще один подход, позволяющий вам не просто выстрелить себе в ногу, но и снести нафиг пол башки.
Ну и среди прочего
Можно ли «это» использовать в промышленной разработке. Однозначно нет.
Как минимум пока. Но поиграться можно уже сейчас.
Ну и конечно же главная цель статьи — обсудить.
Скачать тестовый проект и ознакомится с реализацией можно тут: github.com/Flanker4/MMMutableMethods/ [3]
Автор: Flanker_4
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/objective-c/27171
Ссылки в тексте:
[1] странице: https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html#//apple_ref/doc/uid/TP40008048-CH102-SW1
[2] статье: http://www.friday.com/bbum/2011/03/17/ios-4-3-imp_implementationwithblock/
[3] github.com/Flanker4/MMMutableMethods/: https://github.com/Flanker4/MMMutableMethods/
[4] Источник: http://habrahabr.ru/post/168105/
Нажмите здесь для печати.