This post focuses on the butterKnife and the technical points behind it. The structure of the post is as follows

0x00 butterknife

Project address: github.com/JakeWharton…

Github was introduced in this way

Field and method binding for Android views which uses annotation processing to generate boilerplate code for you.

Translation:

FindViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById: findViewById

0x01 Basic Use

See demo address github.com/xsfelvis/Bu…

configuration

In the main project

Dependencies {the compile 'com. Jakewharton: butterknife: 8.8.1 annotationProcessor 'com. Jakewharton: butterknife - compiler: 8.8.1'}Copy the code

Use in Library

First you need to add it in the project buildScript

Dependencies buildscript {repositories {mavenCentral ()} {the classpath 'com. Jakewharton: butterknife - gradle - plugin: 8.8.1' }}Copy the code

And then in a moudle

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
Copy the code

And use R2 instead of R

class ExampleActivity extends Activity { @BindView(R2.id.user) EditText username; @BindView(R2.id.pass) EditText password; . }Copy the code

R2 is used in the Library because R is not final in the Library, but annotations such as BindView require the id of R to be final.

According to classShark analysis, the number of methods in this library is small, only 112

Routine use of

  • Actvity in

Using the @bindView annotation to a member variable and passing in a View ID, ButterKnife finds the corresponding View and automatically converts the View to a specific subclass:

  • Resource binding

Bind resources to class members using @bindbool, @bindColor, @Binddimen, @bindDrawable, @bindint, @bindString. To use this annotation, you need to pass in the corresponding RESOURCE ID, such as @bindString. You need to pass in the resource ID of r.string.id_string.

  • Layout of the binding

Butter Knife provides several overhauls of bind, allowing annotation binding to be used in any object as long as the layout is passed in, usually in fragments and adapters

  • Fragments of

    public class FancyFragment extends Fragment { @BindView(R.id.button1) Button button1; @BindView(R.id.button2) Button button2; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fancy_fragment, container, false); ButterKnife.bind(this, view); // TODO Use fields... return view; }}}Copy the code
  • In the Adapter

    public class MyAdapter extends BaseAdapter { @Override public View getView(int position, View view, ViewGroup parent) { ViewHolder holder; if (view ! = null) { holder = (ViewHolder) view.getTag(); } else { view = inflater.inflate(R.layout.whatever, parent, false); holder = new ViewHolder(view); view.setTag(holder); } holder.name.setText("John Doe"); // etc... return view; } static class ViewHolder { @BindView(R.id.title) TextView name; @BindView(R.id.job_title) TextView jobTitle; public ViewHolder(View view) { ButterKnife.bind(this, view); }}}Copy the code
  • Listener binding

Listeners can be automatically bound to specific execution methods:

@onclick (r.i.SUBmit) public void submit(View View) {// TODO submit data to server... } // multiple bindings @onclick ({R.I.B tnJumpToLib, R.I.B None, R.id.btnTwo}) void responseClick(View view) { switch (view.getId()) { case R.id.btnJumpToLib: jumpToLib(); break; case R.id.btnOne: Toast.makeText(this, "click btnOne", Toast.LENGTH_SHORT).show(); break; case R.id.btnTwo: Toast.makeText(this, "click btnTwo", Toast.LENGTH_SHORT).show(); break; default: break; }}Copy the code
  • Reset the binding

Fragments have a different life cycle than activities. In fragments, if you use bindings in onCreateView, you need to set all views to null in onDestroyView. To do this, ButterKnife returns an Unbinder instance so you can do this. This is done by calling the unbind function in the appropriate lifecycle callback.

public class FancyFragment extends Fragment { @BindView(R.id.button1) Button button1; @BindView(R.id.button2) Button button2; private Unbinder unbinder; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fancy_fragment, container, false); unbinder = ButterKnife.bind(this, view); // TODO Use fields... return view; } @Override public void onDestroyView() { super.onDestroyView(); unbinder.unbind(); }}Copy the code
  • Optional binding

By default, both @bind and listener binding are required, and Butter Knife will throw an exception if the target view is not found.

If you don’t want to use this default behavior and want to create an optional binding, you can just use the @Nullable annotation on variables or the @Option annotation on functions.

Matters needing attention

  • In the Activity ButterKnife. Bind (this); It must come after setContentView(), and after the parent class bind, the subclass does not need to bind
  • Use butterknife. bind(this, mRootView) in the Fragment;
  • Attribute layouts cannot be private or static, otherwise an error will be reported
  • SetContentView () cannot be implemented with annotations.
  • ButterKnife has been updated to version 8.0.7 and was previously called @injectView, but is now called @bind, which is more semantically appropriate.
  • In the Fragment life cycle, onDestoryView also needs butterknife.unbind (this)
  • Use r2.id.xxx in Libbray

0x02 Code generation techniques explored

From the above we can see that the use of an annotation can group related code automatically generated, such as in the demo implements Unbinder interface MainActivity_ViewBinding file (build/generated/source/apt /). There are three core technologies involved

  • Compile-time annotations
  • APT(Annotation Processor)
  • JavaPoet (automatic code generation)

There are three technical points that need to be understood before analyzing ButterKnife’s source code, and I’ll cover them one by one, and then talk about them at the end

annotations

Java native annotations

There are two main categories

  • Yuan notes

  • Common annotations

Yuan notes (meta – the annotation)

The translation is “annotations of annotations”, that is, annotations used to annotate other annotations, public four, often used for custom annotations

  • @Target

Target describes the scope of the annotation, using methods such as @target (ElementType.TYPE). The values of ElementType are as follows:

FIELD FIELD declaration (including enum instances) elemenetType. LOCAL_VARIABLE Local variable declaration elemenetType. METHOD Elemenettype. PACKAGE PACKAGE elemenetType. PARAMETER Specifies the elemenetType. TYPE class, interface (including annotation TYPE) or enum declarationCopy the code
  • @Retention

Retention is the term used to describe the life cycle of this note (English meaning “Retention”, i.e. the “lifetime” of the note)

@Retention(RetentionPolicy.RUNTIME)
Copy the code

There are three types of save policies

The SOURCE: Annotation only remains in the original code and is discarded by the compiler when it compiles. CLASS: The compiler will record the Annotation in the CLASS file, but the JVM will discard it when the Java program executes. RUNTIME: On the basis of retationPolicy.class, the JVM does not discard this annotation when executing it, so we can usually get this annotation in the program by reflection and then process it.Copy the code

Tips:

  • As we know, Java code has source code (Java) that is compiled by the compiler into class files (binary bytecode) and then interpreted by the JVM. Retention describes how long annotations survive in this phase. Therefore, annotations are treated differently depending on their lifecycle. This is very important. ButterKnife comments that use the Runtime policy only affect compile-time, but because they are stored only in bytecode, Therefore, some other means are needed to preserve annotation information and pass it to the virtual machine for execution, usually using custom annotation handlers
  • SOURCE < CLASS < RUNTIME = SOURCE < CLASS < RUNTIME = SOURCE < CLASS < RUNTIME Generally, if you need to dynamically retrieve annotation information at RUNTIME, you can only use RUNTIME annotations. Use CLASS annotations if you want to do some pre-processing at compile time, such as generating auxiliary code (such as ButterKnife). If you just do some checking operations, such as @Override and @SuppressWarnings, use the SOURCE annotation.

  • @Document

Document tag This annotation should be recorded by the Javadoc tool. By default, Javadoc does not include annotations.

  • @Inherited

Inherited. If an annotation type with the @Inherited annotation is used on a class, the annotation will be used on a subclass of that class.

Common annotations

Annotations used to describe code

Custom annotations

We can implement our own annotations by using meta-annotations. When we use @Interface to customize annotations, we cannot inherit from other annotations or interfaces. @interface is used to declare an annotation, and each of these methods is actually declaring a configuration parameter. The name of the method is the name of the parameter, and the return type is the type of the parameter. The default value of the parameter can be declared by default. Take a look at the custom annotation BindeView in ButterKnife

    @Retention(CLASS) @Target(FIELD)
    public @interface BindView {
      /** View ID to which the field will be bound. */
      @IdRes int value();
    }
Copy the code

use

@BindView(R.id.title) TextView title;
Copy the code

Deal with annotations

See Demo for an example

The way you handle annotations has to do with meta annotation @Retention, again

SOURCE < CLASS < RUNTIME, so where the former works, the latter must also work. Generally, if you need to dynamically retrieve annotation information at RUNTIME, you can only use RUNTIME annotations. Use CLASS annotations if you want to do some pre-processing at compile time, such as generating auxiliary code (such as ButterKnife), page routing information, etc. If you just do some checking operations, such as @Override and @SuppressWarnings, use the SOURCE annotation.

@Retention(SOURCE)

SOURCE annotations (retentionPolicy.source) have a lifetime of only Java SOURCE files, which is the shortest of the three. You don’t really need to do it, like @indef, @stringdef, etc

@Retention(Class)

Use APT to handle annotations

@Retention(RunTime)

Reflection is usually available for the longest life cycle, but custom annotators can also be used

The following is a detailed introduction to the implementation of an annotation with APT handling @Retention(Class) strategy. Before introducing this, we must first look at the process of handling annotations in Java

Annotation Processing is a tool in JavAC used to scan and parse Java annotations at compile timeCopy the code

You can define annotations and customize parsers to handle them. Annotation Processing is done at compile time, and the idea is to read the Java source, parse the annotations, and then generate new Java code. The newly generated Java code is eventually compiled into Java bytecode, and the Annotation Processor cannot change the Java classes read in, such as adding or removing Java methods

Since you need to reference the APT plugin, you need to add it in BuildScript

The classpath 'com. Neenbedankt. Gradle. Plugins: android - apt: 1.8'Copy the code

So what is Android-APT? The website has this description:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:

1, Allow to configure a compile time only annotation processor as a dependency, Not including the artifact in the final APK or library 2, Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio

Basically, it has two functions:

- Can rely on the annotation processor at compile time and work, but does not include anything left over when generating APK - can assist Android Studio to store files generated by the annotation processor at compile time in the corresponding directory of the projectCopy the code

A custom annotator needs to inherit AbstractProcessor and implement the following important methods:

  • Init () to get utility classes such as Elements, Types, and Filer
  • GetSupportedAnnotationTypes annotations () to describe annotation processor need to be addressed
  • Process () scans the annotations to generate code

And javaPoet is used to automatically generate code when generating files. JavaPoet is mainly used for

JavaPoet is a Java API for generating .java source files.
Copy the code

You can refer to github.com/square/java… I won’t expand the explanation here.

However, with the introduction of butterknife, there is no need to introduce apt, because a annotationprocessor was introduced before, and these libraries are written by jakewarton, so they are directly excluded. If continue to introduce transactions Using incompatible plugins for the annotation processing, specific solutions can see stackoverflow.com/questions/4…

0x03 butterKnife core source code analysis

Now that you have this foundation, you can analyze it. Now recall the annotation processing flowchart mentioned above,

The core idea of Butterknife is this

Scanning annotations are analyzed when compiling the source file, and when annotations such as @BindView and @onClick defined by ButterKnife are scanned, JavaPoet is used to generate the code. The generated file is parsed again until no annotation location is parsed that needs to be processed.

Don’t say comment + reflection

Analysis of source code is mainly divided into the following two steps:

  • Apt automatically generated classes
  • How do generated classes relate to the ButterKnife internal framework

Automatically generated classes

Take a look at some of the auto-generated class code in the demo

From this generated class we can get the following information:

  • Butterknife uses annotations to generate some helper code that shields us from the tedious details at the framework level

  • You need to get a decorView to operate on a view with a relative Id (this also confirms a previous point, butterknife.bind (this) in the Activity; Must come after setContentView())

Tips:

  • As the main thread of the application, ActivityThread is responsible for handling various core events, such as AMS notifying the application process to start an Activity, which is ultimately translated into the LAUNCH_ACTIVITY message managed by ActvityThread. Then call handleLaunchActivity, which is the starting point for the entire ViewTree setup process
  • The function is mainly generated an Activity object and call them attach method, and through mInstrumentation callActivityOnCreate call Activity. The onCreate, resulting in a PhoneWindow object. The Window object in an Activity is considered the “frame of the interface,” so the mDecor for the Activity is generated by the setContentView.
  • The Activity’s setContentView is just an intermediary that completes the construction of the DecorView through the corresponding Window object. See PhoneWindow#setContentView#installDecor() for details.
  • After the application process is fork from the Zygote process, the main method of the ActivityThread is called by reflection. In the invokeStaticMain method of RuntimeInit

How to eliminate fidviewById strong roll?

You can see the following code in the generated code

 target.mTvTitle = Utils.findRequiredViewAsType(source, R.id.tvTitle, "field 'mTvTitle'", TextView.class);
Copy the code

Before Utils# findRequiredViewAsType, of which the source is the incoming taget. GetWindow. GetDecoreView

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who, Class<T> cls) { View view = findRequiredView(source, id, who); CastView (view, id, who, CLS); } public static View findRequiredView(View source, @idres int id, String who) { View view = source.findViewById(id); FindViewById if (view! = null) { return view; }... } public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) { try { return cls.cast(view); } Catch (ClassCastException e) {... }}Copy the code

How does MainActivity know and instantiate the helper class MainActivity_ViewBind generated for it by the framework?

Remember that you need to use bind before you use it, for example, activity,

ButterKnife.bind(this);
Copy the code

ButterKnife#bind

@NonNull @UiThread public static Unbinder bind(@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); DevoreView return createBinding(target, sourceView); // Perform binding}Copy the code

What does the bind operation do

ButterKnife#createBinding

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) { Class<? > targetClass = target.getClass(); if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName()); Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); If (constructor == null) {return unbinder.empty; // NoInspection trywithidenticaltodcatch catch: API 19+ only type. try {// Instance MainActivity_ViewBinding(final) created by reflection  MainActivity target, View source) return constructor.newInstance(target, source); } catch (IllegalAccessException e) {...... }}Copy the code

This function is to find the corresponding constructor, then create an instance, emphasis in the further findBindingConstructorForClass function

@Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<? > cls) { // Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); if (bindingCtor ! = null) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; } String clsName = cls.getName(); / / filter system related to the if (clsName. StartsWith (" android. ") | | clsName. StartsWith (" Java. ")) {if (debug) Log. D (TAG, "MISS: Reached framework class. Abandoning search."); return null; } try {// Obtain the corresponding viebindind Class, the file name is + "_ViewBinding" Class<? > bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding"); //noinspection unchecked bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class); if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor."); } catch (ClassNotFoundException e) { if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()); bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to find binding constructor for " + clsName, e); } // Cache the resulting Constructor to avoid performance issues with reflection. BINDINGS.put(cls, bindingCtor); return bindingCtor; }Copy the code

How do I deal with the annotations and the corresponding code helper files

Take a look at the butterKnife engineering structure

Annotations All of the annotations involved are annotations in Butterknife-Complier, a custom annotation handler that handles Class policy annotations and generates the corresponding code helper files

As mentioned earlier, the annotation handler contains the following important methods:

  • Init () to get utility classes such as Elements, Types, and Filer
  • GetSupportedAnnotationTypes annotations () to describe annotation processor need to be addressed
  • Process () scans the annotations to generate code

So the core points are all in the annotation handler’s process() function, so get to the point

ButterKnifeProcessor#process

@Override public boolean process(Set<? Extends TypeElement> elements, RoundEnvironment env) { Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile = binding. BrewJava (SDK, debuggable); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } } return false; }Copy the code

As you can see, the core function is very concise and has a clear division of work. It finds all the bind information in the project and generates the corresponding file

ButterKnifeProcessor#findAndParseTargets is the binding information function. This function is extremely long, mainly parsing the definition of various annotations, we here cut parsing BindeView, other principles are the same

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); scanForRClasses(env); // Process each @BindView element. for (Element element : env.getElementsAnnotatedWith(BindView.class)) { // we don't SuperficialValidation.validateElement(element) // so that an Unresolved View type can be generated by later processing rounds try {// unresolved View type can be generated by later processing rounds try {  parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); }}... return bindingMap; }Copy the code

This method is the main method for handling various annotations. I’ve deleted all the extra ones. This method basically takes all the annotations, parses the annotations, encapsulates all the information about the annotations in a BindingSet, The exact action for parsing should be in parseBindView(Element, builderMap, erasedTargetNames), and look below:

Butterknife# parseBindView method of parsing BindView annotations

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); ... Int id = element.getannotation (bindView.class).value(); // Assemble information on the field.  BindingSet.Builder builder = builderMap.get(enclosingElement); QualifiedId qualifiedId = elementToQualifiedId(element, id); ... String name = simpleName.toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); AddField (getId(qualifiedId), new FieldViewBinding(name, type, required)); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement); }Copy the code

Other annotations are treated similarly,

Code generation – Apply the javaPoet framework

The code generation entry is in the Butterknife# process, using JavaFile with javaPoet

JavaFile javaFile = binding.brewJava(sdk, debuggable); try { javaFile.writeTo(filer); } catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); }Copy the code

Call function chain

brewJava->createType

Private TypeSpec createType(int SDK, Boolean debuggable) {// Generate class name, Modifier is pblic TypeSpec. Builder result. = TypeSpec classBuilder (bindingClassName. SimpleName ()). AddModifiers (PUBLIC); if (isFinal) { result.addModifiers(FINAL); } // If (parentBinding! = null) { result.superclass(parentBinding.bindingClassName); } else { //private static final ClassName UNBINDER = ClassName.get("butterknife", "Unbinder"); So the generated class ends up implementing the Unbider interface result.addSuperInterface (UNBINDER); }... / / according to type, add different constructor if (isView) {result. AddMethod (createBindingConstructorForView ()); } else if (isActivity) {/ / the activity constructor is a default constructor, a parameter result. The addMethod (createBindingConstructorForActivity ()); } else if (isDialog) { result.addMethod(createBindingConstructorForDialog()); } if (! constructorNeedsView()) { // Add a delegating constructor with a target type + view signature for reflective use. result.addMethod(createBindingViewDelegateConstructor()); Result.addmethod (createBindingConstructor(SDK, debuggable)); result.addMethod(createBindingConstructor(SDK, debuggable)) If (hasViewBindings () | | parentBinding = = null) {/ / create unbinder function result. AddMethod (createBindingUnbindMethod (result); } return result.build(); } / / generated in the activy constructor private MethodSpec createBindingConstructorForActivity () {MethodSpec. Builder Builder = MethodSpec.constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC) .addParameter(targetTypeName, "target");  if (constructorNeedsView()) { builder.addStatement("this(target, target.getWindow().getDecorView())"); } else { builder.addStatement("this(target, target)"); } return builder.build(); }Copy the code

This function is responsible for generating functions such as the main constructor, multiple constructors for the binding containing findView, unbinding the unbinder function, and so on

The default constructor for MainActivity_ViewBinding is automatically generated with the Unbinder interface. This is just an overview of the class. The constructor for MainActivity_ViewBinding is a one-parameter constructor for all views. The constructor for the id operation is also above result.addMethod(createBindingConstructor(SDK, debuggable)); Function,

Private MethodSpec createBindingConstructor(int SDK, Boolean debuggable) {// Create a constructor that is public. And add annotations for UiThread MethodSpec. Builder constructor. = MethodSpec constructorBuilder () addAnnotation (UI_THREAD) .addModifiers(PUBLIC); ... ConstructorNeedsView () {constructor.addParameter(View, "source"); constructorNeedsView() {constructor.addParameter(View, "source"); } else {// otherwise add Context Context constructor. AddParameter (Context, "Context "); }... If (hasViewBindings()) {if (hasViewLocal()) {// Local variable in which all views will be temporarily stored. constructor.addStatement("$T view", VIEW); } for (ViewBinding Binding: viewBindings) {// Generate code like findViewById for View bindings!! addViewBinding(constructor, binding, debuggable); } for (FieldCollectionViewBinding binding : collectionBindings) { constructor.addStatement("$L", binding.render(debuggable)); } if (! resourceBindings.isEmpty()) { constructor.addCode("\n"); }}... return constructor.build(); }Copy the code

You can see the logic for generating the code, where one of the most important findView methods, addViewBinding, is interpreted as follows

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) { if (binding.isSingleFieldBinding()) { // Optimize the common case where there's a single binding directly to a field. FieldViewBinding fieldBinding = binding.getFieldBinding(); // Notice that the target. Form is used directly, Builder Builder = codebLock. Builder ().add("target.$L = ", fieldbinding.getName ()); Boolean requiresCast = requiresCast(fieldbinding.getType ()); if (! requiresCast && ! fieldBinding.isRequired()) { builder.add("source.findViewById($L)", binding.getId().code); } else { builder.add("$T.find", UTILS); builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView"); if (requiresCast) { builder.add("AsType"); } builder.add("(source, $L", binding.getId().code); if (fieldBinding.isRequired() || requiresCast) { builder.add(", $S", asHumanDescription(singletonList(fieldBinding))); } if (requiresCast) { builder.add(", $T.class", fieldBinding.getRawType()); } builder.add(")"); } result.addStatement("$L", builder.build()); return; } List requiredBindings = binding.getRequiredBindings(); if (requiredBindings.isEmpty()) { result.addStatement("view = source.findViewById($L)", binding.getId().code); } else if (! binding.isBoundToRoot()) { result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS, binding.getId().code, asHumanDescription(requiredBindings)); } addFieldBinding(result, binding); AddMethodBindings (result, binding); }Copy the code

This is the end of code generation analysis

0 x04 summary

  • This blog post mainly relies on butetrKnife framework, introduces its use, before the analysis of the source code foundation of its core technology, some advanced annotation play, custom annotation processor, Javapoet automatic generation code, and further in-depth analysis of butterKnife source code;
  • Among them, apt+javaPoet is also widely used in some large open source libraries, such as EventBus3.0+, page routing ARout, Dagger, Retrofit and so on
  • Annotations are not only used by reflection but can also be handled at compile time using APT

Finally, I hope you can learn something and have a good weekend

Refer to the link

  • www.cnblogs.com/qiumingchen…
  • Blog.csdn.net/u011326979/…
  • Blog.csdn.net/github_3518…
  • Juejin. Cn/post / 684490…
  • Stackoverflow.com/questions/4…