1. 程式人生 > >APT案例之點選事件

APT案例之點選事件

目錄介紹

  • 01.建立專案步驟
    • 1.1 專案搭建
    • 1.2 專案功能
  • 02.自定義註解
  • 03.建立Processor
  • 04.compiler配置檔案
  • 05.編譯jar
  • 06.如何使用
  • 07.編譯生成程式碼
  • 08.部分原始碼說明
    • 8.1 Process類-process方法
    • 8.2 OnceProxyInfo代理類
    • 8.3 OnceMethod類

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議或者問題,萬事起於忽微,量變引起質變!

關於apt實踐與總結開源庫地址

https://github.com/yangchong211/YCApt

00.註解系列部落格彙總

0.1 註解基礎系列部落格

  • 01.Annotation註解詳細介紹
  • 02.Dagger2深入分析,待更新
  • 03.註解詳細介紹
    • 什麼是註解,註解分類有哪些?自定義註解分類?執行註解案例展示分析,以一個最簡單的案例理解註解……使用註解替代列舉,使用註解限定型別
  • 04.APT技術詳解
    • 什麼是apt?理解註解處理器的作用和用途……android-apt被替代?annotationProcessor和apt區別? 什麼是jack編譯方式?
  • 06.自定義annotation註解
    • @Retention的作用?@Target(ElementType.TYPE)的解釋,@Inherited註解可以被繼承嗎?Annotation裡面的方法為何不能是private?
  • 07.註解之相容kotlin
    • 後期更新
  • 08.註解之處理器類Processor
    • 處理器類Processor介紹,重要方法,Element的作用,修飾方法的註解和ExecutableElement,瞭解修飾屬性、類成員的註解和VariableElement……
  • 10.註解遇到問題和解決方案
    • 無法引入javax包下的類庫,成功執行一次,修改程式碼後再執行就報錯
  • 11.註解代替列舉
    • 在做記憶體優化時,推薦使用註解代替列舉,因為列舉佔用的記憶體更高,如何說明列舉佔用記憶體高呢?這是為什麼呢?
  • 12.註解練習案例開原始碼
    • 註解學習小案例,比較系統性學習註解並且應用實踐。簡單應用了執行期註解,通過註解實現了setContentView功能;簡單應用了編譯器註解,通過註解實現了防暴力點選的功能,同時支援設定時間間隔;使用註解替代列舉;使用註解一步步搭建簡單路由案例。結合相應的部落格,在來一些小案例,從此應該對註解有更加深入的理解……
  • 13 ARouter路由解析
    • 比較詳細地分析了阿里路由庫
  • 14 搭建路由條件
    • 為何需要路由?實現路由方式有哪些,這些方式各有何優缺點?使用註解實現路由需要具備的條件以及簡單原理分析……
  • 15 通過註解去實現路由跳轉
    • 自定義Router註解,Router註解裡有path和group,這便是仿照ARouter對路由進行分組。然後看看註解生成的程式碼,手寫路由跳轉程式碼。
  • 16 自定義路由Processor編譯器
    • Processor介紹,重要方法,Element的作用,修飾方法的註解和ExecutableElement
  • 17 利用apt生成路由對映檔案
    • 在Activity類上加上@Router註解之後,便可通過apt來生成對應的路由表,那麼究竟是如何生成的程式碼呢?
    • 在元件化開發中,有多個module,為何要在build.gradle配置moduleName,又是如何通過程式碼拿到module名稱?
    • process處理方法如何生成程式碼的,又是如何寫入具體的路徑,寫入檔案的?
    • 看完這篇文章,應該就能夠理解上面這些問題呢!
  • 18 路由框架的設計和初始化
    • 編譯期是在你的專案編譯的時候,這個時候還沒有開始打包,也就是你沒有生成apk呢!路由框架在這個時期根據註解去掃描所有檔案,然後生成路由對映檔案。這些檔案都會統一打包到apk裡,app執行時期做的東西也不少,但總而言之都是對對映資訊的處理,如執行執行路由跳轉等。那麼如何設計框架呢?
    • 生成的註解程式碼,又是如何把這些路由對映關係拿到手,或者說在什麼時候拿到手比較合適?為何註解需要進行初始化操作?
    • 如何得到得到路由表的類名,如何得到所有的routerAddress---activityClass對映關係?
  • 19 路由框架設計注意要點
    • 需要注意哪些要點?
  • 20 為何需要依賴注入
    • 有哪些注入的方式可以解耦,你能想到多少?路由框架為何需要依賴注入?路由為何用註解進行依賴注入,而不是用反射方式注入,或者通過構造方法注入,或者通過介面方式注入?
  • 21 Activity屬性注入
    • 在跳轉頁面時,如何傳遞intent引數,或者如何實現跳轉回調處理邏輯?

