1. 程式人生 > >ButterKnife手原始碼分析和手寫

ButterKnife手原始碼分析和手寫

這裡寫圖片描述

參考butterknife原始碼進行手寫,首先分model:butterknife,compiler和annomation

首先butterknife原始碼分析:使用butterknife之後會自動生成這個類

package com.peakmain.butterkinfe;

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this
(target, target.getWindow().getDecorView()); } @UiThread public MainActivity_ViewBinding(MainActivity target, View source) { this.target = target; target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'", TextView.class); target.textView2 = Utils.findRequiredViewAsType(source, R.id.tv2, "field 'textView2'"
, TextView.class); } @Override @CallSuper public void unbind() { MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.textView1 = null; target.textView2 = null; } }

app關聯三個model,並修改其中的compiler和新增外掛

compile project(':butterknife_annotations')
apt project(':butterknife_compiler')
compile project(':butterknif

apply plugin: 'com.neenbedankt.android-apt'

complier關聯annotation(主要用來放註解)

@Retention(RetentionPolicy.CLASS)//編譯時註解
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

ButterknifeProcessor生成器:新增依賴

 compile 'com.google.auto.service:auto-service:1.0-rc3'
 compile 'com.squareup:javapoet:1.8.0'
/*支援中文*/
tasks.withType(JavaCompile){
    options.encoding='UTF-8'
}

//ButterKnifeProcessor 原始碼
AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    private Filer mFiler;
    private Elements mElementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    // 1. 指定處理的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    // 2. 給到需要處理的註解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        // 需要解析的自定義註解 BindView  OnClick
        annotations.add(BindView.class);
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // System.out.println("------------------------>");
        // System.out.println("------------------------>");
        // System.out.println("------------------------>");
        // System.out.println("------------------------>");

        // process 方法代表的是,有註解就都會進來 ,但是這裡面是一團亂麻
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        /*for (Element element : elements) {

            Element enclosingElement = element.getEnclosingElement();
            System.out.println("------------------------>"+element.getSimpleName().toString()+" "+enclosingElement.getSimpleName().toString());
        }*/
        // 解析 屬性 activity -> List<Element>
        Map<Element, List<Element>> elementsMap = new LinkedHashMap<>();
        for (Element element : elements) {
            Element enclosingElement = element.getEnclosingElement();

            List<Element> viewBindElements = elementsMap.get(enclosingElement);
            if (viewBindElements == null) {
                viewBindElements = new ArrayList<>();
                elementsMap.put(enclosingElement, viewBindElements);
            }

            viewBindElements.add(element);
        }

        // 生成程式碼
        for (Map.Entry<Element, List<Element>> entry : elementsMap.entrySet()) {
            Element enclosingElement = entry.getKey();
            List<Element> viewBindElements = entry.getValue();

            // public final class xxxActivity_ViewBinding implements Unbinder
            String activityClassNameStr = enclosingElement.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
            ClassName unbinderClassName = ClassName.get("com.butterknife","Unbinder");
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityClassNameStr+"_ViewBinding")
                    .addModifiers(Modifier.FINAL,Modifier.PUBLIC).addSuperinterface(unbinderClassName)
                    .addField(activityClassName,"target",Modifier.PRIVATE);


            // 實現 unbind 方法
            // android.support.annotation.CallSuper
            ClassName callSuperClassName = ClassName.get("android.support.annotation","CallSuper");
            MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC,Modifier.FINAL)
                    .addAnnotation(callSuperClassName);

            unbindMethodBuilder.addStatement("$T target = this.target",activityClassName);
            unbindMethodBuilder.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");");

            // 建構函式
            MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
                    .addParameter(activityClassName,"target");
            // this.target = target;
            constructorMethodBuilder.addStatement("this.target = target");
            // findViewById 屬性
            for (Element viewBindElement : viewBindElements) {
                // target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'", TextView.class);
                // target.textView1 = Utils.findViewById(source, R.id.tv1);
                String filedName = viewBindElement.getSimpleName().toString();
                ClassName utilsClassName = ClassName.get("com.butterknife","Utils");
                int resId = viewBindElement.getAnnotation(BindView.class).value();
                constructorMethodBuilder.addStatement("target.$L = $T.findViewById(target, $L)",filedName,utilsClassName,resId);
                // target.textView1 = null;
                unbindMethodBuilder.addStatement("target.$L = null",filedName);
            }


            classBuilder.addMethod(unbindMethodBuilder.build());
            classBuilder.addMethod(constructorMethodBuilder.build());

            // 生成類,看下效果
            try {
                String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();

                JavaFile.builder(packageName,classBuilder.build())
                        .addFileComment("butterknife 自動生成")
                        .build().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("翻車了!");
            }
        }
        return false;
    }

最後model是Butterknife目的如butterknife外掛使用的使用的時候butterknife.bind(this);

public class ButterKnife {
    public static Unbinder  bind(Activity activity){
        // xxxActivity_ViewBinding viewBinding = new xxxActivity_ViewBinding(this)
        try {
            Class<? extends Unbinder> bindClassName=(Class<? extends Unbinder>)
                     Class.forName(activity.getClass().getName()+"_ViewBinding");
            //建構函式  new MainActivity_ViewBinding()
            Constructor<? extends Unbinder> bindConstructor  = bindClassName.getDeclaredConstructor(activity.getClass());
            Unbinder unbinder = bindConstructor.newInstance();
            return unbinder;
        } catch (Exception e) {
            e.printStackTrace();
            return Unbinder.EMPTY;
        }
    }
}

Unbinder是複製butterknife的原始碼


public interface Unbinder {
    @UiThread
    void unbind();

    Unbinder EMPTY = new Unbinder() {
        @Override public void unbind() { }
    };
}
public class Utils {
    public static <T extends View> T findViewById(Activity activity,int viewId){
        return (T) activity.findViewById(viewId);
    }
}

使用

  @BindView(R.id.tv1)
    TextView textView1;

    @BindView(R.id.tv2)
    TextView textView2;

    @BindView(R.id.tv2)
    TextView textView3;
      @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Unbinder unbinder = ButterKnife.bind(this);


        /*textView1.setText("textView1");

        textView2.setText("textView2");*/
    }

這時候就完成了但是這時候的生成的id是一串整形資料,這時候我們需要根據id的數字獲得到R.id.xxx,複製butterknife的相關原始碼即可

package com.butterknife.annotation;


import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import com.sun.source.tree.ClassTree;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeScanner;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;


/**
 * 作者:Peakmain
 * 版本:1.0
 * 建立日期:2018/3/6 22:31
 * 郵件:[email protected]
 * 描述:
 */
@AutoService(Processor.class)
public class ButterknifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Elements mElementUtils;
    private final Map<QualifiedId, Id> symbols = new LinkedHashMap<>();

    private Trees trees;
    private static final List<String> SUPPORTED_TYPES = Arrays.asList(
            "array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string"
    );
    private Types typeUtils;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        trees = Trees.instance(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
    }

    //1.獲得當前版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();//最大版本
    }

    // 2. 給到需要處理的註解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        //添加註解
        annotations.add(BindView.class);

        return annotations;
    }

    //所有註解都會走向這裡
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    /*     System.out.println("------------------------>");
         System.out.println("------------------------>");
         System.out.println("------------------------>");
         System.out.println("------------------------>");*/
        // process 方法代表的是,有註解就都會進來 ,但是這裡面是一團亂麻
        /*Set<? extends Element> elements  = roundEnv.getElementsAnnotatedWith(BindView.class);
            for (Element element : elements) {//element.getSimpleName().toString()  自己定義的名字
                Element enclosingElement  = element.getEnclosingElement();//代表MainActivity或者LoginActivity
            System.out.println("------------------------>"+
                    element.getSimpleName().toString()+" "+enclosingElement.getSimpleName().toString());
        }*/
        //解析屬性 activity->list<Element>
        scanForRClasses(roundEnv);
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        Map<Element, List<Element>> elementsMap = new LinkedHashMap<>();
        for (Element element : elements) {
            Element enclosingElement = element.getEnclosingElement();//獲得的是activity的名字
            List<Element> viewBindElement = elementsMap.get(enclosingElement);//根據key獲得所有註解下自己定義的名字,預設是空
            if (viewBindElement == null) {
                viewBindElement = new ArrayList<>();
                elementsMap.put(enclosingElement, viewBindElement);
            }
            viewBindElement.add(element);//新增節點
        }
        //生成程式碼
        for (Map.Entry<Element, List<Element>> entry : elementsMap.entrySet()) {
            Element enclosingElement = entry.getKey();//獲得key
            List<Element> viewBindElement = entry.getValue();//獲得value
            //生成public final class xxxActivity_ViewBinding implements Unbinder
            String activityClassNameStr = enclosingElement.getSimpleName().toString();//如MainActivity
            ClassName activityName = ClassName.bestGuess(activityClassNameStr);
            ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");//獲得類名
            //報錯需要新增依賴  生成public final class xxxActivity_ViewBinding implements Unbinder
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                    .addSuperinterface(unbinderClassName)
                    .addField(activityName, "target", Modifier.PRIVATE);//新增屬性
            // 實現 unbind 方法
            // android.support.annotation.CallSuper
            ClassName callSuperClassName = ClassName.get("android.support.annotation", "CallSuper");
            MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addAnnotation(callSuperClassName);

            //新增構造
            MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
                    .addParameter(activityName, "target");
            // this.target = target
            constructorMethodBuilder.addStatement(" this.target = target");

            //findvById
            for (Element element : viewBindElement) {
                // target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'", TextView.class);
                // target.textView1 = Utils.findViewById(source, R.id.tv1);
                String filedName = element.getSimpleName().toString();//屬性的名字,如tv_name
                ClassName utilsClassName = ClassName.get("com.butterknife", "Utils");
                int resId = element.getAnnotation(BindView.class).value();
                QualifiedId qualifiedId = elementToQualifiedId(element, resId);
                Id id = getId(qualifiedId);
                CodeBlock codeBlock = id.code;
                constructorMethodBuilder.addStatement("target.$L =$L.findViewById(target,$L)", filedName, utilsClassName, codeBlock);
                // target.textView1 = null;
                unbindMethodBuilder.addStatement("target.$L = null", filedName);
            }

            classBuilder.addMethod(unbindMethodBuilder.build());
            classBuilder.addMethod(constructorMethodBuilder.build());
            //生成類,看下效果
            try {
                //生成包名
                String packName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
                JavaFile.builder(packName, classBuilder.build())
                        .addFileComment("butterknife 自動生成")
                        .build().writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("生成失敗!");
            }
        }
        return false;
    }

    private QualifiedId elementToQualifiedId(Element element, int id) {
        return new QualifiedId(mElementUtils.getPackageOf(element), id);
    }

    private Id getId(QualifiedId qualifiedId) {
        if (symbols.get(qualifiedId) == null) {
            symbols.put(qualifiedId, new Id(qualifiedId.id));
        }
        return symbols.get(qualifiedId);
    }

    private void scanForRClasses(RoundEnvironment env) {
        if (trees == null) return;

        RClassScanner scanner = new RClassScanner();

        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            for (Element element : env.getElementsAnnotatedWith(annotation)) {
                JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
                if (tree != null) { // tree can be null if the references are compiled types and not source
                    scanner.setCurrentPackage(mElementUtils.getPackageOf(element));
                    tree.accept(scanner);
                }
            }
        }

        for (Map.Entry<PackageElement, Set<Symbol.ClassSymbol>> packageNameToRClassSet
                : scanner.getRClasses().entrySet()) {
            PackageElement respectivePackageName = packageNameToRClassSet.getKey();
            for (Symbol.ClassSymbol rClass : packageNameToRClassSet.getValue()) {
                parseRClass(respectivePackageName, rClass, scanner.getReferenced());
            }
        }
    }
    private void parseRClass(PackageElement respectivePackageName, Symbol.ClassSymbol rClass,
                             Set<String> referenced) {
        TypeElement element;

        try {
            element = rClass;
        } catch (MirroredTypeException mte) {
            element = (TypeElement) typeUtils.asElement(mte.getTypeMirror());
        }

        JCTree tree = (JCTree) trees.getTree(element);
        if (tree != null) { // tree can be null if the references are compiled types and not source
            IdScanner idScanner =
                    new IdScanner(symbols, mElementUtils.getPackageOf(element), respectivePackageName,
                            referenced);
            tree.accept(idScanner);
        } else {
            parseCompiledR(respectivePackageName, element, referenced);
        }
    }
    private void parseCompiledR(PackageElement respectivePackageName, TypeElement rClass,
                                Set<String> referenced) {
        for (Element element : rClass.getEnclosedElements()) {
            String innerClassName = element.getSimpleName().toString();
            if (SUPPORTED_TYPES.contains(innerClassName)) {
                for (Element enclosedElement : element.getEnclosedElements()) {
                    if (enclosedElement instanceof VariableElement) {
                        String fqName = mElementUtils.getPackageOf(enclosedElement).getQualifiedName().toString()
                                + ".R."
                                + innerClassName
                                + "."
                                + enclosedElement.toString();
                        if (referenced.contains(fqName)) {
                            VariableElement variableElement = (VariableElement) enclosedElement;
                            Object value = variableElement.getConstantValue();

                            if (value instanceof Integer) {
                                int id = (Integer) value;
                                ClassName rClassName =
                                        ClassName.get(mElementUtils.getPackageOf(variableElement).toString(), "R",
                                                innerClassName);
                                String resourceName = variableElement.getSimpleName().toString();
                                QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
                                symbols.put(qualifiedId, new Id(id, rClassName, resourceName));
                            }
                        }
                    }
                }
            }
        }
    }

    private static class RClassScanner extends TreeScanner {
        // Maps the currently evaluated rPackageName to R Classes
        private final Map<PackageElement, Set<Symbol.ClassSymbol>> rClasses = new LinkedHashMap<>();
        private PackageElement currentPackage;
        private Set<String> referenced = new HashSet<>();

        @Override public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
            Symbol symbol = jcFieldAccess.sym;
            if (symbol != null
                    && symbol.getEnclosingElement() != null
                    && symbol.getEnclosingElement().getEnclosingElement() != null
                    && symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
                Set<Symbol.ClassSymbol> rClassSet = rClasses.get(currentPackage);
                if (rClassSet == null) {
                    rClassSet = new HashSet<>();
                    rClasses.put(currentPackage, rClassSet);
                }
                referenced.add(getFqName(symbol));
                rClassSet.add(symbol.getEnclosingElement().getEnclosingElement().enclClass());
            }
        }

        Map<PackageElement, Set<Symbol.ClassSymbol>> getRClasses() {
            return rClasses;
        }

        Set<String> getReferenced() {
            return referenced;
        }

        void setCurrentPackage(PackageElement packageElement) {
            this.currentPackage = packageElement;
        }
    }
    private static String getFqName(Symbol rSymbol) {
        return rSymbol.packge().getQualifiedName().toString()
                + ".R."
                + rSymbol.enclClass().name.toString()
                + "."
                + rSymbol.name.toString();
    }
    private static class IdScanner extends TreeScanner {
        private final Map<QualifiedId, Id> ids;
        private final PackageElement rPackageName;
        private final PackageElement respectivePackageName;
        private final Set<String> referenced;

        IdScanner(Map<QualifiedId, Id> ids, PackageElement rPackageName,
                  PackageElement respectivePackageName, Set<String> referenced) {
            this.ids = ids;
            this.rPackageName = rPackageName;
            this.respectivePackageName = respectivePackageName;
            this.referenced = referenced;
        }

        @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
            for (JCTree tree : jcClassDecl.defs) {
                if (tree instanceof ClassTree) {
                    ClassTree classTree = (ClassTree) tree;
                    String className = classTree.getSimpleName().toString();
                    if (SUPPORTED_TYPES.contains(className)) {
                        ClassName rClassName = ClassName.get(rPackageName.getQualifiedName().toString(), "R",
                                className);
                        VarScanner scanner = new VarScanner(ids, rClassName, respectivePackageName, referenced);
                        ((JCTree) classTree).accept(scanner);
                    }
                }
            }
        }
    }
    private static class VarScanner extends TreeScanner {
        private final Map<QualifiedId, Id> ids;
        private final ClassName className;
        private final PackageElement respectivePackageName;
        private final Set<String> referenced;

        private VarScanner(Map<QualifiedId, Id> ids, ClassName className,
                           PackageElement respectivePackageName, Set<String> referenced) {
            this.ids = ids;
            this.className = className;
            this.respectivePackageName = respectivePackageName;
            this.referenced = referenced;
        }

        @Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
            if ("int".equals(jcVariableDecl.getType().toString())) {
                String resourceName = jcVariableDecl.getName().toString();
                if (referenced.contains(getFqName(jcVariableDecl.sym))) {
                    int id = Integer.valueOf(jcVariableDecl.getInitializer().toString());
                    QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
                    ids.put(qualifiedId, new Id(id, className, resourceName));
                }
            }
        }
    }

    private static AnnotationMirror getMirror(Element element,
                                              Class<? extends Annotation> annotation) {
        for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
            if (annotationMirror.getAnnotationType().toString().equals(annotation.getCanonicalName())) {
                return annotationMirror;
            }
        }
        return null;
    }

}

Id

public final class Id {
    private static final ClassName ANDROID_R = ClassName.get("android", "R");

    final int value;
    final CodeBlock code;
    final boolean qualifed;

    Id(int value) {
        this.value = value;
        this.code = CodeBlock.of("$L", value);
        this.qualifed = false;
    }

    Id(int value, ClassName className, String resourceName) {
        this.value = value;
        this.code = className.topLevelClassName().equals(ANDROID_R)
                ? CodeBlock.of("$L.$N", className, resourceName)
                : CodeBlock.of("$T.$N", className, resourceName);
        this.qualifed = true;
    }

    @Override public boolean equals(Object o) {
        return o instanceof Id && value == ((Id) o).value;
    }

    @Override public int hashCode() {
        return value;
    }

    @Override public String toString() {
        throw new UnsupportedOperationException("Please use value or code explicitly");
    }
}

QualifiedId

public final class QualifiedId {
    final PackageElement packageName;
    final int id;

    QualifiedId(PackageElement packageName, int id) {
        this.packageName = packageName;
        this.id = id;
    }

    @Override public String toString() {
        return "QualifiedId{packageName='" + packageName + "', id=" + id + '}';
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof QualifiedId)) return false;
        QualifiedId other = (QualifiedId) o;
        return id == other.id
                && packageName.equals(other.packageName);
    }

    @Override public int hashCode() {
        int result = packageName.hashCode();
        result = 31 * result + id;
        return result;
    }