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

от автора

Хабрапривет!

Ниже речь пойдет о 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/zserge/knork. Лицензия — MIT.

ссылка на оригинал статьи http://habrahabr.ru/post/230857/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *