Бывают такие ситуации, при которых лучше написать категорию к классу, чем использовать наследование. Так же иногда существует потребность использовать оригинальный метод класса внутри категории, как если бы мы вызвали 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