Android運行時注入淺析與使用


背景

最近接觸新項目,項目中引入了Android Annotation(AA)依賴注入開源框架,代碼中大片的注解代碼,對於沒用過注解框架(或者說沒有如此大面積的使用)的我來說確實看得很費力,於是花時間研究了一下Android中的注解,當然了,這篇文章的目的並非講解AA的使用,而是主要講如何自定義注解以及用自定義的注解完成最常見的View Inject。

定義自己的注解

很簡單,new->Java Class->選擇Annotation,即完成了一個最基本的注解的定義。
new_annotation

public @interface TestAnnotation {

}

 

順帶提一下,任何一個注解其實都是隱式地實現了java.lang.annotaion.Annotaion接口的。對於一個完備的自定義注解,我們通常還需要兩個東西,一是注解的修飾,二是注解的參數。

注解的修飾

我們可以用注解的注解(又叫元注解)來修飾注解,常見的有兩個:Retention和Target。

  1. Retention接收一個RetentionPolicy枚舉作為value,用以確定注解的保持策略,其源碼如下:
    public enum RetentionPolicy {
        /**
         * Annotation is only available in the source code.
         */
        SOURCE,
        /**
         * Annotation is available in the source code and in the class file, but not
         * at runtime. This is the default policy.
         */
        CLASS,
        /**
         * Annotation is available in the source code, the class file and is
         * available at runtime.
         */
        RUNTIME
    }

     

    對於這三種類型,我的理解是這樣的:

    • SOURCE作為最弱的一種策略,被標注為RetentionPolicy.SOURCE的注解,只是作為寫碼時的一種提醒,告訴我們某個特殊的含義,就像Android Studio中關鍵字、類名或者方法名的顏色區分,編譯器並不會讀取它。比如@Override注解,只是為了表明該方法是從父類中重寫的,即便我們注釋掉該注解,也不影響程序的編譯和運行。
    • CLASS是缺省的保持策略,被標注為RetentionPolicy.CLASS的注解會影響到編譯過程,但不影響到運行時。比如@TargetApi,可以用他來屏蔽掉編譯時的非法版本API的錯誤,但是,如果你並沒有在該注解標注的代碼段內處理好新老版本的API使用問題的話,程序還是會在運行時發生異常。
    • RUNTIME作為權限最高的策略,其保持策略會持續到運行時,因此可以使用反射來在運行時檢測注解的存在與否,接下來的Demo中使用到的就是這種方式。
  2. Target接收一個ElementType枚舉類型的數組作為參數,用以表示該注解的適用對象。常見的有FIELD、TYPE、METHOD、PARAMETER,顧名思義,不再贅述。

注解的參數

注解可以定義為需要多個參數,只要在使用該注解時按類型將實參傳入對應的形參即可,如果形參定義時有默認值則可不傳該參數,否則必須傳。如下所示:

public @interface TestAnnotation {
    String param1();
    String param2() default "hello";
    int param3();
}

 

使用時:

@TestAnnotation(param1 = "world", param3 = 1)
void test() {
   
}

 

當然,如果你的注解只需要一個參數,你可以把參數方法名定義成缺省的value()方法,這樣在傳參時就可以省略掉"value=<yourValue>"前面的"value="。

View Inject實現

前面提到,既然注解可以維持到進行時,那我們是不是也可以像一些注解框架那樣實現自己的視圖注解,以緩解頻繁的findViewbyId呢?首先想到的是定義一個ViewInject注解,該注解用於在標記某View會被自動findViewbyId,當然啦,我們需要一個傳入一個id,所以我們的ViewInject類看起來會像這樣:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
    int value();
}

 

使用時會像這樣:

@ViewInject(R.id.tv_inject)
private TextView mTextView;

 

現在我們的注解定義和使用的完成了,我們該要解決如何完成我們的findViewById的動作的問題。這里我們開始用到一點反射的知識,首先我們通過反射獲取到當前類的Fields集合,然后遍歷集合,找出被標注為ViewInject注解的字段並拿出它們注解的value,也就是view在xml布局中的id,有了id我們就能為該字段獲取到它相應的view了。我們的代碼會像這樣:

