Android -- APT手寫實現ARouter功能


1,隨着需求越來越多,項目也越來越大,實現項目的組件化便成為了迫切需要解決的技術點,隨着去年一個多月的重構,我們最后使用了cc來實現了項目的組件化,今天咋們先不來講cc,來和大家一起看看阿里的ARouter是怎么實現的。

2,對比傳統項目我們基本是把所有的業務邏輯放在app的module里面,如果同一個業務涉及到其它模塊業務的話就需要看其它模塊的業務邏輯,這樣對我們后期的迭代和維護有着比較高的成本,且當項目大的時候整個項目一起編譯的話需要很久的時間(我們項目重構前編譯一次要十多分鍾,重構之后只需要兩分多鍾),當我們實現了組件化,就會把項目按照業務模塊來拆分多個module,例如:登錄模塊、訂單模塊、消息模塊、歷史記錄模塊等等,這樣每個模塊負責人只用關心自己負責的模塊,再把自己這個模塊需要對外暴露的能力給暴露出去,如果在開發調試自己模塊的時候,使用組件化后可以直接編譯自己模塊的module,這樣可以縮短項目的編譯時間

 

 

   怎么實現一個module從可獨立運行的app到lib的自由切換呢?很簡單其實就是我們每個module下的build.gradle里面的

apply plugin: 'com.android.library' //lib
apply plugin: 'com.android.application'//app 

  我們來簡單的寫一下,先來創建一個項目,里面有一個默認的app的module ,正常情況下我們項目里面需要實現一個登陸功能,這時候我創建一個login的Android lib,在lib里面來實現我們基本的登錄界面和功能,那這時候因為到實現我們組件化的基本功能,每個模塊能夠獨立運行和調試,我們在開發的階段就需要把login這個Android lib轉換為一個可獨立運行的applicaiton ,其實很簡單 ,只需要在項目目錄下的gradle.properties定義一個變量LOGIN_IS_LIB,用來表示是否將login這個module當做lib來運行

   然后再在我們login的module下面的build.gradle 加一些邏輯判斷

//① 判斷是將次module編譯成lib還是application
if (LOGIN_IS_LIB.toBoolean()){
    apply plugin: 'com.android.library'
}else {
    apply plugin: 'com.android.application'
}

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    //② 添加清單文件判斷 
    sourceSets{
        main{
            if (LOGIN_IS_LIB.toBoolean()){
                manifest.srcFile 'src/main/lib_manifest/AndroidManifest.xml'
            }else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation project(path: ':arouter')
    implementation project(path: ':annotation')
    annotationProcessor project(path: ':process')
}

  很簡單  通過控制gradle.properties定義一個變量LOGIN_IS_LIB的值就可以來解決這個問題了 ,對了順便提一下,因為我們app是主module且依賴於login的module,所以我們在app的build.gradle文件也要添加一下判斷

 if (LOGIN_IS_LIB.toBoolean()){
        implementation project(path: ':login')
}

  好了,這就是我們簡單的實現組件化的一下部分,實現一個module在lib和application的切換,接下來進入到我們的正題

3,在我們傳統的代碼來實現頁面的跳轉基本上是使用startActivity來跳轉的,那么我們就會寫出一下代碼

Intent intent = new Intent() ;
intent.setClass(mContext ,aCls) ;
mContext.startActivity(intent);

  如果我們想對這跳轉頁面的功能稍微的封裝一下,那么我們就建立一個Arouter類,里面提供一個jumpActivity的方法,jumpActivity里面除了我們的cls參數,再添加一個bundle參數用來傳遞參數

package com.ysten.fuxi01;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;

import com.ysten.arouter.IRouter;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import dalvik.system.DexFile;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
public class Arouter {

    private static Arouter instance ;
    private  Context mContext;

    public static Arouter getInstance(){
        if (instance == null){
            synchronized (Arouter.class){
                instance = new Arouter() ;
            }

        }
        return instance ;
    }
    
    public void jumpActivity(Class cls){
        jumpActivity(cls,null);
    }

    public void jumpActivity(Class cls, Bundle bundle){
        Intent intent = new Intent() ;
        if (bundle != null){
            intent.putExtras(bundle);
        }
        intent.setClass(mContext ,cls) ;
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }
}

  上面的代碼很簡單 ,大家應該沒有什么疑問的,在多人開發同一個項目的時候,很多時候我們都是需要去打開別人的界面的,這時候我們需要知道別人的這個類的class名字,直接持有這個class$,從而造成了強依賴關系,提高了耦合度,這個時候用到阿里的Arouter的同學會知道,阿里的Arouter通過注解的方式,將每一個每一個Activity綁定一個path,從而對接人員只需要知道path,就可以跳轉到對應的Activity了

@Route(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ARouter.getInstance().build("/app/MainActivity1").navigation() ;
                //startActivity(new Intent(MainActivity.this, LoginActivity.class));
            }
        });
    }
}

  那么我們按照阿里的這種思路我們繼續來完善我們的代碼,這時候我們創建一個map,將我們的activity和我們的path進行綁定,然后每次跳轉的時候,只需將制定的path傳入就行

package com.ysten.fuxi01;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;

import java.util.Map;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
public class Arouter {

    private static Arouter instance ;
    private  Context mContext;
    private static Map<String,Class<? extends Activity>> activityMap ;

    public static Arouter getInstance(){
        if (instance == null){
            synchronized (Arouter.class){
                instance = new Arouter() ;
                activityMap = new ArrayMap<>();
            }

        }
        return instance ;
    }

    /**
     *  將activity壓入
     * @param activityName
     * @param cls
     */
    public void putActivity(String activityName ,Class cls){
        if (cls != null && !TextUtils.isEmpty(activityName)){
            activityMap.put(activityName,cls) ;
        }
    }

    /**
     * 通過之前定義的path就行啟動
     * @param activityName
     */
    public void jumpActivity(String activityName){
        jumpActivity(activityName,null);
    }

    public void jumpActivity(String activityName, Bundle bundle){
        Intent intent = new Intent() ;
        Class<? extends Activity> aCls = activityMap.get(activityName);
        if (aCls == null){
            Log.e("wangjitao" ," error -- > can not find activityName "+activityName);
            return;
        }
        if (bundle != null){
            intent.putExtras(bundle);
        }
        intent.setClass(mContext ,aCls) ;
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }
}

   這時候我們就只需要調用Arouter.getInstance().jumpActivity("/login/LoginActivity");來進行跳轉了  ,但是這樣的前提是我們在初始化的時候需要把我們的所有的activity給put到map集合中,即調用Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class); 我們先來謝謝這種代碼,先創建接口IRouter

package com.ysten.arouter;

import android.app.Activity;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
public interface IRouter {
    void putActivity() ;
}

  再創建我們的實現類ActivityUtils,在putActivity()方法中我們需要將本module下的所有Activity給壓入

package com.ysten.fuxi01;

import com.ysten.arouter.Arouter;
import com.ysten.arouter.IRouter;

public class ActivityUtils implements IRouter {
    @Override
    public void putActivity() {
        //現在手動的添加Activity堆棧管理
        Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class);
    }
}

  存在多個module的話  ,上面這和個類需要被復制粘貼多次,這樣就有點得不償失了,所以我們打算apt和注解技術,在編譯的時候掃描我們指定的注解,然后輸出成對應的文件,從而解決創建重復類和寫重復代碼的問題,簡單來說就是JVM會在編譯期就運行APT去掃描處理代碼中的注解然后輸出java文件,ok,知道了這個思路我們就開干,先創建一個annotation的java lib,主要作用就是放置我們的注解文件,我們之前看到阿里的ARouter是使用Route注解的,所以我們這里也創建一個Route注解,添加path參數,注解之前我講過了就不和大家解釋里面的東西了,是一個最基本的注解

package com.ysten.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path();
}

  ok,然后我們在創建一個名為process的java lib  ,這個lib主要是我們過濾注解,然后為每個module創建ActiivityUtils文件的地方,這是我們APT(Annotation Processing Tool)的核心內容,在process的module中我們先來引入幾個庫

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(path: ':annotation')
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    implementation 'com.squareup:javapoet:1.12.1'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}

sourceCompatibility = "7"
targetCompatibility = "7"

  然后自定義processor ,創建Process類,集成自AbstractProcessor,且添加注解@AutoService(Processor.class) 

@AutoService(Processor.class)
public class Process extends AbstractProcessor {
    Filer filer ; //

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer =  processingEnv.getFiler() ;
    }

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

    /**
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>() ;
        types.add(Route.class.getCanonicalName()) ;
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    // todo 創建ActivityUtils文件  
    return false ;
}
}

  因為我們要為每個module生成ActivituUtils類,所以要是用到文件創建這塊,需要一個Filer變量,然后我再來說一下這四個方法

init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具類Elements, Types 和 Filer
getSupportedAnnotationTypes:指定這個注解處理器是注冊給哪個注解的,這里說明是注解Route
getSupportedSourceVersion:指定使用的Java版本,通常這里返回SourceVersion.latestSupported() 默認寫法
process:可以在這里寫掃描、處理注解的代碼,生成Java文件(process中的代碼下面詳細說明)

  現在就到了最關鍵的點了,我們首先通過RoundEnvironment獲取到當前被@Route修飾的類,再取出Route的path和被修飾的Activity名稱

 Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Route.class);
        Map<String ,String> map = new HashMap<>() ;
        for (Element element :elementsAnnotatedWith ){
            // TypeElement
            // VariableElement
            TypeElement typeElement = (TypeElement)element ;
            //com.ysten.fuxi01.MainActivity
            String className = typeElement.getQualifiedName().toString();
            String pathName = typeElement.getAnnotation(Route.class).path();
            map.put(pathName,className+".class");
        }

  雖然里面的代碼比較陌生 ,但是都是一些api方法,按照這個寫就行,這時候 我們的map中就存放了關於我們以path內容為key、activity的class為value的數據了,現在就是創建ActivityUtils類,再將這行Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class);代碼寫入到文件中

 Writer writer = null ;
String className = "ActivityUtils"+System.currentTimeMillis();
JavaFileObject classFile = filer.createSourceFile("com.ysten.test." + className);

  這里我們通過filer.createSourceFile() 方法吧文件創建出來,前面是包名隨便寫,后面是className,就是我們的ActivityUtils,這里要注意因為存在多個module,都會創建ActivityUtils類且都在包com.ysten.test的包下,這里為了防止文件重復,我直接在后面加上了時間戳來區別,接下來就是我們寫入文件內容的東西了

writer = classFile.openWriter() ;
            writer.write("package com.ysten.test;\n" +
                    "\n" +
                    "import com.ysten.arouter.Arouter;\n" +
                    "import com.ysten.arouter.IRouter;\n" +
                    "\n" +
                    "public class "+className+" implements IRouter {\n" +
                    "    @Override\n" +
                    "    public void putActivity() {\n");

            Iterator<String> iterator = map.keySet().iterator();
            while (iterator.hasNext()){
                String activityKey = iterator.next() ;
                String cls = map.get(activityKey);
                writer.write("        Arouter.getInstance().putActivity(");
                writer.write("\""+activityKey+"\","+cls+");");
            }
            writer.write("\n}\n" +
                    "}");

  沒什么好說的,就是需要細心,我寫這個的時候寫錯了一個文件名,找了一個多小時的問題,所以這個大家要細心  ,都是一些api方法的調用,約定俗成的,我把整個文件放上來

package com.ysten.process;

import com.google.auto.service.AutoService;
import com.ysten.annotation.Route;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
@AutoService(Processor.class)
public class Process extends AbstractProcessor {
    Filer filer ; //

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer =  processingEnv.getFiler() ;
    }

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

    /**
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>() ;
        types.add(Route.class.getCanonicalName()) ;
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //生成文件代碼
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Route.class);
        Map<String ,String> map = new HashMap<>() ;
        for (Element element :elementsAnnotatedWith ){
            // TypeElement
            // VariableElement
            TypeElement typeElement = (TypeElement)element ;
            //com.ysten.fuxi01.MainActivity
            String className = typeElement.getQualifiedName().toString();
            String pathName = typeElement.getAnnotation(Route.class).path();
            map.put(pathName,className+".class");
        }
        if (map.size() == 0 ){
            return false;
        }

        //
        Writer writer = null ;
        //
        String className = "ActivityUtils"+System.currentTimeMillis();
        try {
            JavaFileObject classFile = filer.createSourceFile("com.ysten.test." + className);
            writer = classFile.openWriter() ;
            writer.write("package com.ysten.test;\n" +
                    "\n" +
                    "import com.ysten.arouter.Arouter;\n" +
                    "import com.ysten.arouter.IRouter;\n" +
                    "\n" +
                    "public class "+className+" implements IRouter {\n" +
                    "    @Override\n" +
                    "    public void putActivity() {\n");

            Iterator<String> iterator = map.keySet().iterator();
            while (iterator.hasNext()){
                String activityKey = iterator.next() ;
                String cls = map.get(activityKey);
                writer.write("        Arouter.getInstance().putActivity(");
                writer.write("\""+activityKey+"\","+cls+");");
            }
            writer.write("\n}\n" +
                    "}");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (writer != null){
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return false;
    }
}

  這時候我們可以編譯項目,可以在build/generated/ap_generated_sources/debug/out目錄下來看到我們生成的文件

 

 

   ok,現在我們在每一個module下都生成了ActivityUtils文件,接下來就是找到所有的ActivityUtils文件,調用它的putActivity方法,那么我們找到com.ysten.test下的所有文件,代碼如下 很簡單

public List<String> getAllActivityUtils(String packageName){
        List<String> list = new ArrayList<>() ;
        String path  ;
        try {
            path = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0).sourceDir;
            DexFile dexFile = null ;
            dexFile = new DexFile(path);
            Enumeration enumeration = dexFile.entries() ;
            while(enumeration.hasMoreElements()){
                String name = (String) enumeration.nextElement();
                if (name.contains(packageName)){
                    list.add(name) ;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list ;
    }

  找到這些文件之后就是需要生成對象,調用它的put方法了,我們第一反應就是反射,也很簡單,知道了類名  ,直接執行方法

 List<String> className =getAllActivityUtils("com.ysten.test");
        Log.d("wangjitao" ,"className "+className);
        for (String cls : className ) {
            try {
                Class<?> aClass =  Class.forName(cls);
                if (IRouter.class.isAssignableFrom(aClass)){
                    IRouter iRouter = (IRouter) aClass.newInstance();
                    iRouter.putActivity();
                }
            }catch (Exception e ){

            }

        }

  ok,到這里我們基本上把功能完成的差不多了,這里我們再在Arouter類里面添加初始化方法,在application的oncreate方法進行初始化,Arouter的完整代碼如下

package com.ysten.arouter;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;

import dalvik.system.DexFile;

/**
 * @author wangjitao on 2020/5/5
 * @desc:
 */
public class Arouter {

    private static Arouter instance ;
    private  Context mContext;
    private static Map<String,Class<? extends Activity>> activityMap ;

    public static Arouter getInstance(){
        if (instance == null){
            synchronized (Arouter.class){
                instance = new Arouter() ;
                activityMap = new ArrayMap<>();
            }

        }
        return instance ;
    }

    public void putActivity(String activityName ,Class cls){
        if (cls != null && !TextUtils.isEmpty(activityName)){
            activityMap.put(activityName,cls) ;
        }
    }

    public void init(Context context){
        mContext = context ;
        List<String> className =getAllActivityUtils("com.ysten.test");
        Log.d("wangjitao" ,"className "+className);
        for (String cls : className ) {
            try {
                Class<?> aClass =  Class.forName(cls);
                if (IRouter.class.isAssignableFrom(aClass)){
                    IRouter iRouter = (IRouter) aClass.newInstance();
                    iRouter.putActivity();
                }
            }catch (Exception e ){

            }

        }
    }

    public void jumpActivity(String activityName){
        jumpActivity(activityName,null);
    }

    public void jumpActivity(String activityName, Bundle bundle){
        Intent intent = new Intent() ;
        Class<? extends Activity> aCls = activityMap.get(activityName);
        if (aCls == null){
            Log.e("wangjitao" ," error -- > can not find activityName "+activityName);
            return;
        }
        if (bundle != null){
            intent.putExtras(bundle);
        }
        intent.setClass(mContext ,aCls) ;
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mContext.startActivity(intent);
    }

    public List<String> getAllActivityUtils(String packageName){
        List<String> list = new ArrayList<>() ;
        String path  ;
        try {
            path = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0).sourceDir;
            DexFile dexFile = null ;
            dexFile = new DexFile(path);
            Enumeration enumeration = dexFile.entries() ;
            while(enumeration.hasMoreElements()){
                String name = (String) enumeration.nextElement();
                if (name.contains(packageName)){
                    list.add(name) ;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list ;
    }

}

  MainApplication.java

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initARouter();
    }
    private void initARouter(){
        Arouter.getInstance().init(this);
}

  兩個Activity代碼如下

@Route(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Arouter.getInstance().jumpActivity("/login/LoginActivity");
            }
        });
    }
}

@Route(path = "/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {

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

  最后再看看效果

 

  ok,上面就是今天的所有內容,我們來總結總結知識點:

  ① Android項目的組件化實現,快速切換application或lib

  ②使用APT技術,編譯時自動生成文件

  ③獲取指定包名下所有的文件

  ④通過反射實現類的創建和方法的調用

   github項目鏈接

  最后,期待和大家再次相見

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM