Android開發(fā)— APT之ButterKnife的簡(jiǎn)單功能實(shí)現(xiàn)

假設(shè)已經(jīng)看完上一篇關(guān)于APT知識(shí)介紹了, 本文介紹下如何實(shí)現(xiàn)最簡(jiǎn)單的Butterknife的功能,主要分為以下幾個(gè)步驟漫萄。

Step1:

新建Java module創(chuàng)建注解:

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

Step2:

再創(chuàng)建一個(gè)Java module編寫注解處理器焙格,引入需要的庫(kù)并引入注解module:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.8.0'
    implementation project(':annotationLib')
}

sourceCompatibility = "7"
targetCompatibility = "7"

編寫注解處理器去處理注解:

@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {

    //輸出日志信息
    private Messager messager;
    //文件寫入
    private Filer filer;
    //獲取類型工具類
    private Types types;
    //模塊名稱
    private String moduleName = null;
    //Element的工具
    private Elements elementUtils;
    // View所在包名
    private static final String ViewClassName = "android.view.View";
    private static final String ActivityClassName = "android.app.Activity";

    /**
     * 初始化各種變量
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        elementUtils = processingEnvironment.getElementUtils();
        //獲取通過(guò)Gradle文件給處理器傳遞的參數(shù)
        moduleName = processingEnvironment.getOptions().get("route_module_name");

        types = processingEnvironment.getTypeUtils();
        LoggerInfo("moduleName = " + moduleName);
    }

    /**
     * 設(shè)置支持的注解類型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new HashSet<String>();
        annotataions.add(BindView.class.getCanonicalName());
        return annotataions;
    }

    /**
     * 日志輸出信息
     *
     * @param msg
     */
    public void LoggerInfo(String msg) {
        messager.printMessage(Diagnostic.Kind.NOTE, ">> " + msg);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 注解處理的核心方法
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (roundEnvironment.processingOver()) {
            return false;
        }
        LoggerInfo("process start");
        // 獲取所有被 @BindView 注解的對(duì)象
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        // 把所有被注解的成員變量根據(jù)類搜集起來(lái)
        Map<TypeElement, Set<Element>> routesMap = new HashMap<>();
        // 存放注解成員變量
        Set<Element> bindViews = null;

        try {
            if (bindViewElements != null && bindViewElements.size() > 0) {
                //遍歷每個(gè) @BindView注解的元素
                for (Element element : bindViewElements) {
                    //獲取注解元素所在的類對(duì)象
                    TypeElement typeElement = (TypeElement) element.getEnclosingElement();
                    Set<Element> elements = routesMap.get(typeElement);
                    if (elements != null) {
                        elements.add(element);
                    } else {
                        bindViews = new HashSet<>();
                        bindViews.add(element);
                        routesMap.put(typeElement, bindViews);
                    }
                }
                //根據(jù)類,生成每個(gè)類對(duì)應(yīng)的文件
                for (Map.Entry<TypeElement, Set<Element>> entry : routesMap.entrySet()) {
                    writeFile(entry.getKey(), entry.getValue());
                }
            }
        } catch (Exception e) {
            LoggerInfo(e.getMessage());
        }
        return true;
    }

    /**
     * 根據(jù)注解元素及所在類的信息,生成文件并寫入
     *
     * @param typeElement
     * @param routes
     */
    public void writeFile(TypeElement typeElement, Set<Element> routes) {
        //Activity的類型
        TypeMirror activityMirror = elementUtils.getTypeElement(ActivityClassName).asType();
        TypeMirror viewMirror = elementUtils.getTypeElement(ViewClassName).asType();

        //獲取所在類文件的類型
        TypeMirror targetMirror = elementUtils.getTypeElement(typeElement.getQualifiedName()).asType();

        //需要的View參數(shù)
        ParameterSpec sourceView = ParameterSpec.builder(TypeName.get(viewMirror), "sourceView")
                .build();
        //需要的Target參數(shù)
        ParameterSpec target = ParameterSpec.builder(TypeName.get(targetMirror), "target")
                .build();

        LoggerInfo("start inject");

        //是否是Activity
        boolean isActivity = types.isSubtype(typeElement.asType(), activityMirror);

        // 構(gòu)建構(gòu)造方法健田,處理Activity
        MethodSpec injectForActivity = null;
        if (isActivity) {
            injectForActivity = createConstructorForActivity(target);
        }
        // 構(gòu)建構(gòu)造方法,處理指定的View
        MethodSpec injectView = createConstructorForView(target, sourceView, routes);
        //創(chuàng)建類
        TypeSpec.Builder bindBuilder = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "$BindView")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(injectView);
        //target是Activity類型的生成對(duì)應(yīng)的bind方法
        if (isActivity) {
            bindBuilder.addMethod(injectForActivity);
        }
        TypeSpec bindHelper = bindBuilder.build();
        JavaFile javaFile = JavaFile.builder("com.wzh.annotation", bindHelper)
                .build();

        //寫入文件
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            LoggerInfo(e.toString());
            e.printStackTrace();
        }
    }

    /**
     * 真正處理邏輯
     *
     * @param target
     * @param sourceView
     * @param routes
     * @return
     */
    private MethodSpec createConstructorForView(ParameterSpec target, ParameterSpec sourceView, Set<Element> routes) {
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(target)
                .addParameter(sourceView);
        // 在方法里插入代碼
        CodeBlock.Builder codeBlock = CodeBlock.builder();
        codeBlock.addStatement("if(target == null) { return; }");
        codeBlock.addStatement("if(sourceView == null) { return; }");
        methodBuilder.addCode(codeBlock.build());
        for (Element element : routes) {
            //變量名
            String fieldName = element.getSimpleName().toString();
            //變量類型
            String fieldType = element.asType().toString();
            //控件ID
            int resId = element.getAnnotation(BindView.class).value();
            methodBuilder.addStatement("target.$L = ($N)sourceView.findViewById($L)", fieldName, fieldType, resId);
        }
        return methodBuilder.build();
    }

    /**
     * 處理Activity注入佛纫,省去傳遞View的操作
     *
     * @param target
     * @return
     */
    private MethodSpec createConstructorForActivity(ParameterSpec target) {
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addStatement("this(target, target.getWindow().getDecorView())")
                .addParameter(target);
        return methodBuilder.build();
    }
}

主要流程就是掃描到被注解的View, 然后分不同文件去存儲(chǔ)妓局,每個(gè)文件生成一個(gè)輔助類,完成賦值操作呈宇。
生成文件如下:

public final class MainActivity$BindView {
  public MainActivity$BindView(MainActivity target, View sourceView) {
    if(target == null) { return; };
    if(sourceView == null) { return; };
    target.mButton = (android.widget.Button)sourceView.findViewById(2131230800);
    target.mTextView = (android.widget.TextView)sourceView.findViewById(2131230904);
    target.mImageView = (android.widget.ImageView)sourceView.findViewById(2131230806);
  }

  public MainActivity$BindView(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }
}
public final class MainFragment$BindView {
  public MainFragment$BindView(MainFragment target, View sourceView) {
    if(target == null) { return; };
    if(sourceView == null) { return; };
    target.fragmentText = (android.widget.TextView)sourceView.findViewById(2131230795);
  }
}

這里區(qū)分Activity和Fragment好爬,Actvitiy不用傳遞View就可以,F(xiàn)ragment需要傳View對(duì)象甥啄。

Step3:

API調(diào)用賦值輔助類:

public class BindHelper {
    public static void bind(Activity context){
        String canonicalName = context.getClass().getCanonicalName();
        try {
            Class<?> clz = context.getClassLoader().loadClass(canonicalName+"$BindView");
            Constructor constructor = clz.getConstructor(context.getClass());
            constructor.newInstance(context);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    /**
     * 注入View
     * @param context
     * @param view
     */
    public static void bind(Object context, View view){
        String canonicalName = context.getClass().getCanonicalName();
        try {
            Class<?> clz = context.getClass().getClassLoader().loadClass(canonicalName+"$BindView");
            Constructor constructor = clz.getConstructor(context.getClass(),View.class);
            constructor.newInstance(context, view);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

這里直接利用反射調(diào)用相關(guān)生成輔助文件的構(gòu)造方法完成賦值操作存炮。

Step4

實(shí)際使用

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.hello)
    protected Button mButton;

    @BindView(R.id.title)
    protected TextView mTextView;

    @BindView(R.id.image)
    protected ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindHelper.bind(this);

        inflateView();
        addFragment();
    }

    private void inflateView() {
        mImageView.setImageResource(R.drawable.icon_test);
        mTextView.setText("我是通過(guò)@BindView(R.id.title)找到的");
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"我是通過(guò)@BindView找到的", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void addFragment() {
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.fragmentContainer,new MainFragment())
                .commit();
    }
}
public class MainFragment extends Fragment {
    @BindView(R.id.fragmentText)
    TextView fragmentText;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        BindHelper.bind(this,view);
        fragmentText.setText("我是Fragment里面添加注解獲取的");
    }
}

運(yùn)行起來(lái),沒啥問(wèn)題型豁,可以直接用我們的view對(duì)象僵蛛。


運(yùn)行圖

這里只是演示了Butterknife的基本原理,其中還有很多細(xì)節(jié)需要更進(jìn)一步處理迎变,如:生成文件命名要能區(qū)分module充尉、加載的時(shí)候最好緩存起來(lái)避免每次加載等等。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衣形,一起剝皮案震驚了整個(gè)濱河市驼侠,隨后出現(xiàn)的幾起案子姿鸿,更是在濱河造成了極大的恐慌,老刑警劉巖倒源,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苛预,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡笋熬,警方通過(guò)查閱死者的電腦和手機(jī)热某,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)胳螟,“玉大人昔馋,你說(shuō)我怎么就攤上這事√撬剩” “怎么了秘遏?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嘉竟。 經(jīng)常有香客問(wèn)我邦危,道長(zhǎng),這世上最難降的妖魔是什么舍扰? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任倦蚪,我火速辦了婚禮,結(jié)果婚禮上妥粟,老公的妹妹穿的比我還像新娘审丘。我一直安慰自己吏够,他們只是感情好勾给,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锅知,像睡著了一般播急。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上售睹,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天桩警,我揣著相機(jī)與錄音,去河邊找鬼昌妹。 笑死捶枢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的飞崖。 我是一名探鬼主播烂叔,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼固歪!你這毒婦竟也來(lái)了蒜鸡?” 一聲冷哼從身側(cè)響起胯努,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逢防,沒想到半個(gè)月后叶沛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忘朝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年灰署,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片局嘁。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氓侧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出导狡,到底是詐尸還是另有隱情约巷,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布旱捧,位于F島的核電站独郎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏枚赡。R本人自食惡果不足惜氓癌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贫橙。 院中可真熱鬧贪婉,春花似錦、人聲如沸卢肃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)莫湘。三九已至尤蒿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幅垮,已是汗流浹背腰池。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留忙芒,地道東北人示弓。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像呵萨,于是被迫代替她去往敵國(guó)和親奏属。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容