Class cls = getClass();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
    if (!field.isAnnotationPresent(ViewInject.class))
        continue;
    ViewInject viewInject = field.getAnnotation(ViewInject.class);

    if (viewInject.value() <= 0)
        continue;

    field.setAccessible(true);
    try {
        field.set(this, findViewById(viewInject.value()));
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

 

為了增加重用性,減少重復代碼,我們很容易想到抽出一個BaseActivity,把這段代碼從子類的onCreate方法拿到父類BaseActivity的onCreate方法中,但是這樣問題就來了,如果進到父類的onCreate中時,子類還沒有setContentView怎么辦?沒有根布局咋findView啊,愁人。。咋辦呢,我們依葫蘆畫瓢,為Activity再定義一個注解並傳入layout id,這下只要我們在BaseActivity中先反射拿到layout id並setContentView,就再不用擔心沒有根布局了,於是我們的注入代碼進一步變成這樣:

Class cls = getClass();
// 整體布局注入
if (!cls.isAnnotationPresent(ActivityInject.class))
    return;
ActivityInject layoutInject = (ActivityInject) cls.getAnnotation(ActivityInject.class);
if (layoutInject.value() <= 0)
    return;
setContentView(layoutInject.value());

// View變量注入
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
    if (!field.isAnnotationPresent(ViewInject.class))
        continue;
    ViewInject viewInject = field.getAnnotation(ViewInject.class);

    if (viewInject.value() <= 0)
        continue;

    field.setAccessible(true);
    try {
        field.set(this, findViewById(viewInject.value()));
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

 

到這里,我們的View Inject其實已經完成了,剩下的就屬於進階~如何進階呢,前面我們用到的都是FIED和TYPE級的注解來簡化findview代碼,接下來我們要用ElementType.METHOD來簡化OnCreate中后續操作的執行和簡化setOnClickListener的操作。新增兩個注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterCreate {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

 

對應的在BaseActivity中新增注入的解析:

Method[] methods = cls.getDeclaredMethods();
ArrayList<Method> afterMethods = new ArrayList<>();
for (final Method method : methods) {
    // 暫存AfterCreate方法
    if (method.isAnnotationPresent(AfterCreate.class)) {
        afterMethods.add(method);
    }
    // 處理OnClick方法
    if (method.isAnnotationPresent(OnClick.class)) {
        OnClick onClick = method.getAnnotation(OnClick.class);

        View.OnClickListener onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Class[] paramClasses = method.getParameterTypes();
                    // 這里只處理了無參數和只有一個view作參數的情況,其他情況不invoke
                    if (paramClasses.length == 0) {
                        method.invoke(BaseActivity.this, null);
                    } else if (paramClasses.length == 1) {
                        if (paramClasses[0].equals(View.class)) {
                            method.invoke(BaseActivity.this, v);
                        }
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int id : onClick.value()) {
            if (id > 0)
                findViewById(id).setOnClickListener(onClickListener);
        }
    }
}

// 執行AfterCreate方法
for (Method method : afterMethods) {
    try {
        method.invoke(this, null);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

 

在子Activity中使用:

@AfterCreate
void init() {
    mTextView.setText("Set TextView Content");
    mImageView.setImageResource(R.mipmap.ic_launcher);
}

@OnClick({R.id.iv_inject,R.id.tv_inject})
void onClick(View view) {
    switch (view.getId()) {
        case R.id.iv_inject:
            Toast.makeText(getApplicationContext(), "ImageView is clicked!", Toast.LENGTH_SHORT).show();
            break;

        case R.id.tv_inject:
            Toast.makeText(getApplicationContext(), "TextView is clicked!", Toast.LENGTH_SHORT).show();
            break;
    }

}

 

到這里就基本結束了。完整的Demo從這里下載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM