Android 自定义gradle插件实现一个Android路由框架

851 阅读6分钟

一、Apt的使用实现一个Android路由框架

1.1、原理:

因为Android组件化的开发,导致了组件间的通信问题,根据路由框架的思想,维护一个路由表,该路由表将串联所有的组件模块。

将所要用的activity维护在表中

思想:分别有app模块(应用程序模块),业务模块分别为:A,B,C,D,E,五个模块:

在Android中要想B模块内容引用A模块内容,A必须依赖B,在这种情况下B要想使用A模块内容也要依赖A,但是gradle不支持相互依赖

这种情况下只能使用Android 隐式跳转

1.2、Android界面跳转:

1、显示跳转:Intent intent=new Intent(MainActivity.this, SecondActivity.class); startActivity(intent);

2、隐式跳转 :AndroidManifest注册action :Intent intent=new Intent("com.example.android.tst.SecondActivity"); startActivity(intent);(不够灵活,不能检查,跳转不唯一)

1.3、路由模块 base-arouter 路由表创建:

项目中的模块全部依赖路由模块,为了保证路由表的唯一性,使用单例

open class ARouterUtils {
​
    companion object {
        //双重校验所,单例
        val instance: ARouterUtils by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            ARouterUtils()
        }
    }
​
​
     val mRouterMap = mutableMapOf<String, Class<out Activity>>()
​
​
    fun register(path: String, activity: Class<out Activity>) {
        mRouterMap[path] = activity
    }
​
​
    fun startActivity(path: String, activity: Activity) {
        val cla = mRouterMap[path]
        cla?.let {
            val intent = Intent(activity, cla)
            activity.startActivity(intent)
        }
    }
}

1.4、因为应用模块都依赖于业务模块所以可以在应用模块中初始化路由表:

class MyApplication : Application() {
​
    override fun onCreate() {
        super.onCreate()
       ARouterUtils.instance.register("arouta", ArouterActivityA::class.java)
       ARouterUtils.instance.register("aroutb", ArouterActivityB::class.java)
    }
​
}

这种情况下就可以在业务模块a下面使用方法跳转到应用模块b:

ARouterUtils.instance.startActivity("aroutb",this)

1.5、这种也麻烦类一多需要注册的就多起来了,有时候忘记了就会出错:

为了优化可以将注册下层到业务模块,使用apt自动生成注册代码,这时候就是要看自己怎么设计了:

我的设计是实现一个注册的接口:

interface IArouteInterface {
​
    fun register(routers:MutableMap<String, Class<out Activity>>)
}

1.6、在各个模块下实现IArouteInterface接口 eg:B模块下:RouteInterfaceBImpl

class RouteInterfaceBImpl: IArouteInterface {
​
    override fun register(routers: MutableMap<String, Class<out Activity>>) {
        routers["mapKey"] = ArouterActivityB::class.java
    }
}

1.7、这种情况下app的module就可以写成,进行进行模块聚合

RouteInterfaceBImpl().register(ARouterUtils.instance.mRouterMap)

二、使用apt生成类似RouteInterfaceBImpl的注册代码:

使用apt就是简化RouteInterfaceBImpl类的生成:

2.1、创建注解类

@Target(AnnotationTarget.CLASS)
@kotlin.annotation.Retention(AnnotationRetention.BINARY)
annotation class AroutBind(val value: String)

2.2、注解使用标记activity:

@AroutBind("ArouterBindActivityB")
public open class ArouterBindActivityB  : AppCompatActivity() {
​
    private lateinit var etTestTem: EditText
    private lateinit var etTestAlert: EditText
    private lateinit var etTestNorm: EditText
    private lateinit var etTestDiff: EditText
    private lateinit var btCalculate: Button
    private lateinit var tvResult: TextView
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main_b)
        etTestTem = findViewById(R.id.et_1)
        etTestAlert = findViewById(R.id.et_2)
        etTestNorm = findViewById(R.id.et_3)
        etTestDiff = findViewById(R.id.et_4)
        btCalculate = findViewById(R.id.bt_count)
        tvResult = findViewById(R.id.tv_result)
​
        tvResult.text = "自定义注解跳转的布局"
//        com.example.myaptuselearing.routes.ArouterActivityARouteInterfaceImp().register(ARouterUtils.instance.mRouterMap)
    }
​
}

2.3、创建一个注解处理类AroutBindProcessor.class

class AroutBindProcessor : AbstractProcessor() {
    private lateinit var mFiler: Filer
    private var mMessager: Messager? = null
    private var mElementUtils: Elements? = null
    private val mActivityMap: HashMap<String, ViewBindParamBean> =
        HashMap<String, ViewBindParamBean>()
​
    @Synchronized
    override fun init(processingEnvironment: ProcessingEnvironment) {
        super.init(processingEnvironment)
​
        /**
         *  用来创建新源、类或辅助文件的 Filer。
         */
        mFiler = processingEnvironment.filer
        /**
         * 返回用来报告错误、警报和其他通知的 Messager。
         */
        mMessager = processingEnvironment.messager
        /**
         * 返回用来在元素上进行操作的某些实用工具方法的实现。<br>
         *
         * Elements是一个工具类,可以处理相关Element(包括ExecutableElement, PackageElement, TypeElement, TypeParameterElement, VariableElement)
         */
        mElementUtils = processingEnvironment.elementUtils
        print("注解处理init")
    }
​
    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
     */
    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.latestSupported()
    }
​
    /**
     * 指定这个注解处理器是注册给哪个注解的,这里说明是注解BindView
     */
    override fun getSupportedAnnotationTypes(): Set<String> {
        val supportTypes: HashSet<String> = LinkedHashSet()
        print("注解处理getSupportedAnnotationTypes")
        supportTypes.add(AroutBind::class.java.canonicalName)
        return supportTypes
    }
​
    override fun process(set: Set<TypeElement>, roundEnvironment: RoundEnvironment): Boolean {
        mActivityMap.clear()
        print("注解处理process")
        if (set != null && set.isNotEmpty()) {
            //通过getElementsAnnotatedWith得到所有注解elements
            val bindViewElements: Set<Element> = roundEnvironment.getElementsAnnotatedWith(
                AroutBind::class.java
            )
            var packName: String? = null
            var className: String? = null
            for (element in bindViewElements) {
                //1.获取包名
                val packageElement: PackageElement = mElementUtils!!.getPackageOf(element)
                packName = packageElement.qualifiedName.toString()
                print(String.format("package = %s", packName))
                print("注解数量" + bindViewElements.size)
                val aroutBind = element.getAnnotation(AroutBind::class.java)
​
                //2.注解所在的类的类名
                val className = element.simpleName.toString()
                print("注解类名$className")
​
​
                creatMethod(packName, className, element)
//                creatMethodPoet(packName, className, element)
            }
​
            return false
        }
        return false
    }
​
    /**
     * 1.字符串普通方式拼接
     */
    private fun creatMethod(packName: String, className: String, element: Element) {
        print("遍历map注解的类包名称与类名称:className=" + className + "packName=" + packName)
        try {
            val newClassName = "${className}RouteInterfaceImp"
            val jfo: JavaFileObject =
                mFiler.createSourceFile("$packName.$newClassName", *arrayOf<Element>())
            val writer: Writer = jfo.openWriter()
            writer.write("package com.example.myaptuselearing.routes;")
            writer.write("\n\n")
            writer.write("import com.example.base_arouter.IArouteInterface;")
            writer.write("\n\n")
            writer.write("import $packName.$className;")
            writer.write("\n\n")
            writer.write("import android.app.Activity;")
            writer.write("\n\n")
            writer.write("import java.util.Map;")
            writer.write("\n\n")
            writer.write("import androidx.annotation.NonNull;")
​
            writer.write("\n\n\n")
            writer.write("public class $newClassName implements IArouteInterface{")
            writer.write("\n\n")
            writer.write("public void register(@NonNull Map<String, Class<? extends Activity>> routers) {")
            //4.获取注解元数据
            val bindView: AroutBind =
                element.getAnnotation<AroutBind>(AroutBind::class.java)
            val id: String = bindView.value
            print("有多个arout绑定")
            //绑定注解
            writer.write("\n\n")
            writer.write("routers.put("$id",$className.class);")
​
            writer.write("\n")
            writer.write("  }")
            writer.write("\n\n")
            writer.write("}")
            writer.flush()
            writer.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
​
    }
​
    /**
     * javapoet 用法  参考:https://blog.csdn.net/qq_26376637/article/details/52374063
     */
    private fun creatMethodPoet(packName: String, className: String, element: Element) {
​
​
        val newClassName = "$className$$RouteInterfacedPoetImp"
        //获取继承的接口
        val iArouteInterface: ClassName =
            ClassName.get("com.example.base_arouter", "IArouteInterface")
​
​
        //拼接方法中的内容
        var content = ""
        //4.获取注解元数据
        val bindView: AroutBind =
            element.getAnnotation<AroutBind>(AroutBind::class.java)
        val id: String = bindView.value
        content = "routers.put("$id",$className.class)"
//        val parameterSpec = ParameterSpec.builder(
//            Map::class.java,
//            "routers", Modifier.FINAL
//        ).build() // 创建参数
//        val subArray: TypeName = ArrayTypeName.of(String::class.java)
        //Map<String, Class<? extends Activity>> routers
        val parameterSpec = ParameterizedTypeName.get(
            ClassName.get(Map::class.java),
            ClassName.get(String::class.java),
            ParameterizedTypeName.get(
                ClassName.get(Class::class.java),
                WildcardTypeName.subtypeOf(String::class.java)
            )
        )
        val main: MethodSpec = MethodSpec.methodBuilder("register")
            .addModifiers(Modifier.PUBLIC)
            .returns(Void::class.java)//Map<String, Class<? extends Activity>> routers
            .addParameter(parameterSpec, "routers")
            .addStatement(content)
            .build()
        val bind: TypeSpec = TypeSpec.classBuilder(newClassName)
            .addModifiers(Modifier.PUBLIC)
            .addMethod(main)
            .build()
        val javaFile: JavaFile = JavaFile.builder(packName, bind).build()
        try {
            // 生成文件
            javaFile.writeTo(processingEnv.filer)
        } catch (e: IOException) {
            e.printStackTrace()
        }
​
    }
​
    /**
     * 打印编译期间的日志
     */
    private fun print(msg: String) {
        mMessager?.printMessage(Diagnostic.Kind.NOTE, "arout注解日志:$msg")
    }
}

