📜 ⬆️ ⬇️

Knork: ButterKnife's simplest alternative to 160 lines of code

Habra

Below we will talk about view injection, crutching, annotations, reflection, a pathetic attempt to beat Jake Wharton and that his bike is closer to the body.

What is view injection? This is a way to avoid such routine code:
')
Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // ... } }); 

If you use view injection with, say, ButterKnife , written by Jake Wharton, the code becomes more transparent:

 @InjectView(R.id.button) Button mButton; @OnClick(R.id.button) public void onButtonClick() { // ... } 

But on closer inspection, it turns out that ButterKnife is not perfect either.

First, it generates helper classes at compile time, and many IDEs and build systems sometimes go crazy (compiling classes in the wrong order). Although, of course, by design, this allows black magic not to degrade the performance of the code.

Secondly, it does not quite correctly cancel view injection - it resets the views, but the callbacks assigned to them are not. If used carelessly, this can lead to memory leaks and other errors (for example, if you make repeated injections in the adapter).

Thirdly, it is very difficult (if at all possible) to add your own binding, say, to bind a method to the View. OnKeyListener.

And, finally, it is very nontrivially arranged to connect it to the old Ant-based build system. But many projects have not yet switched to Gradle.

So I thought - and if not make your own ButterKnife with all the consequences? So it turned out plain Knork library (also cutlery, knife + fork ). The key features of the library are simplicity and small size.

Simplification 1. Dynamic processing of annotations in runtime


“But this is terrible!” - you will say, and you will be absolutely right. This is really slow, but at the end of the article I will give a small benchmark, and not everything is as bad as it seems in terms of speed. But this little horror will save us from code generation, build process errors, etc. And still allow to expand the library according to their needs.

Simplification 2. Only two annotations


We confine ourselves to just two annotations that are easy to remember:

Id - annotation before class field, needed for widget injection.
On - the annotation before the method is needed for injecting various Listeners.

But how do we pass the widget identifier to @On (), and also the action to which the annotated method should be attached? We know that an annotation can have only one nameless value, and for a larger number of parameters it will be necessary to give names, that is:

 @On(R.id.button) // : @On(value=R.id.button, action=CLICK) 


The old skills of embedded-development and the enduring love for ugly non-trivial solutions. We know that ID can be an integer in the range 0x7f000000..0xffffffff. And in annotations you can use 64-bit long. This gives us free upper 32 bits for personal use. There we will store the event number with which you want to associate the method. For example:

 @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) { // ... } 

In my humble opinion, the readability of such a code is not much worse than the above-mentioned annotations with parameters.

Simplification 3. Flexible Injection Classes


It turns out that our main class Knork, dealing with injection, will run over the object, look for annotations and for each annotation On will find the corresponding injector and delegate control to it. This means that the developer will be able to add his own injectors in the process of the program. The injectors will be responsible for binding the method to the widget, as well as for deleting the created listeners. No leakage.

Overall picture


All the code turned out to be within the framework of a single Knork class, so to connect, you just need to write:

 import static trikita.knork.Knork.*; 


This ideologically is not entirely correct, but since our class will be only a half hundred lines - I hope you will forgive this approach.

So, in the Knork class there will be something like the following:

 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()); } } 


While there are only three standard injectors — one performs the method after the end of the injection (allows you to customize the widget to your taste, for example, assign a font for the TextView group), the other two injectors do onClick and onLongClick processing, respectively. But adding other injectors (OnTouch, OnBeforeTextChanged, OnItemClick, ...) is a technical matter.

Fully Knork class code can be seen here .

The implementation of inject () and reset () is rather trivial - the first method iterates over the annotated fields and methods through reflection and remembers the list of embedded widgets and methods, the second goes over these lists and asks the injectors to untie the corresponding methods.

The price of success. Benchmarks


I sketched a simple example, which also serves as a benchmark. Here are the results of the "cold" start on the average phone of a year and a half ago and on the nexus:

Normal brake phone
image

Nexus 5
image


In the first and second benchmarks, I performed performClick () and callOnClick () on a specific (invisible) button. Strange, but the losses from method.invoke () compared with a direct method call turned out to be less than I expected (I thought tens or hundreds of times)

In the third benchmark, I injected the view, deleted it, injected again, and so on. Knork in this case is indeed 10..100 times slower compared to ButterKnife and the usual manual implementation. Although we should not forget that ButterKnife does not remove listeners during a cut, a cheater. There is where to dig - you can memorize the found fields and methods in the cache so as not to use reflection again, this will give a big gain in adapters. In addition, you can look at accelerating the search for annotations, as is done in ORMLite and other libraries.

But still, in the end, we understand that Knork is not fast. It would seem that the time is right for me to admit defeat, however, in absolute terms, the views and event handlers injects now usually spend up to 10 milliseconds in Knork. Personally, I like this delay when opening a fragment, so I still try to use Knork in my projects.

Further development of the project is quite predictable - add more injectors, add support for lists to the On summary (as in ButterKnife, not to write a few annotations), add tests, it is possible to add a cache of methods to speed up the injection. Maybe I will add the library to some AAR-repository, but so far I am impassively dark in this area and have not figured out how to do this correctly in Gradle (can anyone help?).

Well, that's all. Sources of the library and example / benchmark - bitbucket.org/trikita/knork . License - MIT.

Source: https://habr.com/ru/post/230857/


All Articles