Pull to refresh

Knork: простейшая альтернатива ButterKnife в 160 строк кода

Reading time5 min
Views11K
Хабрапривет!

Ниже речь пойдет о view injection, костылестроении, аннотациях, рефлексии, о жалкой попытке превзойти Джейка Уортона и о том, что свой велосипед ближе к телу.

Что же такое view injection? Это способ избежать вот такого рутинного кода:

Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
  public void onClick(View v) {
    // ...
  }
});


Если использовать view injection с помощью, скажем, ButterKnife, написанного Джейком Уортоном (Jake Wharton), то код становится прозрачнее:

@InjectView(R.id.button) Button mButton;

@OnClick(R.id.button)
public void onButtonClick() {
  // ...
}


Но при ближайшем рассмотрении оказывается, что и ButterKnife не идеален.

Во-первых, он генерирует вспомогательные классы на этапе компиляции, и многие IDE и билд-системы иногда сходят с ума (компилируют классы не в том порядке). Хотя конечно по замыслу это позволяет черной магии не ухудшать производительность кода.

Во-вторых, он не совсем правильно отменяет view injection — вьюхи он обнуляет, а вот назначенные им коллбэки — нет. При неосторожном использовании это может привести к утечкам памяти и другим ошибкам (например, если в адаптере делать повторные инжекты).

В-третьих, очень непросто (если вообще возможно) добавить свой собственный биндинг, скажем, для привязки метода к View.OnKeyListener.

И, наконец, очень уж нетривиально устроено подключение его к старой Ant-based билд-системе. А ведь многие проекты до сих пор еще не перешли на Gradle.

Поэтому я подумал — а не сделать ли свой собственный ButterKnife со всеми вытекающими? Так вот и получилась незамысловатая библиотечка Knork (тоже столовый прибор, knife + fork). Из ключевых особенностей библиотеки — простота и малый размер.

Упрощение 1. Динамическая обработка аннотаций в рантайме


«Но это же ужасно!» — скажете вы, и будете совершенно правы. Это действительно медленно, но в конце статьи я приведу небольшой бенчмарк, и не все так плохо как кажется в плане скорости. Зато этот маленький ужас избавит нас от кодогенерации, от ошибок билд процесса и т.д. А еще позволит расширять библиотеку по своим нуждам.

Упрощение 2. Всего две аннотации


Мы ограничимся всего двумя аннотациями, которые легко запомнить:

Id — аннотация перед полем класса, нужна для инжекта виджетов.
On — аннотация перед методом, нужна для инжекта различных Listener-ов.

Но как нам передать в @On() идентификатор виджета, да еще и действие, на которое нужно привязать аннотируемый метод? Мы же знаем, что у аннотации может быть только один безымянный value, а для большего числа параметров нужно будет давать имена, т.е.:

@On(R.id.button)
// Однако:
@On(value=R.id.button, action=CLICK)


На помощь приходят старые навыки embedded-разработки и непроходящая любовь к уродливым нетривиальным решениям. Нам известно, что ID может быть целым числом в диапазоне 0x7f000000..0xffffffff. А в аннотациях можно использовать 64-битный long. Это дает нам свободные старшие 32 бита для личных нужд. Там и будем хранить номер события с которым нужно связать метод. Например:

@Id(R.id.button) mButton;

// Арифметическое сложение
@On(CLICK + R.id.button)
public void onButtonClick(Button b) {
  // ...
}

// Побитовое сложение тоже сойдет
@On(LONGCLICK | R.id.button)
public boolean onButtonLongClick(Button b) {
  // ...
}


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

Упрощение 3. Гибкие классы-инжекторы


Получается что наш основной класс Knork, занимающийся инжектом, будет пробегаться по объекту, искать аннотации и для каждой аннотации On будет находить соответствующий инжектор и делегировать ему управление. Значит разработчик сможет добавлять и свои собственные инжекторы в прямо в процессе работы программы. Инжекторы будут отвечать за привязку метода к виджету, а также за удаление созданных listener-ов.Никаких утечек.

Общая картина


Весь код оказался в рамках одного класса Knork, так что для подключения нужно будет всего лишь написать:

import static trikita.knork.Knork.*;


Это идеологически не совсем правильно, но поскольку наш класс будет всего на полторы сотни строк — я надеюсь вы простите такой подход.

Итак, в классе Knork будет примерно следующее:

class Knork {

  // Инжект вьюх в определенный объект
  public static void inject(Object obj, View v) { ... }

  // Отмена инжекта
  public static void reset(Object obj) { ... }

  // Регистрация кастомного инжектора
  public static void registerInjector(long action, Injector injector) { ... }

  // Интерфейс инжекторов
  public static interface Injector {
    void inject(View v, Invoker invoker); // Invoker - небольшая обертка над method.invoke()
    void reset(View v);
  }

  // Стандартные коды действий и классы-инжекторы
  public final static long CLICK = 1L << 32;
  public static class ClickInjector implements Injector {
    public void inject(View v, final Invoker invoker) {
      v.setOnClickListener(new View.OnClickListener() {
        public void onClick(View view) {
          invoker.invoke(view);
        }
      });
     }
    public void reset(View v) {
      v.setOnClickListener(null);
    }
  }

  public final static long LONGCLICK = 2L << 32;
  public static class LongClickInjector implements Injector { ... }

  // Аннотации
  public static @interface Id { int value(); }
  public static @interface On { long value(); }

  // Инициализация стандартных инжекторов
  static {
    registerInjector(CLICK, new ClickInjector());
    registerInjector(LONGCLICK, new LongClickInjector());
  }
}


Пока стандартных инжекторов только три — один выполняет метод по окончании инжекта (позволяет настроить виджет по вкусу, например для группы TextView назначить шрифт), два остальных инжектора делают обработку onClick и onLongClick соответственно. Но добавление остальных инжекторов (OnTouch, OnBeforeTextChanged, OnItemClick, ...) — это дело техники.

Полностью код класса Knork можно увидеть здесь.

Реализация inject() и reset() довольно тривиальная — первый метод перебирает аннотированные поля и методы через рефлексию и запоминает список внедренных виджетов и методов, второй пробегается по этим спискам и просит инжекторы отвязать соответствующие методы.

Цена успеха. Бенчмарки


Я набросал простенький пример, который заодно служит и бенчмарком. Вот результаты «холодного» старта на среднем телефоне полуторагодичной давности и на нексусе:

Обычный тормозной телефон
image

Nexus 5
image


В первом и втором бенчмарках я выполнял performClick() и callOnClick() на определенной (невидимой) кнопке. Странно, но потери от method.invoke() по сравнению с прямым вызовом метода оказались меньше чем я ожидал (я думал в десятки-сотни раз)

В третьем бенчмарке я инжектил вьюхи, удалял, инжектил повторно и так далее. Knork в этом случае действительно в 10..100 раз медленнее по сравнению с ButterKnife и обычной реализацией вручную. Хотя не стоит забывать, что ButterKnife не удаляет listener'ы во время резета, читер эдакий. Здесь есть куда копать — можно запоминать найденные поля и методы в кэше чтобы не использовать рефлексию повторно, это даст большой выигрыш в адаптерах. Кроме того можно посмотреть на ускорение поиска аннотаций, как это делают в ORMLite и других библиотеках.

Но все равно в итоге мы понимаем, что Knork не быстрый. Казалось бы, самое время мне признать поражение, однако в абсолютных цифрах на инжекты вьюх и на обработчики событий сейчас в Knork обычно тратится до 10 миллисекунд. Лично меня подобная задержка при открытии какого-нибудь фрагмента устраивает, так что я все равно попробую использовать Knork в своих проектах.

Дальнейшее развитие у проекта вполне предсказуемо — добавить больше инжекторов, добавить поддержку списков в аннотацию On (как в ButterKnife, чтобы не писать несколько аннотаций), добавить тесты, возможно добавить кэш методов чтобы ускорить инжект. Может быть добавлю библиотеку в какой-нибудь AAR-репозиторий, но пока что я непроходимо темный в этой области и не разобрался как это правильно делать в Gradle (может кто поможет?).

Ну вот собственно и все. Исходники библиотеки и примера/бенчмарка — bitbucket.org/trikita/knork. Лицензия — MIT.
Tags:
Hubs:
+5
Comments15

Articles