要想该注解能够使用,需要为这个类注册,按下图创建文件,文件内容为相关注解的路径

image.png 2.4、要想注解可以生成代码要在app的module下的build中依赖注解处理器所在的模块

implementation project(path: ':apt-compile')
kapt project(path: ':apt-compile')

三、如何统一调用(下面介绍两种方式-》1.反射,2-》自定义插件的方式):

因为注解生成了类该类名称我们做了特殊标记,一个是名字特别包名确定唯一继承一个接口 ,通过这三个条件可以创建一个实例对象进行调用

3.1、根据包名寻找所有的类(com.example.myaptuselearing.routes)//该包名是我们生成apt是写的包名称

可以根据Android加载机制解析dex文件找到在,通过反射创建实例调用方法,完成路由的注册

public class ClassFindHelper {
​
​
    //读取系统所有包名
    public static List<String> allPackage(Context context) {
        //获取PackageManager
        PackageManager packageManager = context.getPackageManager();
        //获取所有已安装程序的包信息
        List<PackageInfo> packageInfos = packageManager.getInstalledPackages(0);
        //用于存储所有已安装程序的包名
        // List<String> packageNames = new ArrayList<>();
        List<String> packageNames = new ArrayList<>();
​
        //从pinfo中将包名字逐一取出,压入pName list中
        if (packageInfos != null) {
            for (int i = 0; i < packageInfos.size(); i++) {
                String packName = packageInfos.get(i).packageName;
                Log.d("tgw TAG", "packageInfos:" + packName);
                packageNames.add(packName);
            }
        }
        return packageNames;
    }
​
​
    //给一个接口,返回这个接口的所有实现类
    public static List<Class> getAllClassByInterface(Context context, Class c, String packageName) {
        List<Class> returnClassList = new ArrayList<>(); //返回结果
        //如果不是一个接口,则不做处理
        if (c.isInterface()) {
            packageName = "com.example.myaptuselearing.routes"; //获得当前的包名
            try {
                List<Class> allClass = getClasses(context, packageName); //获得当前包下以及子包下的所有类
                //判断是否是同一个接口
                for (int i = 0; i < allClass.size(); i++) {
                    if (c.isAssignableFrom(allClass.get(i))) { //判断是不是一个接口
                        if (!c.equals(allClass.get(i))) { //本身不加进去
                            returnClassList.add(allClass.get(i));
                        }
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return returnClassList;
    }
​
    //从一个包中查找出所有的类,在jar包中不能查找
    private static List<Class> getClasses(Context context, String packageName) throws ClassNotFoundException, IOException {
        List<String> dexFileClassNames = getDexFileClassNames(context, packageName);
        ArrayList<Class> classes = new ArrayList<>();
        for (String s : dexFileClassNames) {
            try {
                Class scanClass = Class.forName(s);
                classes.add(scanClass);
            } catch (Exception e) {
                continue;
            }
        }
        return classes;
    }
​
    public static List<String> getDexFileClassNames(Context context, String packageName) throws IOException {
        DexFile df = new DexFile(context.getPackageCodePath());//通过DexFile查找当前的APK中可执行文件
        Enumeration<String> enumeration = df.entries();//获取df中的元素  这里包含了所有可执行的类名 该类名包含了包名+类名的方式
        List<String> classes = new ArrayList<>();
        while (enumeration.hasMoreElements()) {//遍历
            String className = enumeration.nextElement();
            if (className.startsWith(packageName)) {
                classes.add(className);
            }
        }
​
        return classes;
    }
​
​
    /**
     * 得到路由表的类名
     *
     * @param context
     * @param packageName
     * @return
     * @throws PackageManager.NameNotFoundException
     * @throws InterruptedException
     */
    public static Set<String> getFileNameByPackageName(Application context, final String packageName)
            throws PackageManager.NameNotFoundException, InterruptedException {
        final Set<String> classNames = new HashSet<>();
        List<String> paths = getSourcePaths(context);
        //使用同步计数器判断均处理完成
        final CountDownLatch countDownLatch = new CountDownLatch(paths.size());
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(paths.size(), paths.size(),
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
        for (final String path : paths) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexFile = null;
                    try {
                        //加载 apk中的dex 并遍历 获得所有包名为 {packageName} 的类
                        dexFile = new DexFile(path);
                        Enumeration<String> dexEntries = dexFile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) {
                                classNames.add(className);
                                Log.d("tgw 所有类", "run: " + className);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        if (null != dexFile) {
                            try {
                                dexFile.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        //释放一个
                        countDownLatch.countDown();
                    }
                }
            });
        }
        //等待执行完成
        countDownLatch.await();
        return classNames;
    }
​
​
    /**
     * 获得程序所有的apk(instant run会产生很多split apk)
     *
     * @param context
     * @return
     * @throws PackageManager.NameNotFoundException
     */
    private static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException {
        ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        List<String> sourcePaths = new ArrayList<>();
        sourcePaths.add(applicationInfo.sourceDir);
        //instant run
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (null != applicationInfo.splitSourceDirs) {
                sourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs));
            }
        }
        return sourcePaths;
    }
}

3.2、反射调用:

class MyApplication : Application() {
​
    override fun onCreate() {
        super.onCreate()
       
        val classList = ClassFindHelper.getAllClassByInterface(this, IArouteInterface::class.java,"com.example.myaptuselearing.routes")
        for (index in classList) {
            Log.d("tgw TAG", "onCreate: $index")
            init(index.name)
        }
    }
​
​
    //做了一个缓存,只有第一次bind时才通过反射创建对象
    fun init(className: String) {
        var iArouter: IArouteInterface? = null
        try {
            val aClass = Class.forName(className)
            iArouter = aClass.newInstance() as IArouteInterface?
        } catch (e: Exception) {
            e.printStackTrace()
        }
        iArouter?.register(ARouterUtils.instance.mRouterMap)
    }
​
}

四、Asm字节码插桩的使用:

看这篇文章:juejin.cn/post/707261…

五、Asm字节码插桩的使用:

通过编写代码然后生成class文件 ,利用jar包将class文件生成所需的asm文件代码:

//Asm的使用:https://www.jianshu.com/p/905be2a9a700
* 使用asm提供的通过 ASMifier 自动生成对应的 ASM 代码。首先需要在ASM官网 下载 asm-all.jar 库,我下载的是最新的 asm-all-5.2.jar,然后使用如下命令,即可生成
*  命令:
* java -classpath E:\googleDowmload\asm-all-5.1.jar org.objectweb.asm.util.ASMifier E:\MyLearing\MyAptUseLearing\base-arouter\build\tmp\kotlin-classes\debug\com\example\base_arouter\ARouterUtils.class
*
* jar包下载地址:
* http://nexus.neeveresearch.com/nexus/content/repositories/public/org/ow2/asm/asm-all/5.1/
AOP插桩点

class文件-> 字节码文件-> dex文件--> 打包-->apk

我们要想对字节码进行修改,只需要在javac之后,dex之前对class文件进行字节码扫描,并按照一定规则进行过滤及修改就可以了,这样修改过后的字节码就会在后续的dex打包环节被打到apk中,这就是我们的插桩入口。

具体做法

<1>字节码扫描,并按照一定规则进行过滤出哪些类的class文件需要进行字节码修改 <2>对筛选出来的类进行字节码修改操作 最后将修改过字节码的class文件,将连同资源文件,一起打入Apk中,得到最终可以在Android平台可以运行的APP。

从Java到android:类的加载机制

字节码插桩参考:

www.jianshu.com/p/4fff882ab…

www.jianshu.com/p/c20285305…

blog.csdn.net/liyang_nash…

www.jianshu.com/p/403f5a586…

blog.csdn.net/tubby_ting/…

\