APT案例之點選事件
阿新 • • 發佈:2019-02-27
目錄介紹
- 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資料夾,如圖所示
- 編譯之後生成的程式碼
// 編譯生成的程式碼,不要修改 // 更多內容: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.關於我的部落格
- 我的個人站點:www.yczbj.org,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 簡書:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
- 開源中國:https://my.oschina.net/zbj1618/blog
- 泡在網上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿里雲部落格:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e