Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 2.07.2014 | нет комментариев

Исследуя основы разработки под 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). Безусловно жqvmk!br/>
    Как вы видите перехватить всякое сообщение, отсылаемого объекту 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;
             }];
    

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

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

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB