Android注解AndroidAnnotation的使用及实现流程分析

Hello,大家好,老衲第一次在简书上发文章,请大家多多支持。今天给大家带来的是Android开发中常用的AndroidAnnotation(以下简称AA)框架的使用及其内部的实现流程。

AA在Android开发者中使用非常广泛。他减少了无用代码的编写。提高了开发者的效率。让开发者将更多的时间放到真正需要关注的地方。首先说明下AA的使用方法 , 这里以AndroidStudio为例

如何在AndroidStudio中使用AA注解框架

首先说明下需要修改的文件
Paste_Image.png
1.在工程的根Build.gradle文件中需要添加如下代码
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
Paste_Image.png
2.在app目录下的Build.gradle文件中需要添加如下四段代码
//1.
apply plugin: 'android-apt'
//2.
def AAVersion = '3.3.2'
//3.
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
//4.
apt {
arguments {
    androidManifestFile variant.outputs[0].processResources.manifestFile
    resourcePackageName 'com.example.annotation.annotationtest'
}

}
添加位置如下

Paste_Image.png
3.修改AndroidManifest.xml文件
修改规则:需要将加入@EActivity,@EService等注解的类的文件名后加下划线,如下图,因为通过AA来生成新的类名
是在原有的基础上加上了下划线,所以我们声明的时候也需要加上下划线,否则提示找不到文件。
Paste_Image.png

常用的注解

以下是我直接从官网去扒的代码

@NoTitle    //没有标题栏
@Fullscreen   //全屏
@EActivity(R.layout.bookmarks)  //布局文件
public class BookmarksToClipboardActivity extends Activity {
 
 @ViewById(R.id.booklist)//绑定View
 ListView bookmarkList;

 @App 绑定application
 BookmarkApplication application;
 
 @RestService
 BookmarkClient restClient;
 
 @AnimationRes //绑定动画
 Animation fadeIn;
 
 @SystemService  //绑定系统服务
 ClipboardManager clipboardManager;
 
 @AfterViews
 void initBookmarkList() {
     adapter = new BookmarkAdapter(this);
     bookmarkList.setAdapter(adapter);
 }
 
 @Click({R.id.updateBookmarksButton1,  R.id.updateBookmarksButton2
    })//绑定OnClick事件
 void updateBookmarksClicked() {
     searchAsync(search.getText().toString(), application.getUserId());
 }
 
 @Background//子线程执行
 void searchAsync(String searchString, String userId) {
     Bookmarks bookmarks = restClient.getBookmarks(searchString,     userId);
     updateBookmarks(bookmarks);
 }
 
 @UiThread//主线程执行
 void updateBookmarks(Bookmarks bookmarks) {
 adapter.updateBookmarks(bookmarks);
 bookmarkList.startAnimation(fadeIn);
 }
 
 @ItemClick //ItemClick监听
 void bookmarkListItemClicked(Bookmark selectedBookmark) {
 clipboardManager.setText(selectedBookmark.getUrl());
 }

@EBean //声明一个普通的java类(不能是Android中的组件)并且只有一个无参构造方法或者带context的构造方法(在AA version2.7)

@EProvider //contentProvider

@EReceiver //BroadcastReceiver

@EIntentService //IntentService

@EService //Service

@EView //View的注解,需注意:使用的时候是 ClassName_

@EViewGroup //ViewGroup的注解

@AfterExtras//activity之间的参数传递完成后调用

@AfterInject //依赖注入完成后执行的方法

@AfterViews //View绑定后执行的方法

@Extras //参数传递调用

  balabala....实在太多太全面了,建议大家去github上自己去看...— —!
   https://github.com/excilys/androidannotations/wiki/AvailableAnnotations
}

AA实现的流程。

AA在使用时,无需增加其他的额外代码。。只增加必要的注解。因此它的逻辑全部都在AndroidAnnotationProcessor的process方法中。

1.注解处理的初始化操作:
  AndroidAnnotationProcessor.java

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);
      androidAnnotationsEnv = new InternalAndroidAnnotationsEnvironment(processingEnv);

      //定义新生成的java文件的后缀,默认的是在原类名的基础上添加下划线“_”
      ModelConstants.init(androidAnnotationsEnv);
      //日志处理
      LoggerContext loggerContext = LoggerContext.getInstance();
      loggerContext.setEnvironment(androidAnnotationsEnv);
      try {
        //CorePlugin中包含了所有的AA能够处理的注解类的处理逻辑,XXXHandler 
        //比如EActivityHandler
        AndroidAnnotationsPlugin corePlugin = new CorePlugin();

        ...

        List<AndroidAnnotationsPlugin> plugins = loadPlugins();
        plugins.add(0, corePlugin);
        //添加能够处理的注解到Env(可以理解为上下文)
        androidAnnotationsEnv.setPlugins(plugins);
        } catch (Exception e) {
            LOGGER.error("Can't load plugins", e);
        }
}
2.接下来就是正式的处理流程:
 @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    ...

    Set<? extends Element> rootElements = roundEnv.getRootElements();
    ...

    try {
        checkApiAndProcessorVersions();
        //the point
        processThrowing(annotations, roundEnv);

    } catch (Exception e) {
        ...
    }
    ...
    return true;
}
2.1 processThrowing
private void processThrowing(Set<? extends TypeElement> annotations, RoundEnvironment 
    roundEnv) throws Exception {
    ...

    //提取自身及父类注解
    AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
    //注解的校验(是否合法)
    AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
    //注解环境中放入合法的注解
    androidAnnotationsEnv.setValidatedElements(validatingHolder);
    try {
        //解析AndroidAnnotationManifest文件
        AndroidManifest androidManifest = extractAndroidManifest();
        //创建R文件对象
        IRClass rClass = findRClasses(androidManifest);
        //将R文件和AndroidManifest放到Context中去
        androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest);

    } catch (Exception e) {
        return;
    }
    //注解的校验
    //这里不会进行所有情况的校验,而是假设父类已经校验过了。
    AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);
    //注解的处理
    //processResult中包含了某一个类创建所需要的全部信息,名称,属性和方法,然后我们根据信息生成.java
    ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);

    generateSources(processResult);
}
2.2 extractAndroidManifest AndroidManifest的解析
第一步是判断该项目是否是一个libraryProject,如果不是则执行下面的方法

/**
 * 解析AndroidManifest文件
 *
 * @param androidManifestFile
 * @param libraryProject
 * @return
 * @throws AndroidManifestNotFoundException
 */
private AndroidManifest parse(File androidManifestFile, boolean libraryProject) throws AndroidManifestNotFoundException {
    DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();

    Document doc;
    try {
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        doc = docBuilder.parse(androidManifestFile);
    } catch (Exception e) {
        ...
    }

    Element documentElement = doc.getDocumentElement();
    documentElement.normalize();
    //拿到包名
    String applicationPackage = documentElement.getAttribute("package");

    ...
    //解析uses-sdk节点
    NodeList sdkNodes = documentElement.getElementsByTagName("uses-sdk");
    if (sdkNodes.getLength() > 0) {
        Node sdkNode = sdkNodes.item(0);
        minSdkVersion = extractAttributeIntValue(sdkNode, "android:minSdkVersion", -1);
        ...
    }
    //该项目是否是lib
    if (libraryProject) {
        return AndroidManifest.createLibraryManifest(applicationPackage, minSdkVersion, maxSdkVersion, targetSdkVersion);
    }
    //解析application节点
    NodeList applicationNodes = documentElement.getElementsByTagName("application");

    String applicationClassQualifiedName = null;
    boolean applicationDebuggableMode = false;

    if (applicationNodes.getLength() > 0) {
        Node applicationNode = applicationNodes.item(0);
        Node nameAttribute = applicationNode.getAttributes().getNamedItem("android:name");
        //前缀(包名),用来后面拼上activity或者service的名字
        //因为部分开发者在开发时,manifeist文件中定义activity或者service 习惯不写全路径而是以.开头,这种特殊情况需要处理
        applicationClassQualifiedName = manifestNameToValidQualifiedName(applicationPackage, nameAttribute);

        if (applicationClassQualifiedName == null) {
            if (nameAttribute != null) {
                LOGGER.warn("");
            }
        }

        Node debuggableAttribute = applicationNode.getAttributes().getNamedItem("android:debuggable");
        if (debuggableAttribute != null) {
            applicationDebuggableMode = debuggableAttribute.getNodeValue().equalsIgnoreCase("true");
        }
    }
    //解析四大组件的节点,并将每一种都组成一个list,用在后面创建AndroidManifest对象

    NodeList activityNodes = documentElement.getElementsByTagName("activity");
    List<String> activityQualifiedNames = extractComponentNames(applicationPackage, activityNodes);

    NodeList serviceNodes = documentElement.getElementsByTagName("service");
    List<String> serviceQualifiedNames = extractComponentNames(applicationPackage, serviceNodes);

    NodeList receiverNodes = documentElement.getElementsByTagName("receiver");
    List<String> receiverQualifiedNames = extractComponentNames(applicationPackage, receiverNodes);

    NodeList providerNodes = documentElement.getElementsByTagName("provider");
    List<String> providerQualifiedNames = extractComponentNames(applicationPackage, providerNodes);

    List<String> componentQualifiedNames = new ArrayList<>();
    componentQualifiedNames.addAll(activityQualifiedNames);
    componentQualifiedNames.addAll(serviceQualifiedNames);
    componentQualifiedNames.addAll(receiverQualifiedNames);
    componentQualifiedNames.addAll(providerQualifiedNames);

    ...

    return AndroidManifest.createManifest(applicationPackage, applicationClassQualifiedName, componentQualifiedNames, permissionQualifiedNames,
            minSdkVersion, maxSdkVersion, targetSdkVersion, applicationDebuggableMode);
}
2.3 processAnnotations 注解的解析处理
/**
 * ProcessResult的生成,用来在下面生成java类的代码
 * 
 * @param validatedModel
 * @return
 * @throws Exception
 */
public ProcessResult process(AnnotationElements validatedModel) throws Exception {
    ProcessHolder processHolder = new ProcessHolder(environment.getProcessingEnvironment());

    environment.setProcessHolder(processHolder);

    LOGGER.info("Processing root elements");

    /*
     * 循环生成类文件,包括内部类和内部类中的内部类..
     */
    while (generateElements(validatedModel, processHolder)) {
        // CHECKSTYLE:OFF
        ;
        // CHECKSTYLE:ON
    }

     //处理父类上的注解
    for (AnnotationHandler annotationHandler : environment.getDecoratingHandlers()) {
        String annotationName = annotationHandler.getTarget();


        Set<AnnotatedAndRootElements> ancestorAnnotatedElements = validatedModel
          .getAncestorAnnotatedElements(annotationName);

        for (AnnotatedAndRootElements elements : ancestorAnnotatedElements) {
            GeneratedClassHolder holder = processHolder
                    .getGeneratedClassHolder(elements.rootTypeElement);

            if (holder != null) {
                processThrowing(annotationHandler, elements.annotatedElement, holder);
            }
        }

        Set<? extends Element> rootAnnotatedElements = validatedModel.getRootAnnotatedElements(annotationName);

        for (Element annotatedElement : rootAnnotatedElements) {

            Element enclosingElement;
            if (annotatedElement instanceof TypeElement) {//类或接口
                enclosingElement = annotatedElement;
            } else {
                enclosingElement = annotatedElement.getEnclosingElement();

                if (enclosingElement instanceof ExecutableElement) { //方法
                    enclosingElement = enclosingElement.getEnclosingElement();
                }
            }


            if (!isAbstractClass(enclosingElement)) {
                GeneratedClassHolder holder = processHolder.getGeneratedClassHolder(enclosingElement);
                
                /*
                 * 其实是调用annotationHandler的各种实现类来执行
                 */
                if (holder != null) {
                    processThrowing(annotationHandler, annotatedElement, holder);
                }
            } else {
                LOGGER.trace("Skip element {} because enclosing element {} is abstract", annotatedElement, enclosingElement);
            }
        }

    }

    return new ProcessResult(//
            processHolder.codeModel(), //
            processHolder.getOriginatingElements());
}
2.4 processThrowing
private <T extends GeneratedClassHolder> void processThrowing(AnnotationHandler<T> 
      handler, Element element, T generatedClassHolder) throws ProcessingException {
    try {
        handler.process(element, generatedClassHolder);
    } catch (Exception e) {
        throw new ProcessingException(e, element);
    }
}
2.5 以EActivity为例,看下process的实现
@Override
public void process(Element element, EActivityHolder holder) {

    List<JFieldRef> fieldRefs = annotationHelper.extractAnnotationFieldRefs(element, IRClass.Res.LAYOUT, false);

    JFieldRef contentViewId = null;
    if (fieldRefs.size() == 1) {
        contentViewId = fieldRefs.get(0);
    }

    if (contentViewId != null) {
        //JBlock 是 CodeModel jar包中的一个类,CodeModel是生成 Java 代码的 Java 库

        //以下代码的功能纯属猜想,EActivityHolder生成onCreate方法,并在onCreate方法执行setContentView,
        //参数为contentViewId
        JBlock onCreateBody = holder.getOnCreate().body();
        JMethod setContentView = holder.getSetContentViewLayout();
        onCreateBody.invoke(setContentView).arg(contentViewId);
    }
}
2.6 以EActivity为例,看下EActivityHolder的实现
/**
 * 新生成的java文件中onCreate方法的创建过程
 */
private void setOnCreate() {
    onCreate = generatedClass.method(PUBLIC, getCodeModel().VOID, "onCreate");
    //Override
    onCreate.annotate(Override.class);
    AbstractJClass bundleClass = getClasses().BUNDLE;
    JVar onCreateSavedInstanceState = onCreate.param(bundleClass, "savedInstanceState");
    JBlock onCreateBody = onCreate.body();
    JVar previousNotifier = viewNotifierHelper.replacePreviousNotifier(onCreateBody);
    //执行init方法
    onCreateBody.invoke(getInit()).arg(onCreateSavedInstanceState);
    //super.oncreate
    onCreateBody.invoke(_super(), onCreate).arg(onCreateSavedInstanceState);
    viewNotifierHelper.resetPreviousNotifier(onCreateBody, previousNotifier);
}

/**
 *  onCreate方法中setContentView的实现
 */
private void setSetContentView() {
    getOnCreate();

    AbstractJClass layoutParamsClass = getClasses().VIEW_GROUP_LAYOUT_PARAMS;

    setContentViewLayout = setContentViewMethod(new AbstractJType[] { getCodeModel().INT }, new String[] { "layoutResID" });
    setContentViewMethod(new AbstractJType[] { getClasses().VIEW, layoutParamsClass }, new String[] { "view", "params" });
    setContentViewMethod(new AbstractJType[] { getClasses().VIEW }, new String[] { "view" });
}

我们反编译一下生成的APK,拿到MainActivity_文件,验证下结果

Paste_Image.png

总结下:AndroidAnnotation是一个非常不错的注解框架,大大减少了无用代码的编写,核心原理就是在编译阶段,编译器读取java文件的所有的注解,然后根据不同的注解来重新生成新的代码,将新的代码组合成新的java类(生成类),在运行的时候,我们实际操作的都是“生成类”。AA总共提供了130个注解,涉及到组件,View,Fragment,网络请求,各类监听,后台线程处理,数据库,JavaBean等方方面面。

凡事都具有两面性,AA的使用所带来的也不全是好处,下面来说一下AA的不足。

AndroidAnnotation的问题

  1. 使用成本:
    如第一部分所介绍的一样,使用AA时,我们需要在不同的配置文件中配置各种信息。

  2. 开发中的成本:
    开发过程中,尽管缺少了findViewById等无用代码的书写,但是又引入了新的问题,我们需要在manifest文件中手动修改activity,service等组件的类名。

     以上两项严格意义上来说不算缺陷,写上去凑个字数。→_→
    
  3. 使用AA所生成的代码造成dex文件中无用代码增加:
    AA的使用不是避免了无用代码的书写,而是将这部分工作交由代码来完成,也就是说是通过 “代码生成代码”,打个比方,要实现一个功能,如果不用注解,我们可能30行代码就可以实现,虽然其中可能有findViewById,各种Listener的声明等。但是这就是我们应用运行时所需要的代码,而使用注解的话,尽管我们只需要20行代码就可以实现,但是编译生成的新的类文件可能会包含有两倍甚至更多的代码。以下是测试的结果

源文件

Paste_Image.png

生成后的文件

Paste_Image.png

可以看到AA不仅生成了新的java类来继承原有的组件(activity),并且新的类中的代码远多于原始类。如果是大型的项目,使用AA时需要三思。

不管怎么说,AndroidAnnotation都是一款优秀的注解框架,但是市面上的注解框架也不止AA这个一款,下一章老衲将会给大家带来Android另一款框架ButterKnife的使用介绍。敬请期待

如有错误请指正,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容