Переопределение реализации метода. Вдохновленный Java’ой

в 3:08, , рубрики: fun, imp, method, objective-c, override, runtime, разработка под iOS, метки: , , , , ,

Вступление

Изучая основы разработки под 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.
И да, то самое место меня не подвело.

Примеры

Начнем с примеров. О реализации и подводных камнях я расскажу чуть ниже

1) Переопределение метода viewWillApear для вновь созданного UIViewContoller'a

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 

2) Реализация протокола UITableViewDataSource

    //Объявляем 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];

3) Like Java.

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

Реализация

Итак, у меня была идея и план ее воплощения:

  • получить нужный метод по селектору (Method)
  • переопределить у него реализацию (IMP). Конечно же через блоки. Ибо стильно и модно сейчас
  • ...
  • PROFIT

Глупец, как же я был наивен.

Попытка #1

Тут наверное стоит сделать небольшое отступление и рассказать немного о методах. Давайте разберемся, что из себя представляет метод в Objective-C:

  • SEL — селектор, имя метода
  • IMP — реализация метода. Обычная си-функция
  • method_types — строка, описывающая типы аргументов метода, а так же тип значения, которое возвращает метод.

Так как планировалось изменение реализации метода, то в первую очередь нужно было разобраться с IMP. Но к моему сожалению документация от Apple была ну уж слишком поверхностна. Все что удалось найти по IMP на этой странице:

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; Но что делать с методами у который есть параметры (или возвращающие что-то) было не ясно. И самое главное! С-функция это хорошо, но хотелось бы использовать блоки, как с ними быть? Эти два вопроса меня привели к этой замечательной статье и я получил ответы на все свои вопросы. Начиная с ios 4 Runtime Api позволяет получить IMP прямо из блока. Кстати, документации по Runtime API от Apple о этом ни слова. Приведу описание по IMP и примеры из этой же статьи:

  • IMP в виде C функции имеет как минимум два аргумента: s-объект, метод которого был вызван (наверное правильней сказать объект, которому послано сообщение) и _с — селектор метода. Все остальное — аргументы метода.
  • 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

Не то что бы это и плохо… переопределение реализации метода для всех экземпляров класса с помощью блока это круто, но не то что мне нужно было изначально.

Попытка #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-С возможно перехватить посылку сообщения объекту в следующих случаях:

  1. при использовании обертки NSProxy
  2. в случае получения объектом сообщения, которое он не может обработать (unrecognized selector)

Как вы видите перехватить любое сообщение, отсылаемого объекту 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

  • реализована категория для NSObject
  • при переопределении реализации метода генерируется новый метод вида <адрес объекта>_<селектор метода>.
  • так же генерируется новый метод вида <mm_old>_<селектор метода>, который содержит IMP(реализацию) оригинального метода
  • оригинальный метод «удаляется»
  • при посылке сообщения объекту по старому селектору срабатывает механизм forwardInvocation, так как старый метод «удален»
  • в forwardInvocation проверяется, может ли класс обработать селектор вида <адрес объекта>_<селектор метода>. Если да, то вызывается он.
  • в forwardInvocation проверяется, может ли класс обработать селектор <mm_old>_<селектор метода>. Если да, то вызывается он.
  • иначе мы вызываем [self doesNotRecognizeSelector:anInvocation.selector]; — стандартная реализация.

Использование подобного костыля черевато проблемами. Так, к примеру, если пользовательский класс переопределит метод 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, просто делегатов и прочих объектов. И все это в две строчки кода. А методы то бывают разные: и черные, и белые, и публичные, и прива. Ой, что-то меня понесло.

Ограничения

Хотя есть еще несколько ограничений, о которых я не упомянул раньше:

  • нельзя переопределять методы из протокола NSObject(как для NSProxy, так и для категории NSObject), а так же методы класса NSInvokation(для категории NSObject)
  • для вызова метода super прийдется обратиться к runtime api
  • для доступа к приватным свойствам объекта прийдется обратиться к runtime api

Возможно еще что-то, о чем я не знаю. Подобные манипуляции на грани фола, они как Восток — дело тонкое.

Как пользоваться

Есть два возможных сценария изменения реализации:

Используя обертку 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;
позволяет добавить метод к объекту в случае если у класса нет такого метода. Аналогично, если возникнут какие-либо проблемы — будет сгенерировано исключение.

Область применения

Отдельно стоит обсудить область применения и вообще нужность такого подхода. Как по мне так да, это нужно. Это еще один подход, позволяющий вам не просто выстрелить себе в ногу, но и снести нафиг пол башки.
Ну и среди прочего

  • fun
  • добавление поддержки блоков туда, где изначально все базировалось на делегатах. Причем это универсальное решение, при использовании NSProxy. Оно подойдет и для UIAlertViewDelegate, и для UIActionSheet, и для tableView, без необходимости модификации этих классов (через категорий или наследование)
  • отладка
  • тесты, это конечно Вам не OCMock, но все же.

Можно ли «это» использовать в промышленной разработке. Однозначно нет.
Как минимум пока. Но поиграться можно уже сейчас.
Ну и конечно же главная цель статьи — обсудить.

Скачать тестовый проект и ознакомится с реализацией можно тут: github.com/Flanker4/MMMutableMethods/

P.S.

«Чукча не писатель, чукча читатель.» Возможно статья написана слишком непонятно или содержит ошибки. Извиняюсь, пишите ЛС, поправим.

Автор: Flanker_4

Источник

Поделиться

* - обязательные к заполнению поля