Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android ButterKnife 的實現思路

Android ButterKnife 的實現思路

編輯:關於Android編程

在Android開發中,我們為了方便初始化Activity中的各種View,我們可能會使用到Jake Wharton的 ButterKnife庫,這個庫是針對View、資源id等進行注解的開源庫,它能夠去除掉一些丑陋不堪的樣板式代碼,使得我們的代碼更加簡潔、易於維護,同時基於APT也使得它的效率得到保證。

(如果你想快速了解ButterKnife的實現思路,可以先閱讀 ExampleActivity$InjectAdapter類以及後續的結論,然後再回過頭來閱讀 )

下面我們來看看 ButterKnife 的簡單使用。
首先我們看在沒有使用ButterKnife時,我們初始化一個Activity中的各個控件的代碼:

public class ExampleActivity extends Activity {
    TextView title;
    ImageView icon;
    TextView footer;

    @Override 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        // 通過findViewById進行視圖查找,然後進行類型轉換
        title = (TextView) findViewById(R.id.title);
        icon = (ImageView) findViewById(R.id.icon);
        footer = (TextView) findViewById(R.id.footer);
    }
}

在ExampleActivity函數的onCreate函數中,我們通常會對各個子視圖進行初始化,這些代碼看起來重復性很高,而且丑陋不堪,幾乎都要對View進行強轉,當一個布局中含有十個以上的View時,再加上為某些View添加上事件處理等,這部分的代碼將占用很大的篇幅。ButterKnife就是為了簡化這些工作而出現的,讓開發人員專注在真正有用的代碼上。使用ButterKnife之後我們的代碼變成了這樣:

public class ExampleActivity extends Activity {
  @InjectView(R.id.title) TextView title;
  @InjectView(R.id.icon) ImageView icon;
  @InjectView(R.id.footer) TextView footer;

  @Override 
  public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.simple_activity);
     // 將Activity注入ButterKnife
     ButterKnife.inject(this);
  }
}

當運行完onCreate函數之後Activity中的幾個View就已經被初始化了。findViewById、強制轉換等樣板代碼被去除了,代碼變得更加簡單,使得我們可以更專注在代碼邏輯的編寫上,整個類型也更易於維護。

那麼ButterKnife的原理是什麼呢?@InjectView又是什麼?ButterKnife的inject函數又有什麼作用?

這是因為ButterKnife使用了一種叫做編譯時注解的技術(即APT),代碼在編譯時會掃描AbstractProcessor的所有子類,並且調用這些子類的process函數,在這個函數就會將所有的代碼元素傳遞進來。此時我們只需要在這個process函數中獲取所有添加了某個注解的元素,然後對這些元素進行操作,使之能夠滿足我們的需求,這樣我們就可以在編譯期對源代碼進行處理,例如生成新的類等。在運行時,我們通過一些接口對這些新生成的類進行調用以此完成我們的功能

說了這麼多還是太抽象了,還是以小民的例子來為大家一一解除疑問吧。

小民自從知道ButterKnife之後也被它的魅力所吸引了,於是決定研究個究竟,經過一番搜索得知ButterKnife是基於編譯時注解,然後通過APT生成輔助類,然後在運行時通過inject函數調用那些生成的輔助類來完成功能。小民決定自己寫一個只支持View 的id注入的簡版ButterKnife來深入學習,這個庫被命名為SimpleDagger。

首先小民建了一個注解類,代碼如下 : 
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewInjector {
    int value();
}

因為我們的這個注解只支持View的id注入,因此它的目標元素是字段,它只存在在class文件中,因為一旦過了編譯期我們就不再需要它了。關於注解方面的基礎知識我們不做過多講解,對這方面不了解的同學可以先閱讀相關書籍,例如《Java編程思想》、《Java核心技術》。

在添加AbstractProcessor 之前,為了使Eclipse支持 APT 需要一些配置,可以參考 injectdagger。Android Studio要支持 APT則需要添加APT插件,有興趣的同學可以自行搜索相關解決方案。

