Java注解蔽氨、反射藐唠,安卓IOC(二)

在項目開發(fā)中,大部分 Android 開發(fā)者都使用過 ButterKnife 這個通過注解簡化代碼的框架鹉究。部分 Android 開發(fā)者也使用過 xUtils 這種快速開發(fā)的框架宇立。
兩者均使用到了注解。在上一篇 Java注解自赔、反射妈嘹,安卓IOC(一) 中我們知道了注解及反射的用法。本篇會分別介紹下其實現機制绍妨。

運行時注解

首先我們自己簡單實現類似 xUtils 這種運行時注解框架润脸。

綁定 View 控件

創(chuàng)建注解:


@Retention(RetentionPolicy.RUNTIME)//運行時注解
@Target(ElementType.FIELD)//Target為屬性
public @interface FindView {
    int value() default -1;
}

View解析代碼:


public class ViewInject {

    public static void bind(Activity activity) {
        inject(new ViewFinder(activity), activity);
    }

    public static void bind(View view) {
        inject(new ViewFinder(view), view);
    }

    public static void bind(View view, Object obj) {
        inject(new ViewFinder(view), obj);
    }

    private static void inject(ViewFinder finder, Object obj) {
        injectFields(finder, obj);
        injectMethods(finder, obj);
    }

    private static void injectFields(ViewFinder finder, Object obj) {
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();//獲取所有變量
        for (Field field : fields) {
            if (field.isAnnotationPresent(FindView.class)) {
                FindView findView = field.getAnnotation(FindView.class);//獲取注解
                if (findView.value() < 0) {
                    throw new IllegalArgumentException("The id can't be -1.");
                } else {
                    View view = finder.findViewById(findView.value());
                    try {
                        field.setAccessible(true);//破壞封裝
                        field.set(obj, view); //設置屬性
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

主要流程為通過反射獲取并遍歷所有變量,若變量被注解修飾痘绎,則將注解的 ID 賦值給指定方法并調用津函。

綁定 OnClick 事件

創(chuàng)建注解:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //Target為方法
public @interface OnClick {
    int[] id();
}

OnClick 事件注入:


private static void injectMethods(ViewFinder finder, final Object obj) {
    Method[] methods = obj.getClass().getDeclaredMethods();
    for (final Method method : methods) {
        if (method.isAnnotationPresent(OnClick.class)) {
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick.id().length != 0) {
                for (int i : onClick.id()) {
                    View view = finder.findViewById(i);
                    method.setAccessible(true);
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.invoke(obj, v);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }
}

主要流程為通過反射獲取并遍歷所有方法,若方法被注解修飾孤页,遍歷所有的 ID 尔苦,將注解的 ID 賦值給 findViewById 方法,然后在 setOnClickListener 調用 method 方法行施。

在 Activity 中的使用:


public class IocActivity extends AppCompatActivity {

    @FindView(R.id.txt_ioc_test)
    private TextView mTxtTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ioc);
        ViewInject.bind(this);
        mTxtTest.setText("測試");
    }

    @OnClick(id = {R.id.btn_ioc_test, R.id.btn_ioc_test2})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_ioc_test:
                Toast.makeText(this, "Click1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_ioc_test2:
                Toast.makeText(this, "Click2", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

以上為運行時注解的簡單實現允坚,即 xUtils 使用的注解方法。但是這種方法因為通過一系列反射獲取屬性蛾号、方法等稠项,對性能會有所影響,所以不建議在實際項目中使用鲜结,下邊介紹下輕量級的編譯時注解展运。

編譯時注解

ButterKnife 源碼解析網上已經有很多不錯的文章了,例如這篇 ButterKnife源碼分析 講的就很好精刷。這里主要介紹下自己的大致實現以及編譯時注解在 Android Studio 中的使用拗胜。

首先介紹下大概的項目結構,如下圖所示:

APT結構.png
  • annotation module: Java library - 定義一系列注解怒允。
  • injetc module: Android library - 定義注解的接口及調用方法埂软。
  • compiler module: Java library - 自定義編譯時注解 AbstractProcessor 在編譯期間生成 java 代碼。
  • app: 使用方法纫事。

本篇文章主要為介紹及學習勘畔,所以此處僅實現 setContentView 的編譯時注解所灸。

1、聲明注解


@Retention(RetentionPolicy.CLASS) //編譯時注解
@Target(ElementType.TYPE) //修飾類
public @interface ContentView {
    int value();
}

2炫七、聲明外界接口及方法

聲明接口:


public interface ContentInjector<T> {
    void injectContent(T obj, Activity activity); //此處僅用 Activity 參數即可實現文章的 demo
}

提供方法:


public class ContentViewInject {
    public static void bind(Activity activity) {//綁定
        injectContentView(activity);
    }
    private static void injectContentView(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        try {
            ContentInjector injector = (ContentInjector) Class.forName(clazz.getName()
                    + "$$ViewBinder").newInstance();
            injector.injectContent(activity, activity);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

以上代碼為簡單使用爬立,butterknife 中的 UnBinder 解綁,使用 Map 緩存等暫不考慮诉字。
此處的 Class.forName("") 以及 class.newInstance() 會對性能略有影響懦尝,butterknife 在此進行了 map 緩存優(yōu)化知纷。

3壤圃、自定義 AbstractProcessor,此處需將 module 設置為 Java library 才可繼承 AbstractProcessor琅轧。


@AutoService(Processor.class)
@SupportedSourceVersion(value = SourceVersion.RELEASE_7)
public class ContentViewInjectProcessor extends AbstractProcessor {


    //可用 @SupportedAnnotationTypes("com.lauzy.ContentView") 注解 ContentViewInjectProcessor 代替
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(ContentView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        String packageName;
        String className;
        //遍歷每個被 ContentView 修飾的 class 文件
        for (Element element : roundEnvironment.getElementsAnnotatedWith(ContentView.class)) {
            if (element.getKind() == ElementKind.CLASS) {
                TypeElement typeElement = (TypeElement) element;
                PackageElement packageEle = (PackageElement) element.getEnclosingElement();
                packageName = packageEle.getQualifiedName().toString();//獲取包名
                //因為是 ElementKind.CLASS 類型伍绳,所以可以直接強制轉換,獲取類名
                className = typeElement.getSimpleName().toString();

                int layoutId = typeElement.getAnnotation(ContentView.class).value();//獲取注解的 id
                
                //拼接 Java 類的字符串
                StringBuilder builder = new StringBuilder();
                builder.append("package ").append(packageName).append(";\n");
                builder.append("import android.view.View;\n");
                builder.append("import android.app.Activity;\n");
                builder.append("import com.freedom.lauzy.inject.ContentInjector;\n");
                builder.append('\n');

                builder.append("public class ").append(className + "$$ViewBinder");
                builder.append("<T extends ").append(className).append(">");
                builder.append(" implements ContentInjector<T>");
                builder.append(" {\n");
                builder.append("@Override\n")
                        .append("public void injectContent(final T source, Activity activity) {\n");
                builder.append("    ((Activity) source).setContentView(" + layoutId);
                builder.append(");\n");
                builder.append("}\n\n}\n");


                //寫入 Java 文件
                try {
                    JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(
                            packageName + "." + className + "$$ViewBinder",
                            typeElement);
                    Writer writer = fileObject.openWriter();
                    writer.write(builder.toString());
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("error");
                }
            }
        }
        return true;
    }
}

butterknife 中使用了 javapoet 生成 Java 代碼文件

此 module 的 gradle 配置如下:


apply plugin: 'java'

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.google.auto.service:auto-service:1.0-rc2' // google 的生成源代碼庫
    compile project(':annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

4乍桂、app 使用

app 的 gradle 配置如下:


dependencies {
    ...
    annotationProcessor project(':compiler')
    compile project(':annotation')
    compile project(':inject')
}

activity 中使用:


@ContentView(R.layout.activity_ioc)
public class IocActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ContentViewInject.bind(this);
    }
}

這樣冲杀,整個流程就結束了。此時 build 整個項目則會在 app/build/generated/source/apt/debug/com.lauzy.freedom.lauzycode/IOC 文件夾下生成一個IocActivity$$ViewBinder 的 Java 文件睹酌,代碼如下:


package com.lauzy.freedom.lauzycode.IOC;
import android.app.Activity;
import com.freedom.lauzy.inject.ContentInjector;

public class IocActivity$$ViewBinder<T extends IocActivity> implements ContentInjector<T> {
@Override
public void injectContent(final T source, Activity activity) {
    ((Activity) source).setContentView(2130968606);
}
}

可以看到权谁,其實是在編譯時生成了一個 Java 文件,并在 activity 的 onCreate 方法中調用了 setContentView 方法憋沿。

注意事項:

1旺芽、若不使用 com.google.auto.service:auto-service:1.0-rc2 這個 google 的生成源代碼庫,則需要手動創(chuàng)建一個 META_INF 文件來指定注解辐啄。
需要在 compiler 中創(chuàng)建 compiler/src/main/resources/META-INF/services 目錄(注意 META-INF 中間不是下劃線采章,減號即可),并新建 txt 文件 javax.annotation.processing.Processor壶辜,
文件中寫入自定義 AbstractProcessor 的全名稱悯舟,如: com.lauzy.ContentViewInjectProcessor 。多個的話換行寫入砸民。

如下圖所示:


META_INF

2抵怎、本文使用 annotationProcessor 的注解處理器代替 android-apt ,Google 內置的注解處理器岭参,建議使用反惕。

分析

編譯時注解的優(yōu)點 :在于對性能影響很小的情況下,大量簡化程序員的代碼冗荸,像 butterknife 在首次查找類的時候對性能稍有影響承璃,其他情況下影響微乎其微。
編譯時注解的缺點 :build 過程生成更多的代碼蚌本,增加了類和方法的數量盔粹;對性能影響很小隘梨,但是多少會有的。

本人認為編譯時注解在優(yōu)化代碼舷嗡,提高效率方面是有很大優(yōu)勢的轴猎,遠遠大于其缺點。

本篇文章主要是分析及梳理大致的實現方式进萄,僅為學習使用捻脖,所有代碼均托管在 我的Github 上。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末中鼠,一起剝皮案震驚了整個濱河市可婶,隨后出現的幾起案子,更是在濱河造成了極大的恐慌援雇,老刑警劉巖矛渴,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異惫搏,居然都是意外死亡具温,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門筐赔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铣猩,“玉大人,你說我怎么就攤上這事茴丰〈锩螅” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵较沪,是天一觀的道長鳞绕。 經常有香客問我,道長尸曼,這世上最難降的妖魔是什么们何? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮控轿,結果婚禮上冤竹,老公的妹妹穿的比我還像新娘。我一直安慰自己茬射,他們只是感情好鹦蠕,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著在抛,像睡著了一般钟病。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天肠阱,我揣著相機與錄音票唆,去河邊找鬼。 笑死屹徘,一個胖子當著我的面吹牛走趋,可吹牛的內容都是我干的。 我是一名探鬼主播噪伊,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼簿煌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鉴吹?” 一聲冷哼從身側響起姨伟,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拙寡,沒想到半個月后授滓,有當地人在樹林里發(fā)現了一具尸體琳水,經...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡肆糕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了在孝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诚啃。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖私沮,靈堂內的尸體忽然破棺而出始赎,到底是詐尸還是另有隱情,我是刑警寧澤仔燕,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布造垛,位于F島的核電站,受9級特大地震影響晰搀,放射性物質發(fā)生泄漏五辽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一外恕、第九天 我趴在偏房一處隱蔽的房頂上張望杆逗。 院中可真熱鬧,春花似錦鳞疲、人聲如沸罪郊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悔橄。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間癣疟,已是汗流浹背尺铣。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留争舞,地道東北人凛忿。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像竞川,于是被迫代替她去往敵國和親店溢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內容

  • 什么是注解注解分類注解作用分類 元注解 Java內置注解 自定義注解自定義注解實現及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,071評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,846評論 25 707
  • 一委乌、什么是注解床牧? 注解對于開發(fā)人員來講既熟悉又陌生,熟悉是因為只要你是做開發(fā)遭贸,都會用到注解(常見的@Overrid...
    _Justin閱讀 1,351評論 0 10
  • 讀這本書時沒有按照書的順序讀戈咳,而是撿著目錄中感興趣的章節(jié)讀的。這里我想說明一下壕吹,我把書分為兩類著蛙,一類是有趣的,一類...
    七月下雨天閱讀 204評論 0 0
  • 在春風清曲的水流中 千年回首彈指過 如同去年撒下的花籽 已然在跟我訴說它和陽光耳贬,雨露踏堡,土地的故事 我最悔的不是不曾...
    齊魯師范學院王浩宇閱讀 272評論 1 3