01.建立專案步驟

1.1 專案搭建

  • 首先建立一個Android專案。然後給我們的專案增加一個module,一定要記得是Java Library。因為APT需要用到jdk下的 【 *javax.~ *】包下的類,這在AndroidSdk中是沒有的。
  • 一定要注意:**需要說明的是:**我們的目的是寫一個Android庫,APT Moudle是java Library,不能使用Android API。所以還需要建立一個Android Library,負責框架主體部分. 然後由Android Library引用APT jar包。
  • 專案目錄結構如圖:
    • app:Demo
    • AptAnnotation:java Library主要放一些專案中需要用到的自定義註解及相關程式碼
    • AptApi:Android Library. OnceClick是我們真正對外發布並交由第三方使用的庫,它引用了apt-jar包
    • AptCompiler:java Library主要是應用apt技術處理註解,生成相關程式碼或者相關原始檔,是核心所在。

1.2 專案功能

  • 在一定時間內,按鈕點選事件只能執行一次。未到指定時間,不執行點選事件。

02.自定義註解

  • 建立Annotation Module,需要建立一個Java Library,名稱可為annotation,主要放一些專案中需要用到的自定義註解及相關程式碼
  • 新建一個類,OnceClick。就是我們自定義的註解。
    /**
     * <pre>
     *     @author 楊充
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/06/21
     *     desc  : 一定time時間內該點選事件只能執行一次
     *     revise:
     * </pre>
     */
    //@Retention用來修飾這是一個什麼型別的註解。這裡表示該註解是一個編譯時註解。
    @Retention(RetentionPolicy.CLASS)
    //@Target用來表示這個註解可以使用在哪些地方。
    // 比如:類、方法、屬性、介面等等。這裡ElementType.METHOD 表示這個註解可以用來修飾:方法
    @Target(ElementType.METHOD)
    //這裡的interface並不是說OnceClick是一個介面。就像申明類用關鍵字class。申明註解用的就是@interface。
    public @interface OnceClick {
        //返回值表示這個註解裡可以存放什麼型別值
        int value();
    }
    

03.建立Processor

  • 建立Compiler Module,需要再建立一個Java Library,名稱可為compiler,主要是應用apt技術處理註解,生成相關程式碼或者相關原始檔,是核心所在。
  • Processor是用來處理Annotation的類。繼承自AbstractProcessor。
    /**
     * <pre>
     *     @author 楊充
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/06/21
     *     desc  : 自定義Processor編譯器
     *     revise:
     * </pre>
     */
    @SupportedSourceVersion(SourceVersion.RELEASE_7)
    public class OnceClickProcessor extends AbstractProcessor {
    
        private Messager messager;
        private Elements elementUtils;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
            elementUtils = processingEnv.getElementUtils();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            //獲取proxyMap
            Map<String, OnceProxyInfo> proxyMap = getProxyMap(roundEnv);
            //遍歷proxyMap,並生成程式碼
            for (String key : proxyMap.keySet()) {
                OnceProxyInfo proxyInfo = proxyMap.get(key);
                writeCode(proxyInfo);
            }
            return true;
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new LinkedHashSet<>();
            types.add(OnceClick.class.getCanonicalName());
            return types;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return super.getSupportedSourceVersion();
        }
    }
    

04.compiler配置檔案

  • build.gradle檔案配置
    • auto-service的作用是向系統註冊processor(自定義註解處理器),執行編譯時使用processor進行處理。
    • javapoet提供了一套生成java程式碼的api,利用這些api處理註解,生成新的程式碼或原始檔。
    • OnceClickAnnotation是上文建立的註解module。
    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'com.google.auto.service:auto-service:1.0-rc3'
        implementation 'com.squareup:javapoet:1.10.0'
        implementation project(':OnceClickAnnotation')
    }
    
    sourceCompatibility = "7"
    targetCompatibility = "7"
    

05.編譯jar

  • 這裡有一個坑,主Module是不可以直接引用這個java Module的。(直接引用,可以成功執行一次~修改程式碼以後就不能運行了)而如何單獨編譯這個java Module呢?在編譯器Gradle視圖裡,找到Module apt下的build目錄下的Build按鈕。雙擊執行。
    • 程式碼沒有問題編譯通過的話,會有BUILD SUCCESS提示。生成的jar包在 apt 下的build目錄下的libs下。將apt.jar拷貝到app下的libs目錄,右鍵該jar,點選Add as Library,新增Library

06.如何使用

  • 程式碼如下所示
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //初始化OnceClick,並設定點選事件間隔是2秒
            OnceInit.once(this,2000);
        }
    
        @OnceClick(R.id.tv_1)
        public void Click1(){
            Log.d("tag--------------------","tv_1");
        }
    
        @OnceClick(R.id.tv_2)
        public void Click2(View v){
            Log.d("tag--------------------","tv_2");
        }
    }
    

07.編譯生成程式碼

  • 編譯之後生成的程式碼路徑,在專案中的build資料夾,如圖所示
    • image
  • 編譯之後生成的程式碼
    // 編譯生成的程式碼,不要修改
    // 更多內容:https://github.com/yangchong211
    package com.ycbjie.ycapt;
    
    import android.view.View;
    import com.ycbjie.api.Finder;
    import com.ycbjie.api.AbstractInjector;
    
    public class MainActivity$$_Once_Proxy<T extends MainActivity> implements AbstractInjector<T> {
    
        public long intervalTime; 
    
        @Override 
        public void setIntervalTime(long time) {
            intervalTime = time;
        } 
    
        @Override 
        public void inject(final Finder finder, final T target, Object source) {
            View view;
            view = finder.findViewById(source, 2131165325);
            if(view != null){
                view.setOnClickListener(new View.OnClickListener() {
                long time = 0L;
                @Override
                public void onClick(View v) {
                    long temp = System.currentTimeMillis();
                    if (temp - time >= intervalTime) {
                        time = temp;
                        target.Click1();
                    }
                }});
            }
            view = finder.findViewById(source, 2131165326);
            if(view != null){
                view.setOnClickListener(new View.OnClickListener() {
                long time = 0L;
                @Override
                public void onClick(View v) {
                    long temp = System.currentTimeMillis();
                    if (temp - time >= intervalTime) {
                        time = temp;
                        target.Click2(v);
                    }
                }});
            }
      }
    
    }
    

08.部分原始碼說明

8.1 Process類-process方法

  • 當某個類Activity使用了@OnceClick註解之後,我們就應該為其生成一個對應的代理類,代理類實現我們框架的功能:為某個View設定點選事件,並且這個點選事件一定時間內只能執行一次。所以,一個代理類可能有多個需要處理的View。
  • 先看process程式碼:
    • ProxyInfo物件:存放生成代理類的必要資訊,並生成程式碼。
    • getProxyMap方法:使用引數roundEnv,遍歷所有@OnceClick註解,並生成代理類ProxyInfo的Map。
    • writeCode方法:真正生成程式碼的方法。
    • 總結一下:編譯時,取得所有需要生成的代理類資訊。遍歷代理類集合,根據代理類資訊,生成程式碼。
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //獲取proxyMap
        Map<String, OnceProxyInfo> proxyMap = getProxyMap(roundEnv);
        //遍歷proxyMap,並生成程式碼
        for (String key : proxyMap.keySet()) {
            OnceProxyInfo proxyInfo = proxyMap.get(key);
            //寫入程式碼
            writeCode(proxyInfo);
        }
        return true;
    }
    

8.2 OnceProxyInfo代理類

  • 其實這個類,才是這個框架的重中之重,因為生成什麼程式碼,全靠這個類說了算。這個類也沒什麼好講的,就是用StringBuidler拼出一個類來。ProxyInfo儲存的是類資訊,方法資訊我們用List methods儲存。然後根據這些資訊生成類。
    public class OnceProxyInfo {
    
        private String packageName;
        private String targetClassName;
        private String proxyClassName;
        private TypeElement typeElement;
        private List<OnceMethod> methods;
        private static final String PROXY = "_Once_Proxy";
    
        OnceProxyInfo(String packageName, String className) {
            this.packageName = packageName;
            this.targetClassName = className;
            this.proxyClassName = className + "$$" + PROXY;
        }
    
        String getProxyClassFullName() {
            return packageName + "." + proxyClassName;
        }
    
        String generateJavaCode() throws OnceClickException {
    
            StringBuilder builder = new StringBuilder();
            builder.append("// 編譯生成的程式碼,不要修改\n");
            builder.append("// 更多內容:https://github.com/yangchong211\n");
            builder.append("package ").append(packageName).append(";\n\n");
    
            //寫入導包
            builder.append("import android.view.View;\n");
            builder.append("import com.ycbjie.api.Finder;\n");
            builder.append("import com.ycbjie.api.AbstractInjector;\n");
            builder.append('\n');
    
            builder.append("public class ").append(proxyClassName)
                    .append("<T extends ").append(getTargetClassName()).append(">")
                    .append(" implements AbstractInjector<T>").append(" {\n");
            builder.append('\n');
    
            generateInjectMethod(builder);
            builder.append('\n');
    
            builder.append("}\n");
            return builder.toString();
    
        }
    
        private String getTargetClassName() {
            return targetClassName.replace("$", ".");
        }
    
        private void generateInjectMethod(StringBuilder builder) throws OnceClickException {
            builder.append("    public long intervalTime; \n");
            builder.append('\n');
    
            builder.append("    @Override \n")
                    .append("    public void setIntervalTime(long time) {\n")
                    .append("        intervalTime = time;\n    } \n");
            builder.append('\n');
    
            builder.append("    @Override \n")
                    .append("    public void inject(final Finder finder, final T target, Object source) {\n");
            builder.append("        View view;");
            builder.append('\n');
    
            //這一步是遍歷所有的方法
            for (OnceMethod method : getMethods()) {
                builder.append("        view = ")
                        .append("finder.findViewById(source, ")
                        .append(method.getId())
                        .append(");\n");
                builder.append("        if(view != null){\n")
                        .append("            view.setOnClickListener(new View.OnClickListener() {\n")
                        .append("            long time = 0L;\n");
                builder.append("            @Override\n")
                        .append("            public void onClick(View v) {\n");
                builder.append("                long temp = System.currentTimeMillis();\n")
                        .append("                if (temp - time >= intervalTime) {\n" +
                                "                    time = temp;\n");
                if (method.getMethodParametersSize() == 1) {
                    if (method.getMethodParameters().get(0).equals("android.view.View")) {
                        builder.append("                    target.")
                                .append(method.getMethodName()).append("(v);");
                    } else {
                        throw new OnceClickException("Parameters must be android.view.View");
                    }
                } else if (method.getMethodParametersSize() == 0) {
                    builder.append("                    target.")
                            .append(method.getMethodName()).append("();");
                } else {
                    throw new OnceClickException("Does not support more than one parameter");
                }
                builder.append("\n                }\n")
                        .append("            }")
                        .append("});\n        }\n");
            }
    
            builder.append("  }\n");
        }
    
        TypeElement getTypeElement() {
            return typeElement;
        }
    
        void setTypeElement(TypeElement typeElement) {
            this.typeElement = typeElement;
        }
    
        List<OnceMethod> getMethods() {
            return methods == null ? new ArrayList<OnceMethod>() : methods;
        }
    
        void addMethod(OnceMethod onceMethod) {
            if (methods == null) {
                methods = new ArrayList<>();
            }
            methods.add(onceMethod);
        }
    }
    

8.3 OnceMethod類

  • 需要講的一點是,每一個使用了@OnceClick註解的Activity或View,都會為其生成一個代理類,而一個代理中有可能有很多個@OnceClick修飾的方法,所以我們專門為每個方法有建立了一個javaBean用於儲存方法資訊:
    public class OnceMethod {
    
        private int id;
        private String methodName;
        private List<String> methodParameters;
    
        OnceMethod(int id, String methodName, List<String> methodParameters) {
            this.id = id;
            this.methodName = methodName;
            this.methodParameters = methodParameters;
        }
    
        int getMethodParametersSize() {
            return methodParameters == null ? 0 : methodParameters.size();
        }
    
        int getId() {
            return id;
        }
    
        String getMethodName() {
            return methodName;
        }
    
        List<String> getMethodParameters() {
            return methodParameters;
        }
    
    }
    

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

關於apt實踐與總結開源庫地址

https://github.com/ya