通過 APT 來生成輔助類型

添加這個注解之後,我們還需要在編譯期對這個注解進行處理。上文說到,編譯器會在編譯時檢測所有的AbstractProcessor並且調用它的process函數來讓開發人員對代碼元素進行處理。因此我們新建一個AbstractProcessor的子類,代碼如下 :

@SupportedAnnotationTypes("org.simple.injector.anno.*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ViewInjectorProcessor extends AbstractProcessor {

    //所有注解處理器的列表
    List mHandlers = new LinkedList();
    //類型與字段的關聯表,用於在寫入Java文件時按類型來寫不同的文件和字段
    final Map> map = new HashMap>();
    // 生成輔助累的Writer類
    AdapterWriter mWriter;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // 注冊注解處理器
        registerHandlers();
        // 初始化代碼生成器
        mWriter = new DefaultJavaFileWriter(processingEnv);
    }

    // 注冊處理器
    private void registerHandlers() {
        mHandlers.add(new ViewInjectHandler());
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        // 迭代所有的注解處理器,使得每個注解都有一個處理器,
        for (AnnotationHandler handler : mHandlers) {
            // 關聯ProcessingEnvironment
            handler.attachProcessingEnv(processingEnv);
            // 解析注解相關的信息
            map.putAll(handler.handleAnnotation(roundEnv));
        }
        // 將解析到的數據寫入到具體的類型中
        mWriter.generate(map);
        return true;
    }
    // 代碼省略
}

在ViewInjectorProcessor類的上面我們看到如下注解@SupportedAnnotationTypes(“org.simple.injector.anno.*”), 這個注解表明這個類只支持org.simple.injector.anno路徑下的注解,我們的ViewInjector注解就是在這個包下。在該類的init函數中我們注冊了一個注解處理器,也就是ViewInjectHandler類,該類實現了AnnotationHandler接口,該接口的聲明如下 :

// 注解處理接口
public interface AnnotationHandler {
    // 關聯ProcessingEnvironment
    void attachProcessingEnv(ProcessingEnvironment processingEnv);
    // 處理注解,將結果存儲到Map中
    Map> handleAnnotation(RoundEnvironment env);
}

該接口聲明了兩個函數,一個是關聯ProcessingEnvironment,另一個是handleAnnotation函數,負責處理標識了ViewInjector注解的元素。小民的設計思路是定義一個AnnotationHandler接口,每個實現類處理一種類型的注解,例如ViewInjectHandler只處理ViewInject注解。下面我們看看ViewInjectHandler的核心代碼 :

public class ViewInjectHandler implements AnnotationHandler {
    ProcessingEnvironment mProcessingEnv;

    @Override
    public void attachProcessingEnv(ProcessingEnvironment processingEnv) {
        mProcessingEnv = processingEnv;
    }

    @Override
    public Map> handleAnnotation(RoundEnvironment roundEnv) {
        Map> annotationMap = new HashMap>();
        // 1、獲取使用ViewInjector注解的所有元素
        Set elementSet = roundEnv.getElementsAnnotatedWith(ViewInjector.class);
        for (Element element : elementSet) {
            // 2、獲取被注解的字段
            VariableElement varElement = (VariableElement) element;
            // 3、獲取字段所在類型的完整路徑名,比如一個TextView所在的Activity的完整路徑,也就是變量的宿主類
            String className = getParentClassName(varElement);
            // 4、獲取這個宿主類型的所有元素,例如某個Activity中的所有注解對象
            List cacheElements = annotationMap.get(className);
            if (cacheElements == null) {
                cacheElements = new LinkedList();
            }
            // 將元素添加到該類型對應的字段列表中
            cacheElements.add(varElement);
            // 以宿主類的路徑為key,所有字段列表為value,存入map.
            // 這裡是將所在字段按所屬的類型進行分類
            annotationMap.put(className, cacheElements);
        }

        return annotationMap;
    }
    // 代碼省略
}

