Вызов не предопределенного метода в категории после его переопределения

в 10:41, , рубрики: Программирование

Бывают такие ситуации, при которых лучше написать категорию к классу, чем использовать наследование. Так же иногда существует потребность использовать оригинальный метод класса внутри категории, как если бы мы вызвали super при наследовании. О такой возможности иногда любят спрашивать на собеседованиях и почему-то уверены, что этого нельзя сделать. Google так же умалчивает о такой возможности. Кому интересно — добро пожаловать под кат.

Собственно всё началось с поддержки языка с письмом справа налево старым существующим проектом. Первой жертвой перевода стал класс UITableView. После того, как всё более или менее стало работать с наследованием, было принято решение применить категорию для последующего быстрого добавления поддержки такого рода письма в другие проекты.

Поддержка письма справа налево заключалась переопеределении некоторых методов таблицы. В данной статье рассмотрим метод initWithCoder:.

Для начала напишем категорию для UITableView

@interface UITableView (RTL)
@end


@implementation UITableView (RTL)

- (id)initWithCoder:(NSCoder *)aDecoder{
    //self = какая-то инициализация;
    if (self) {
        //Здесь что-то выполнится
    }
    return self;
}

@end

Нам нужно чем-то проинициализировать таблицу, самый подходящий способ — это вызвать оригинальный метод initWithCoder:. Но как мне ранее говорили, это сделать невозможно. Я решил этому не верить, а посмотреть, что творится в классе, используя функцию Method *class_copyMethodList(Class cls, unsigned int *outCount). Там я увидел, что реализация предопределенного метода встречается 2 раза. Соответственно, если мы получим адреса этих реализаций и проверим, в какой мы находимся, то другая будет нужной нам для вызова. Для поиска нужной нам реализации Был написан следующий метод класса:

- (objc_methodPointer)methodBySelector:(SEL)sel{
    unsigned int mc = 0;
    Class selfClass = object_getClass(self);
    Method *mList = class_copyMethodList(selfClass, &mc);
    Method currentMethod = class_getInstanceMethod(selfClass, sel);
    NSString *oldName = NSStringFromSelector(sel);
    Method oldMethod = NULL;
    for(int i = 0;i < mc; i++){
        NSString *name = [NSString stringWithUTF8String:sel_getName(method_getName(mList[i]))];
        if ([name isEqualToString:oldName] && currentMethod != mList[i]){
            oldMethod = mList[i];
            break;
        }
    }
    return oldMethod;
}

Тут всё просто, в функцию передаётся селектор для поиска. Из него мы получаем имя и текущий метод. Далее мы проходим по списку методов и ищем нужную нам реализацию, проверяя соответствие имени и несоответствие методов.

Теперь у нас есть указатель на нужный нам метод, но нам ещё надо знать его реализацию. К счастью она хранится в полученной нами структуре. Но почему-то в Apple решили не помещать в заготовочные файлы полную структуру Method, хотя на developer.apple.com есть её описание. По этому всё прийдётся описать самим.

struct objc_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};
typedef struct objc_method *objc_methodPointer;

Теперь у нас всё есть для вызова оригинального метода инициализации.

- (id)initWithCoder:(NSCoder *)aDecoder{
    //Получение старого метода при первом вызове
    static objc_methodPointer m = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        m = [self methodBySelector:_cmd];
    });
    //Проверяем нашли ли мы реализацию
    if (m){
        //Сам вызов старого метода
        self = (void *)m->method_imp(self, m->method_name, aDecoder);
        if (self) {
            //Здесь находится дополнительная собственная инициализация
        }
    }
    return self;
}

После сборки и запуска приложения с данным кодом выполнится базовая инициализация таблицы. После этого выполнится наша.

Данный код не претендует на оригинальность и идеальность, это первая версия, которая в ближайшие дни ещё будет доработана. (Даже сейчас вижу, что вызывается копирование списка методов, но не освобождается память).

Спасибо за внимание.

Автор: 5un

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js