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/yangchong211/YCApt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章