在handleAnnotation函數中小民獲取了所有被ViewInject注解標識了的VariableElement元素,然後將這些元素按照宿主類進行分類存到一個map中,key就是宿主類的完整類路徑,value就是這個宿主類中的所有被標識了ViewInject的VariableElement元素列表。例如將上述ExampleActivity的示例替換成小民的SimpleDagger,使用ViewInject注解標識中三個View,代碼如下 :

package com.simple.apt;
public class ExampleActivity extends Activity {
  @ViewInject (R.id.title) TextView title;
  @ViewInject (R.id.icon) ImageView icon;
  @ViewInject (R.id.footer) TextView footer;

  @Override 
  public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.simple_activity);
     // 其他代碼暫時省略
     SimpleDagger.inject(this);
  }
}

那麼此時ExampleActivity的完整路徑為com.simple.apt.ExampleActivity,這個完整路徑我們可以通過VariableElement元素獲取到,這些VariableElement就是代表了ExampleActiivty中的title、icon、footer三個對象。因此通過ViewInjectHandler的handleAnnotation處理之後我們的map中就含有了以com.simple.apt.ExampleActivity為key,以title、icon、footer三個成員變量對應的VariableElement列表為value的數據。

此時執行到process函數的最後一步,這裡調用了AdapterWriter來生成輔助類,這個輔助類要生成的代碼素材就是我們上述的VariableElement元素列表,調用的是AdapterWriter的generate函數,在AdapterWriter之下我們還建立了一個AbsWriter來封裝一些通用邏輯,AbsWriter核心代碼如下 :

public abstract class AbsWriter implements AdapterWriter {

    ProcessingEnvironment mProcessingEnv;
    Filer mFiler;
    // 代碼省略

    @Override
    public void generate(Map> typeMap) {
        Iterator>> iterator = typeMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry> entry = iterator.next();
            List cacheElements = entry.getValue();
            if (cacheElements == null || cacheElements.size() == 0) {
                continue;
            }

            // 取第一個元素來構造注入信息
            InjectorInfo info = createInjectorInfo(cacheElements.get(0));
            Writer writer = null;
            JavaFileObject javaFileObject;
            try {
                // 1、創建源文件,也就是生成輔助類
                javaFileObject = mFiler.createSourceFile(info.getClassFullPath());
                writer = javaFileObject.openWriter();
                // 2、寫入package, import, class以及findViews函數等代碼段
                generateImport(writer, info);
                // 3、寫入該類中的所有字段到findViews方法中
                for (VariableElement variableElement : entry.getValue()) {
                    writeField(writer, variableElement, info);
                }
                // 4、寫入findViews函數的大括號以及類的大括號
                writeEnd(writer);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                IOUtil.closeQuitly(writer);
            }

        }
    }
    // 代碼省略
}

在AbsWriter的generate函數中,我們定義了一個生成輔助類的邏輯骨架,分別為獲取宿主類型的所有元素,並且通過第一個元素獲取宿主類所在的包以及構建輔助類的類名等,然後創建一個新的java類,最後分別寫入import、所有被注解的元素等信息寫入到輔助類當中,所有生成的輔助類都是InjectAdapter的子類。實現代碼如下的功能在DefaultJavaFileWriter類中,核心代碼如下 :

public class DefaultJavaFileWriter extends AbsWriter {
  // 代碼省略
    // 寫入import以及類前面的類型聲明
    @Override
    protected void generateImport(Writer writer, InjectorInfo info)
            throws IOException {
        writer.write("package " + info.packageName + " ;");
        writer.write("\n\n");
        writer.write("import org.simple.injector.adapter.InjectAdapter ;");
        writer.write("\n");
        writer.write("import org.simple.injector.util.ViewFinder;");

        writer.write("\n\n\n");
        writer.write("/* This class is generated by Simple ViewInjector, please don't modify! */ ");
        writer.write("\n");
        writer.write("public class " + info.newClassName
                + " implements InjectAdapter<" + info.classlName + "> { ");
        writer.write("\n");
        writer.write("\n");
        // 查找方法
        writer.write("  public void injects(" + info.classlName
                + " target)  { ");
        writer.write("\n");
    }

    // 寫入一個結尾的大括號
    @Override
    protected void writeEnd(Writer writer) throws IOException {
        writer.write("  }");
        writer.write("\n\n");
        writer.write(" } ");
    }

    // 寫入字段
    @Override
    protected void writeField(Writer writer, VariableElement element, InjectorInfo info)
            throws IOException {
        ViewInjector injector = element.getAnnotation(ViewInjector.class);
        String fieldName = element.getSimpleName().toString();
        writer.write("      target." + fieldName + " =  ViewFinder.findViewById(target, "
                + injector.value() + "  ) ; ");
        writer.write("\n");
    }
}

在DefaultJavaFileWriter中分別寫入了輔助類的各個部分,最終的是寫入字段的部分,也就是writeField函數。在該函數中,小民獲取了這個字段的名字,並且寫下了一行如下一行代碼 :

target.fieldName = ViewFinder.findViewBydId(target, ViewInject注解的值); 

其實這就是一個初始化某個View的語句,這個target在這個例子中就是ExampleActivity,這個ViewInject注解的值就是View的id,我們知道每個含有id的View最終都會在R類中生成一個整型的數值,這裡的view id就是這個整型數值。需要注意的是這些被添加注解的字段都必須是非私有的,否則不能通過target.fieldName的形式直接訪問。這些初始化代碼都被寫到了InjectAdapter子類的inject函數中,inject函數傳遞一個target參數,這個target就是元素所在的類,比如ExampleActivity,而生成的輔助類的名稱格式為宿主類+”InjectAdapter”,例如ExampleActivityInjectAdapter,它與ExampleActivity在同一個包中,因此可以訪問到ExampleActivity的protected、package權限的字段。

InjectAdapter 接口

InjectAdapter的聲明如下 :

public interface InjectAdapter {
    void injects(T target);
}

ExampleActivity$InjectAdapter 類

這相當於我們為每個元素都生成一行初始化代碼來替換手動在ExampleActiivty中進行findViewById,當我們在ExampleAcivity的onCreate函數中調用SimpleDagger的inject函數時,會將ExampleActivity傳遞到InjectAdapter中,因此最後為ExampleActivity生成的輔助類就成為了如下這樣 :

public class ExampleActivity$InjectAdapter 
            implements InjectAdapter { 

  public void injects(ExampleActivity target)  { 
      target.title = ViewFinder.findViewById(target, 2131099648  ) ; 
      target.icon = ViewFinder.findViewById(target, 2131099649  ) ; 
      target.footer=ViewFinder.findViewById(target, 2131099332  ) ; 
  }
}

當調用SimpleDagger的inject時就會先通過傳遞進來的類名構建一個InjectAdapter子類的類名,例如傳遞進來的是ExampleActivity,那麼此時的輔助類的類名為 ExampleActivity$InjectAdapter,它InjectAdapter的子類。拿到完整類名之後再反射構建一個對象,然後轉換為InjectAdapter,最後調用inject函數。而這個生成的ExampleActivity$InjectAdapter的inject函數中又對每個View進行了findViewBydId,也就是對它們進行了初始化。至此,這些View字段就被自動初始化了!

我們最後再來捋一捋這個過程,大致分為如下幾步 :

通過ViewInject注解標識一些View成員變量; 通過ViewInjecyProcessor捕獲添加了ViewInject注解的元素,並且按照宿主類進行分類; 為每個含有ViewInject注解的宿主類生成一個InjectAdapter輔助類,並且在它的inject函數中生成初始化View的代碼; 在SimpleDagger的inject函數中構建生成的輔助類,此時內部會它這個InjectAdapter輔助類的inject函數,這個函數中又會初始化宿主類中的View成員變量,至此,View就已經被初始化了。

SimpleDagger的完整代碼在這裡,有興趣的同學可以下載下來進行學習以及擴展。

需要注意的是在eclipse中使用APT需要添加JRE庫的引用,在Android Studio則需要引用APT的插件。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved