首页> 标签> ARouter
"ARouter"
共 109 条结果
全部 问答 文章 公开课 课程 电子书 技术圈 体验
Android 组件化指南
一、背景随着项目逐渐扩展,业务功能越来越多,代码量越来越多,开发人员数量也越来越多。此过程中,你是否有过以下烦恼?项目模块多且复杂,编译一次要5分钟甚至10分钟?太慢不能忍?改了一行代码 或只调了一点UI,就要run整个项目,再忍受一次10分钟?合代码经常发生冲突?很烦?被人偷偷改了自己模块的代码?很不爽?做一个需求,发现还要去改动很多别人模块的代码?别的模块已实现的类似功能,自己要用只能去复制一份代码再改改?“这个不是我负责的,我不管”,代码责任范围不明确?只做了一个模块的功能,但改动点很多,所以要完整回归测试?做了个需求,但不知不觉导致其他模块出现bug?如果有这些烦恼,说明你的项目需要进行 组件化 了。二、组件化的理解2.1 模块化在介绍组件化之前,先说说模块化。我们知道在Android Studio中,新建工程默认有一个App module,然后还可以通过File->New->New Module 新建module。那么这里的“module” 实际和我们说的“模块”基本是一个概念了。也就是说,原本一个 App模块 承载了所有的功能,而模块化就是拆分成多个模块放在不同的Module里面,每个功能的代码都在自己所属的 module 中添加。以京东为例,大致可以分为 “首页”、“分类”、“发现”、“购物车”、“我的”、“商品详情” 六个模块。 项目结构如下: 这是一般项目都会采用的结构。另外通常还会有一个通用基础模块module_common,提供BaseActivity/BaseFragment、图片加载、网络请求等基础能力,然后每个业务模块都会依赖这个基础模块。 那么业务模块之间有没有依赖呢?很显然是有的。例如 “首页”、“分类”、“发现”、“购物车”、“我的”,都是需要跳转到“商品详情” 的,必然是依赖“商品详情” ;而“商品详情”是需要能添加到“购物车”能力的;而“首页”点击搜索显然是“分类”中的搜索功能。 所以这些模块之间存在复杂的依赖关系。模块化 在各个业务功能比较独立的情况下是比较合理的,但多个模块中肯定会有页面跳转、数据传递、方法调用 等情况,所以必然存在以上这种依赖关系,即模块间有着高耦合度。 高耦合度 加上 代码量大,就极易出现上面提到的那些问题了,严重影响了团队的开发效率及质量。为了 解决模块间的高耦合度问题,就要进行组件化了。2.2 组件化介绍 — 优势及架构组件化,去除模块间的耦合,使得每个业务模块可以独立当做App存在,对于其他模块没有直接的依赖关系。 此时业务模块就成为了业务组件。而除了业务组件,还有抽离出来的业务基础组件,是提供给业务组件使用,但不是独立的业务,例如分享组件、广告组件;还有基础组件,即单独的基础功能,与业务无关,例如 图片加载、网络请求等。这些后面会详细说明。组件化带来的好处 就显而易见了:加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。提高协作效率:解耦 使得组件之间 彼此互不打扰,组件内部代码相关性极高。 团队中每个人有自己的责任组件,不会影响其他组件;降低团队成员熟悉项目的成本,只需熟悉责任组件即可;对测试来说,只需重点测试改动的组件,而不是全盘回归测试。功能重用:组件 类似我们引用的第三方库,只需维护好每个组件,一建引用集成即可。业务组件可上可下,灵活多变;而基础组件,为新业务随时集成提供了基础,减少重复开发和维护工作量。下图是我们期望的组件化架构:组件依赖关系是上层依赖下层,修改频率是上层高于下层。基础组件是通用基础能力,修改频率极低,作为SDK可共公司所有项目集成使用。common组件,作为支撑业务组件、业务基础组件的基础(BaseActivity/BaseFragment等基础能力),同时依赖所有的基础组件,提供多数业务组件需要的基本功能,并且统一了基础组件的版本号。所以 业务组件、业务基础组件 所需的基础能力只需要依赖common组件即可获得。业务组件、业务基础组件,都依赖common组件。但业务组件之间不存在依赖关系,业务基础组件之间不存在依赖关系。而 业务组件 是依赖所需的业务基础组件的,例如几乎所有业务组件都会依赖广告组件 来展示Banner广告、弹窗广告等。最上层则是主工程,即所谓的“壳工程”,主要是集成所有的业务组件、提供Application唯一实现、gradle、manifest配置,整合成完备的App。2.3 组件化开发的问题点我们了解了组件化的概念、优点及架构特点,那么要想实施组件化,首先要搞清楚 要解决问题点有哪些?核心问题是 业务组件去耦合。那么存在哪些耦合的情况呢?前面有提到过,页面跳转、方法调用、事件通知。 而基础组件、业务基础组件,不存在耦合的问题,所以只需要抽离封装成库即可。 所以针对业务组件有以下问题:业务组件,如何实现单独运行调试?业务组件间 没有依赖,如何实现页面的跳转?业务组件间 没有依赖,如何实现组件间通信/方法调用?业务组件间 没有依赖,如何获取fragment实例?业务组件不能反向依赖壳工程,如何获取Application实例、如何获取Application onCreate()回调(用于任务初始化)?下面就来看看如何解决这些问题。三、组件独立调试每个 业务组件 都是一个完整的整体,可以当做独立的App,需要满足单独运行及调试的要求,这样可以提升编译速度提高效率。如何做到组件独立调试呢?有两种方案:单工程方案,组件以module形式存在,动态配置组件的工程类型;多工程方案,业务组件以library module形式存在于独立的工程,且只有这一个library module。3.1 单工程方案3.1.1 动态配置组件工程类型单工程模式,整个项目只有一个工程,它包含:App module 加上各个业务组件module,就是所有的代码,这就是单工程模式。 如何做到组件单独调试呢?我们知道,在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来配置不同的module类型。Application 插件,id: com.android.applicationLibrary 插件,id: com.android.library区别比较简单, App 插件来配置一个 Android App 工程,项目构建后输出一个 APK 安装包,Library 插件来配置一个 Android Library 工程,构建后输出 ARR 包。显然我们的 App module配置的就是Application 插件,业务组件module 配置的是 Library 插件。想要实现 业务组件的独立调试,这就需要把配置改为 Application 插件;而独立开发调试完成后,又需要变回Library 插件进行集成调试。如何让组件在这两种调试模式之间自动转换呢? 手动修改组件的 gralde 文件,切换 Application 和 library ?如果项目只有两三个组件那么是可行的,但在大型项目中可能会有十几个业务组件,一个个手动修改显得费力笨拙。我们知道用AndroidStudio创建一个Android项目后,会在根目录中生成一个gradle.properties文件。在这个文件定义的常量,可以被任何一个build.gradle读取。 所以我们可以在gradle.properties中定义一个常量值 isModule,true为即独立调试;false为集成调试。然后在业务组件的build.gradle中读取 isModule,设置成对应的插件即可。代码如下://gradle.properties // 组件独立调试开关, 每次更改值后要同步工程 isModule = false //build.gradle //注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换 if (isModule.toBoolean()){ apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }3.1.2 动态配置ApplicationId 和 AndroidManifest我们知道一个 App 是需要一个 ApplicationId的 ,而组件在独立调试时也是一个App,所以也需要一个 ApplicationId,集成调试时组件是不需要ApplicationId的;另外一个 APP 也只有一个启动页, 而组件在独立调试时也需要一个启动页,在集成调试时就不需要了。所以ApplicationId、AndroidManifest也是需要 isModule 来进行配置的。//build.gradle (module_cart) android { ... defaultConfig { ... if (isModule.toBoolean()) { // 独立调试时添加 applicationId ,集成调试时移除 applicationId "com.hfy.componentlearning.cart" } ... } sourceSets { main { // 独立调试与集成调试时使用不同的 AndroidManifest.xml 文件 if (isModule.toBoolean()) { manifest.srcFile 'src/main/moduleManifest/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } ... }可见也是使用isModule分别设置applicationId、AndroidManifest。其中独立调试的AndroidManifest是新建于目录moduleManifest,使用 manifest.srcFile 即可指定两种调试模式的AndroidManifest文件路径。 moduleManifest中新建的manifest文件 指定了Application、启动activity://moduleManifest/AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hfy.module_cart" > <application android:name=".CartApplication" android:allowBackup="true" android:label="Cart" android:theme="@style/Theme.AppCompat"> <activity android:name=".CartActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>原本自动生成的manifest,未指定Application、启动activity:<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hfy.module_cart"> <application> <activity android:name=".CartActivity"></activity> </application> </manifest>独立调试、集成调试 ,分别使用“assembleDebug”构建结果如下: 3.2 多工程方案3.2.1 方案概述多工程方案,业务组件以library module形式存在于独立的工程。独立工程 自然就可以独立调试了,不再需要进行上面那些配置了。例如,购物车组件 就是 新建的工程Cart 的 module_cart模块,业务代码就写在module_cart中即可。app模块是依赖module_cart。app模块只是一个组件的入口,或者是一些demo测试代码。 那么当所有业务组件都拆分成独立组件时,原本的工程就变成一个只有app模块的壳工程了,壳工程就是用来集成所有业务组件的。3.2.1 maven引用组件那么如何进行集成调试呢?使用maven引用组件:1、发布组件的arr包 到公司的maven仓库,2、然后在壳工程中就使用implemention依赖就可以了,和使用第三方库一毛一样。另外arr包 分为 快照版本(SNAPSHOT) 和 正(Realease)式版本,快照版本是开发阶段调试使用,正式版本是正式发版使用。具体如下:首先,在module_cart模块中新建maven_push.gradle文件,和build.gradle同级目录apply plugin: 'maven' configurations { deployerJars } repositories { mavenCentral() } //上传到Maven仓库的task uploadArchives { repositories { mavenDeployer { pom.version = '1.0.0' // 版本号 pom.artifactId = 'cart' // 项目名称(通常为类库模块名称,也可以任意) pom.groupId = 'com.hfy.cart' // 唯一标识(通常为模块包名,也可以任意) //指定快照版本 maven仓库url, todo 请改为自己的maven服务器地址、账号密码 snapshotRepository(url: 'http://xxx/maven-snapshots/') { authentication(userName: '***', password: '***') } //指定正式版本 maven仓库url, todo 请改为自己的maven服务器地址、账号密码 repository(url: 'http://xxx/maven-releases/') { authentication(userName: '***', password: '***') } } } } // type显示指定任务类型或任务, 这里指定要执行Javadoc这个task,这个task在gradle中已经定义 task androidJavadocs(type: Javadoc) { // 设置源码所在的位置 source = android.sourceSets.main.java.sourceFiles } // 生成javadoc.jar task androidJavadocsJar(type: Jar) { // 指定文档名称 classifier = 'javadoc' from androidJavadocs.destinationDir } // 打包main目录下代码和资源的task,生成sources.jar task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } //配置需要上传到maven仓库的文件 artifacts { archives androidSourcesJar archives androidJavadocsJar } maven_push.gradle主要就是发布组件ARR的配置:ARR的版本号、名称、maven仓地址账号等。然后,再build.gradle中引用://build.gradle apply from: 'maven_push.gradle'接着,点击Sync后,点击Gradle任务uploadArchives,即可打包并发布arr到maven仓。最后,壳工程要引用组件ARR,需要先在壳工程的根目录下build.gradle中添加maven仓库地址:allprojects { repositories { google() jcenter() //私有服务器仓库地址 maven { url 'http://xxx' } } }接着在app的build.gradle中添加依赖即可:dependencies { ... implementation 'com.hfy.cart:cart:1.0.0' //以及其他业务组件 }可见,多工程方案 和我们平时使用第三方库是一样的,只是我们把组件ARR发布到公司的私有maven仓而已。实际上,我个人比较建议 使用多工程方案的。单工程方案没法做到代码权限管控,也不能做到开发人员职责划分明确,每个开发人员都可以对任意的组件进行修改,显然还是会造成混乱。多工程把每个组件都分割成单独的工程,代码权限可以明确管控。集成测试时,通过maven引用来集成即可。并且业务组件和业务基础组件也可以 和 基础组件一样,可以给公司其他项目复用。四、页面跳转4.1 方案 TheRouter前面说到,组件化的核心就是解耦,所以组件间是不能有依赖的,那么如何实现组件间的页面跳转呢?例如 在首页模块 点击 购物车按钮 需要跳转到 购物车模块的购物车页面,两个模块之间没有依赖,也就说不能直接使用 显示启动 来打开购物车Activity,那么隐式启动呢? 隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式—路由。比较著名的路由框架 有阿里老式的ARouter、货拉拉最新的 TheRouter,它们原理基本是一致的。这里我们采用使用更广泛更先进的TheRouter: TheRouter 核心功能具备四大能力:页面导航跳转能力(Navigator) 页面跳转能力介绍跨模块依赖注入能力(ServiceProvider)跨模块依赖注入单模块初始化(业务节点订阅)能力 (FlowTaskExecutor)单模块自动初始化能力介绍动态化能力 (ActionManager) 动态化能力支持4.2 TheRouter实现路由跳转前面提到,所有的业务组件都依赖了 Common 组件,所以我们在 Common 组件中使用关键字 api 添加的依赖,业务组件都能访问。 我们要使用 TheRouter 进行界面跳转,需要Common 组件添加 TheRouter 的依赖(另外,其它组件共同依赖的库也要都放到 Common 中统一依赖)。4.2.1 引入依赖因为TheRouter比较特殊,“cn.therouter:apt:xx” 的annotationProcessor依赖 需要所有使用到 TheRouter 的组件中都单独添加,不然无法在 apt 中生成索引文件,就无法跳转成功。并且在每个使用到 TheRouter 的组件的 build.gradle 文件中,都需要添加kapt或者annotationProcessor引入。然后壳工程需要依赖业务组件。如下所示://common组件的build.gradle dependencies { ... api 'cn.therouter:library:1.2.0' annotationProcessor 'cn.therouter:apt:1.2.0' //业务组件、业务基础组件 共同依赖的库(网络库、图片库等)都写在这里~ } //业务组件的build.gradle dependencies { ... annotationProcessor 'cn.therouter:apt:1.2.0' implementation 'com.github.hufeiyang:Common:1.0.0'//业务组件依赖common组件 } //壳工程app module的build.gradle dependencies { ... //这里没有使用私有maven仓,而是发到JitPack仓,一样的意思~ // implementation 'com.hfy.cart:cart:1.0.0' implementation 'com.github.hufeiyang:Cart:1.0.1' //依赖购物车组件 implementation 'com.github.hufeiyang:HomePage:1.0.2' //依赖首页组件 //壳工程内 也需要依赖Common组件,因为需要初始化TheRouter implementation 'com.github.hufeiyang:Common:1.0.0' }4.2.2 初始化依赖完了,TheRouter是自动初始化的,我们不需要任何初始化的代码调用。4.2.3 路由跳转由于首页组件是没有依赖购物车组件的,下面就来实现前面提到的 首页组件 无依赖 跳转到 购物车组件页面。而使用TheRouter进行简单路由跳转,只有两步:添加注解路径、通过路径路由跳转。1、在支持路由的页面上添加注解@Route(path = "/xx/xx"),路径需要注意的是至少需要有两级,/xx/xx。这里就是购物车组件的CartActivity:@Route(path = "/cart/cartActivity") public class CartActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cart); } } 2、然后在首页组件的HomeActivity 发起路由操作—点击按钮跳转到购物车,调用TheRouter.build("/xx/xx").navigation()即可:@Route(path = "/homepage/homeActivity") public class HomeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //通过路由跳转到 购物车组件的购物车页面(但没有依赖购物车组件) TheRouter .build("/cart/cartActivity") .withString("key1","value1")//携带参数1 .withString("key2","value2")//携带参数2 .navigation(); } }); } }另外,注意在HomeActivity上添加了注解和路径,这是为了壳工程的启动页中直接打开首页。还看到路由跳转可以像startActivity一样待参数。最后,壳工程的启动页中 通过路由打开首页(当然这里也可以用startActivity(),毕竟壳工程依赖了首页组件)://启动页 public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //通过路由直接打开home组件的HomeActivity, TheRouter.build("/homepage/homeActivity").navigation(); finish(); } }我们run壳工程 最后看下效果: 到这里,组件间页面跳转的问题也解决了。五、组件间通信组件间没有依赖,又如何进行通信呢?例如,首页需要展示购物车中商品的数量,而查询购物车中商品数量 这个能力是购物车组件内部的,这咋办呢?5.1 服务暴露组件平时开发中 我们常用 接口 进行解耦,对接口的实现不用关心,避免接口调用与业务逻辑实现紧密关联。这里组件间的解耦也是相同的思路,仅依赖和调用服务接口,不会依赖接口的实现。可能你会有疑问了:既然首页组件可以访问购物车组件接口了,那就需要依赖购物车组件啊,这俩组件还是耦合了啊,那咋办啊?答案是组件拆分出可暴露服务。见下图: 左侧是组件间可以调用对方服务 但是有依赖耦合。右侧,发现多了export_home、export_cart,这是对应拆分出来的专门用于提供服务的暴露组件。操作说明如下:暴露组件 只存放 服务接口、服务接口相关的实体类、路由信息、便于服务调用的util等服务调用方 只依赖 服务提供方的 露组件,如module_home依赖export_cart,而不依赖module_cart组件 需要依赖 自己的暴露组件,并实现服务接口,如module_cart依赖export_cart 并实现其中的服务接口接口的实现注入 依然是由TheRouter完成,和页面跳转一样使用路由信息下面按照此方案 来实施 首页调用购物车服务 来获取商品数量,更好地说明和理解。5.2 实施5.2.1 新建export_cart首先,在购物车工程中新建module即export_cart,在其中新建接口类ICartService并定义获取购物车商品数量方法,注意接口必须继承IProvider,是为了使用TheRouter的实现注入:/** * 购物车组件对外暴露的服务 */ public interface ICartService { /** * 获取购物车中商品数量 * @return */ CartInfo getProductCountInCart(); }CartInfo是购物车信息,包含商品数量:/** * 购物车信息 * @author hufeiyang */ public class CartInfo { /** * 商品数量 */ public int productCount; }接着,创建路由表信息,存放购物车组件对外提供跳转的页面、服务的路由地址:/** * 购物车组件路由表 * 即 购物车组件中 所有可以从外部跳转的页面 的路由信息 * @author hufeiyang */ public interface CartRouterTable { /** * 购物车页面 */ String PATH_PAGE_CART = "/cart/cartActivity"; /** * 购物车服务 */ String PATH_SERVICE_CART = "/cart/service"; }前面说页面跳转时是直接使用 路径字符串 进行路由跳转,这里是和服务路由都放在这里统一管理。然后,为了外部组件使用方便新建CartServiceUtil:/** * 购物车组件服务工具类 * 其他组件直接使用此类即可:页面跳转、获取服务。 * @author hufeiyang */ public class CartServiceUtil { /** * 跳转到购物车页面 * @param param1 * @param param2 */ public static void navigateCartPage(String param1, String param2){ TheRouter.get() .build(CartRouterTable.PATH_PAGE_CART) .withString("key1",param1) .withString("key2",param2) .navigation(); } /** * 获取服务 * @return */ public static ICartService getService(){ //return TheRouter.get().navigation(ICartService.class);//如果只有一个实现,这种方式也可以 return (ICartService) TheRouter.get().build(CartRouterTable.PATH_SERVICE_CART).navigation(); } /** * 获取购物车中商品数量 * @return */ public static CartInfo getCartProductCount(){ return getService().getProductCountInCart(); } }注意到,这里使用静态方法 分别提供了页面跳转、服务获取、服务具体方法获取。 其中服务获取 和页面跳转 同样是使用路由,并且服务接口实现类 也是需要添加@Route注解指定路径的。5.2.2 module_cart的实现首先,module_cart需要依赖export_cart://module_cart的Build.gradle dependencies { ... annotationProcessor 'cn.therouter:apt:1.2.0' implementation 'com.github.hufeiyang:Common:1.0.0' //依赖export_cart implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5' }点击sync后,接着CartActivity的path改为路由表提供:@Route(path = CartRouterTable.PATH_PAGE_CART) public class CartActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cart); } }然后,新建服务接口的实现类来实现ICartService,添加@Route注解指定CartRouterTable中定义的服务路由:/** * 购物车组件服务的实现 * 需要@Route注解、指定CartRouterTable中定义的服务路由 * @author hufeiyang */ @Route(path = CartRouterTable.PATH_SERVICE_CART) public class CartServiceImpl implements ICartService { @Override public CartInfo getProductCountInCart() { //这里实际项目中 应该是 请求接口 或查询数据库 CartInfo cartInfo = new CartInfo(); cartInfo.productCount = 666; return cartInfo; } @Override public void init(Context context) { //初始化工作,服务注入时会调用,可忽略 } }这里的实现是直接实例化了CartInfo,数量赋值666。然后发布一个ARR(“com.github.hufeiyang.Cart:module_cart:xxx”)。5.2.3 module_home中的使用和调试module_home需要依赖export_cart://module_home的Build.gradle dependencies { ... annotationProcessor 'cn.therouter:apt:1.2.0' implementation 'cn.therouter:library:1.2.0' implementation 'com.github.hufeiyang:Common:1.0.0' //注意这里只依赖export_cart(module_cart由壳工程引入) implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5' }在HomeActivity中新增TextView,调用CartServiceUtil获取并展示购物车商品数量:@Route(path = "/homepage/homeActivity") public class HomeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); //跳转到购物车页面 findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //通过路由跳转到 购物车组件的购物车页面(但没有依赖购物车组件) // TheRouter // .build("/cart/cartActivity") // .withString("key1","param1")//携带参数1 // .withString("key2","param2")//携带参数2 // .navigation(); CartServiceUtil.navigateCartPage("param1", "param1"); } }); //调用购物车组件服务:获取购物车商品数量 TextView tvCartProductCount = findViewById(R.id.tv_cart_product_count); tvCartProductCount.setText("购物车商品数量:"+ CartServiceUtil.getCartProductCount().productCount); } }看到 使用CartServiceUtil.getCartProductCount()获取购物车信息并展示,跳转页面也改为了CartServiceUtil.navigateCartPage()方法。到这里home组件的就可以独立调试了:页面跳转和服务调用,独立调试ok后 再集成到壳工程。 先让HomePage工程的app模块依赖Common组件、module_cart 以及本地的module_home//HomePage工程,app模块的Build.gradle dependencies { ... //引入本地Common组件、module_cart、module_home,在app module中独立调试使用 implementation 'com.github.hufeiyang:Common:1.0.0' implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6' implementation project(path: ':module_home') }然后新建MyApplication,引入TheRouter、在app的MainActivity中使用TheRouter.build("/homepage/homeActivity").navigation()打开首页,这样就可以调试了。调试ok后接着就是集成到壳工程。5.2.4 集成到壳工程壳工程中的操作和独立调试类似,区别是对首页组件引入的是ARR:dependencies { ... //这里没有使用私有maven仓,而是发到JitPack仓,一样的意思~ // implementation 'com.hfy.cart:cart:1.0.0' implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6' implementation 'com.github.hufeiyang:HomePage:1.0.4' //壳工程内 也需要依赖Common组件,因为需要引入TheRouter implementation 'com.github.hufeiyang:Common:1.0.0' }最后run壳工程来看下效果: 获取数量是666、跳转页面成功。另外,除了export_xxx这种方式,还可以添加一个 ComponentBase 组件,这个组件被所有的Common组件依赖,在这个组件中分别添加定义了业务组件可以对外提供访问自身数据的抽象方法的 Service。相当于把各业务组件的export整合到ComponentBase中,这样就只添加了一个组件而已。但是这样就不好管理了,每个组件对外能力的变更都要改ComponentBase。另外,除了组件间方法调用,使用EventBus在组件间传递信息也是ok的(注意Event实体类要定义在export_xxx中)。好了,到这里组件间通信问题也解决了。六、fragment实例获取上面介绍了Activity 的跳转,我们也会经常使用 Fragment。例如常见的应用主页HomeActivity 中包含了多个属于不同组件的 Fragment、或者有一个Fragment多个组件都需要用到。通常我们直接访问具体 Fragment 类来new一个Fragment 实例,但这里组件间没有直接依赖,那咋办呢?答案依然是TheRouter。先在module_cart中创建CartFragment://添加注解@Route,指定路径 @Route(path = CartRouterTable.PATH_FRAGMENT_CART) public class CartFragment extends Fragment { ... public CartFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //显示“cart_fragment" return inflater.inflate(R.layout.fragment_cart, container, false); } }同时是fragment添加注解@Route,指定路由路径,路由还是定义在export_cart的CartRouterTable中,所以export_cart需要先发一个ARR,module_cart来依赖,然后module_cart发布ARR。然后再module_home中依赖export_cart,使用TheRouter获取Fragment实例:@Route(path = "/homepage/homeActivity") public class HomeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); ... FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction= manager.beginTransaction(); //使用TheRouter获取Fragment实例 并添加 Fragment userFragment = (Fragment) TheRouter.build(CartRouterTable.PATH_FRAGMENT_CART).createFragment(); transaction.add(R.id.fl_test_fragment, userFragment, "tag"); transaction.commit(); } }可以先独立调试,然后集成到壳工程——依赖最新的module_cart 、HomePage,结果如下:绿色部分就是引用自cart组件的fragment。七、Application生命周期分发我们通常会在Application的onCreate中做一些初始化任务,例如前面提到的TheRouter初始化。而业务组件有时也需要获取应用的Application,也要在应用启动时进行一些初始化任务。你可能会说,直接在壳工程Application的onCreate操作就可以啊。但是这样做会带来问题:因为我们希望壳工程和业务组件 代码隔离(虽然有依赖),并且 我们希望组件内部的任务要在业务组件内部完成。那么如何做到 各业务组件 无侵入地获取 Application生命周期 呢?——答案是 使用TheRouter,他有单模块初始化(业务节点订阅)能力 (FlowTaskExecutor)单模块自动初始化能力,它专门用于在Android组件化开发中,Application生命周期主动分发到组件。支持单模块独立初始化支持懒加载初始化独立初始化允许多任务依赖(参考Gradle Task)支持编译期循环引用检测支持自定义业务初始化时机具体使用如下:common组件依赖 applifecycle-api 首先,common组件通过 api 添加 applifecycle-api 依赖 并发布ARR://common组件 build.gradle dependencies { ... //AppLifecycle api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4' }业务组件依赖applifecycle-compiler、实现接口+注解 各业务组件都要 依赖最新common组件,并添加 applifecycle-compiler 的依赖://业务组件 build.gradle ... //这里Common:1.0.2内依赖了applifecycle-api implementation 'com.github.hufeiyang:Common:1.0.2' annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'实现的方法 有onCreate、onTerminate、onLowMemory、onTrimMemory。最重要的就是onCreate方法了,相当于Application的onCreate方法,可在此处做初始化任务。 并且还可以通过getPriority()方法设置回调 多个组件onCreate方法调用的优先顺序,无特殊要求设置NORM_PRIORITY即可。壳工程引入AppLifecycle插件、触发回调壳工程引入新的common组件、业务组件,以及 引入AppLifecycle插件:// 假设隐私协议任务名为:AgreePrivacyCache /** * 同意隐私协议后初始化录音SDK */ @FlowTask(taskName="initRecord", dependsOn="AgreePrivacyCache") fun init(context:Context) = initRecordAudioSDK() // 当用户同意隐私协议时,调度依赖隐私协议的所有任务执行 TheRouter.runTask("TheRouter-AgreePrivacyCache") @FlowTask(taskName = "app1") public static void test3(Context context) { System.out.println("TheRouter线程=========应用启动就会自动执行"); }首先在inCreate方法中调用 ApplicationLifecycleManager的init()方法,用于收集组件内实现了IApplicationLifecycleCallbacks且添加了@AppLifecycle注解的类。然后在各生命周期方法内调用对应的ApplicationLifecycleManager的方法,来分发到所有组件。这样 组件 就能接收到Application的生命周期了。 到这里,组件化开发的5个问题点 都已经解决了。 下面来看看针对老项目如何实现组件化改造。八、总结本文介绍了 组件化开发的背景、架构、优势、要解决的问题 以及详细解决方案,独立调试、页面跳转、组件通信等,最后介绍的老项目组件化方案。其中涉及的最重要的工具是TheRouter,TheRouter 是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。支持KSP、支持AGP8,不仅能对常规的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法TheRouter还有很多进阶用法,有机会我也针对TheRouter写一篇全面分析。 你也可以先看看TheRouter官网的文章:https://therouter.cn/ GitHub地址:https://github.com/HuolalaTech/hll-wp-therouter-android不管 Android组件化还是Android模块化,是在项目发展到一定规模后 必定要使用的技术,学习至完全掌握非常必要。好了,今天就到这里,欢迎留言讨论~
文章
ARouter  ·  Java  ·  测试技术  ·  API  ·  Maven  ·  开发工具  ·  Android开发  ·  Kotlin  ·  索引
2023-03-14
企业级项目组件化重构之路
Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!前言前面几篇文章我们讲解了一个云音乐app的基础库搭建,今天我们就来对这个app进行组件化代码重构组件化基础库封装系列文章如下:Android组件化开发(一)--Maven私服的搭建 Android组件化开发(二)--网络请求组件封装Android组件化开发(三)--图片加载组件封装Android组件化开发(四)--进程保活组件的封装Android组件化开发(五)--完整版音乐播放组件的封装Android组件化开发(六)-- 短视频播放组件封装Android组件化开发(七)--从零开始教你分析项目需求并实现项目地址:https://github.com/ByteYuhb/anna_music_app项目演示:1.组件化重构效果这里先看下我们重构前后的框架图比较:重构前:重构后ft_xxx表示业务层模块 lib_xxx表示基础库模块重构后的架构图如下:重构前的代码业务封装在宿主app中,业务耦合严重,如果修改一个业务模块,需要对整个app进行完整测试,测试工作量巨大而重构后,我们只需要对单一app进行独立调试即可。重构后的框架结构:所有的业务组件之间通讯都通过ft_base_service进行通讯2.组件化重构准则1.单一业务可以单独调试,也可以作为lib提供给宿主app使用2.同一级别的模块不允许直接调用,比如我们的ft_home组件不允许直接调用ft_login组件,不然组件化的意义就不存在了3.组件间通讯不能直接使用显示的class文件跳转,可以考虑很用ARouter框架进行解耦4.每个组件可打包为aar或者jar上传到maven私服,宿主使用的时候,直接引用私服中aar包即可能做到以上几点,你的app就可以称为一个组件化框架的app了。3.组件化重构思路1.拆:拆代码,拆资源,拆构建由于所有业务和资源都耦合在宿主app中,所以需要将代码和资源拆开到对应模块中当然我们的构建build.gradle也需要拆分到不同模块中2.接:对外提供接口组件化之间不能直接通讯,需要使用暴露接口的方式对外通讯3.测:反复测试重构后代码,需要反复测试,防止出现意想不到的bug4.组件化重构过程这里我以登录业务ft_login为例子:1.步骤1:首先新建一个业务模块ft_login,然后在宿主app中将登录功能相关联的代码和资源抽离到ft_login中2.步骤2:将和登录构建相关的依赖分配到ft_login构建中。3.步骤3:单独调试功能实现3.1:在gradle.properties中创建一个全局变量:isRunAlone=true3.2:在build.gradle中:if(isRunAlone.toBoolean()){ apply plugin:'com.android.application' }else{ apply plugin:'com.android.library' } android { compileSdkVersion 33 buildToolsVersion "33.0.0" defaultConfig { if(isRunAlone.toBoolean()){ applicationId 'com.anna.ft_login' } ... } sourceSets { main { java { srcDirs = ['src/main/java'] } resources { srcDirs = ['src/main/res'] } aidl { srcDirs = ['src/main/aidl'] } manifest { if(isRunAlone.toBoolean()){ srcFile 'src/main/manifest/AndroidManifest.xml' }else { srcFile 'src/main/AndroidManifest.xml' } } } } } def dependList = [rootProject.depsLibs.okhttp, rootProject.depsLibs.gson, rootProject.depsLibs.appcompact, rootProject.depsLibs.design, rootProject.depsLibs.eventbus, rootProject.depsLibs.arouterapi, ':lib_network',':lib_common_ui',':ft_base_service'] dependencies { if(!isRunAlone.toBoolean()){ dependList.each { String depend -> depend.startsWithAny(':lib',':ft')? compileOnly(project(depend)):compileOnly(depend){ switch (depend){ case rootProject.depsLibs.arouterapi: exclude group: 'com.android.support' break; } } } }else { dependList.each { String depend -> depend.startsWithAny(':lib',':ft')? implementation(project(depend)):implementation(depend) { switch (depend) { case rootProject.depsLibs.arouterapi: exclude group: 'com.android.support' break; } } } } //arouter注解处理器 annotationProcessor rootProject.depsLibs.aroutercompiler testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } 单独调试状态下注意四点:1.引用application插件2.引入applicationId3.引入不同给的sourceSets构建路径4.引入的库单独调试状态下需要使用implementation导入,不能使用compileOnly实现上面四点,只要打开isRunAlone就可作为一个单独app运行了。4.步骤4:组件间通讯这里,我们引入一个ft_base_service模块,这个模块用来实现组件间通讯用,需要调用别的业务模块都需要使用这个模块才能通讯、业务模块与ft_base_service之间通讯使用的是路由ARouter:关于ARouter的使用可以参考这篇文章:Android开源系列-组件化框架Arouter-(一)使用方式详解1.创建ft_base_service,在这个模块中:创建一个LoginService接口继承IProvider引入ARouter依赖:android { javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"] } } } //arouter核心api implementation rootProject.depsLibs.arouterapi //arouter注解处理器 annotationProcessor rootProject.depsLibs.aroutercompiler创建LoginService:public interface LoginService extends IProvider { boolean hasLogin(); void login(Context context); }2.在ft_login业务模块中实现LoginService接口注意这里因为使用了ARouter注解,所以也需要引入ARouter依赖@Route(path = "/login/login_service") public class LoginServiceImpl implements LoginService { Context context; @Override public boolean hasLogin() { return UserManager.getInstance().hasLogined(); } @Override public void login(Context context) { LoginActivity.start(context); } @Override public void init(Context context) { Log.d("TAG","LoginServiceImpl is init"); } }3.在ft_base_service模块中对LoginService接口进行依赖注入public class LoginImpl { @Autowired(name = "/login/login_service") public LoginService mLoginService; private static LoginImpl mLoginImpl = null; public static LoginImpl getInstance() { if (mLoginImpl == null) { synchronized (LoginImpl.class) { if (mLoginImpl == null) { mLoginImpl = new LoginImpl(); } return mLoginImpl; } } return mLoginImpl; } private LoginImpl(){ ARouter.getInstance().inject(this); } public boolean hasLogin(){ return mLoginService.hasLogin(); } public void login(Context context){ mLoginService.login(context); } }笔者使用了一个单例类LoginImpl,在构造器中对LoginService依赖注入ARouter.getInstance().inject(this);然后宿主app或者其他模块引用登录业务功能时,需要依赖ft_base_service模块,并使用LoginImpl的接口即可。这里要说明下,平时我们使用的四大组件跳转也可以使用这个方式来处理,在服务接口中定义跳转接口即可。当然也可以使用Arouter的Activity跳转方式或者Fragment实例获取方式5.代码打包aar上传到maven私服:关于这块maven私服更多内容可以参考这篇文章:Gradle筑基篇(六)-使用Maven实现组件化类库发布这里我们封装了一个通用组件发布库:apply plugin: 'maven' uploadArchives { repositories { mavenDeployer { // 是否快照版本 def isSnapShot = Boolean.valueOf(MAVEN_IS_SNAPSHOT) def versionName = MAVEN_VERSION if (isSnapShot) { versionName += "-SNAPSHOT" } // 组件信息 pom.groupId = MAVEN_GROUP_ID pom.artifactId = MAVEN_ARTIFACTID pom.version = versionName // 快照仓库路径 snapshotRepository(url: uri(MAVEN_SNAPSHOT_URL)) { authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME) } // 发布仓库路径 repository(url: uri(MAVEN_RELEASE_URL)) { authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME) } println("###################################" + "\nuploadArchives = " + pom.groupId + ":" + pom.artifactId + ":" + pom.version + "." + pom.packaging + "\nrepository =" + (isSnapShot ? MAVEN_SNAPSHOT_URL : MAVEN_RELEASE_URL) + "\n###################################" ) } } } 然后在对应的组件下面引用:apply from:file('../maven.gradle')发布的时候直接在Gradle面板中点击uploadArchives任务即可**经过上面几个步骤就基本完成了login组件的封装并发布,且对外提供了login组件接口其他组件也是按照上面的逻辑进行重构**更多详细信息可以自己拿到项目源代码查看。5.组件化重构总结**组件化不仅是一种架构,更是一种思想,架构是可以变得,但是核心思想却是统一的,在拆分代码的时候,要注意模块的颗粒度,不是颗粒度越小就越好,模块分离的好,后期对组件改造会有很大帮助,关于组件化的文章就讲到这里,组件化重构的项目已经上传到Github。后面会出一期插件化的项目改造。敬请期待。**
文章
ARouter  ·  Java  ·  Maven  ·  Android开发
2023-02-14
Gradle筑基篇(一)-Gradle初探
theme: smartbluehighlight: a11y-dark携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 >> Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!前言:大家回想一下自己第一次接触Gradle是什么时候?相信大家也都是和我一样,在我们打开第一个AS项目的时候,发现有很多带gradle字样的文件:`setting.gradle,build.gradle,gradle.warpper,以及在gradle`文件中各种配置,这些都是啥wy啊。。特别对于一些小公司开发人员,因为接触架构层面的机会很少,可能在使用AS几年后都不一定对Gradle有太多深入了解,这是实话,因为笔者就是这么过来的。。而Gradle又是进阶高级开发的必经之路。好了,接下来进入正题,此系列笔者会由浅入深的方式,带领大家来了解下,Gradle背后究竟有哪些奥秘。本系列文章:Gradle筑基篇:Gradle筑基篇(一)-Gradle初探Gradle筑基篇(二)-Groovy语法的详解Gradle筑基篇(三)-Gradle生命周期Gradle筑基篇(四)-Gradle APi详解Gradle筑基篇(五)-Gradle自定义插件Gradle筑基篇(六)-Gradle Maven仓库管理Gradle进阶篇Gradle进阶篇(六)-AGP详解本篇是这个系列的第一篇文章:Gradle初探1.Gradle定义:很多开发喜欢把Gradle简单定义为一种构建工具,和ant,maven等作用类似,诚然Gradle确实是用来做构建,但是如果简单得把Gradle拿来做构建,就太小看Gradle了.笔者更愿意将Gradle看做一种编程框架。在这个框架中,你可以做很多ant,maven等常用构建工具做不了的事情,如将自己的任务task集成到构建生命周期中,完成文件拷贝,脚本编写等操作。2.Gradle优缺点:相较早期的构建工具:ant,maven等。优点如下:1.使用DSL Grovvy语言来编写::了解ant的同学应该都知道:ant使用的是xml配置的模式,而Gradle使用的是表达性的Groovy来编写,Groovy同时支持面向对象和面向过程进行开发,这个特性让Groovy可以写出一些脚本的任务,这在传统ant,maven上是不可能实现的2.基于java虚拟机::Groovy是基于jvm的语言,groovy文件编译后其实就是class文件,和我们的java一样。所以在gradle构建过程中,我们完全可以使用java/kotlin去编写我们的构建任务以及脚本,极大的降低我们学习的成本。3.Gradle自定义task:可以构建自己的任务,然后挂接到gradle构建生命周期中去,这在ant,maven上也是不可能实现的,4.扩展性好:gradle将关键配置扔给我们开发者,开发者配置好任务后,无需关心gradle是如何构建的。5.支持增量更新:增量更新可以大大加快我们的编译速度关于Groovy的语法篇:可以参考这篇文章:Gradle筑基篇(二)-groovy语法详解缺点:用过gradle都知道,低版本gradle的项目在高版本的gradle中经常出现很多莫名其妙的错误,向后兼容性较差。3.Gradle工程结构:gradle标准工程代码如下├── moduleA │ └── build.gradle ├── moduleB │ └── build.gradle ├── build.gradle ├── settings.gradle ├── gradle.properties ├── local.properties ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── gradlew.bat1.build.gradle:可以理解为一个Project脚本,Project脚本中有自己的任务,最外层的Project为rootProject2.settings.gradle:主要用来配置我们项目中需要用到的模块。用include关键字给包裹进3.gradle.properties:这个文件主要是设置一些全局变量,包括jvm运行以及自定义的一些全局参数4.local.properties:这个文件主要配置一些本地的sdk和ndk版本信息以及路径5.gradle-wrapper.jar:负责自动下载Gradle脚本运行环境6.gradle-wrapper.properties:用来配置当前使用的Gradle的版本以及存储的路径distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip distributionBase + distributionPath:指定Gradle安装路径; zipStoreBase + zipStorePath:指定Gradle安装包的存储路径; distributionUrl:Gradle版本的下载地址。注意这里如果将bin改为all,则可以查看当前Gradle的源码信息。7.gradlew和gradlew.bat:用来执行构建任务的脚本,可以在命令行使用gradlew xxxTask4.Gradle生命周期Gradle作为新兴的构建工具,其内部也有自己的生命周期阶段,每个阶段做的事情都层次分明,了解Gradle生命周期,才能很好的使用我们的Gradle工具。1.初始化阶段做了哪些事情?:1.初始化Setting.gradle文件,获取setting实例,2.执行setting中的脚本,根据include字段,创建对应的project实例3.设置构建需要的环境注意:初始化阶段执行任何任务都会执行一次。Project实例关系如下:2.配置阶段1.下载所有插件和构建脚本依赖项2.执行build.gradle文件中的脚本信息3.实现task任务的拓扑图,这个图是一个有向无环图,防止任务执行进入死循环。注意:配置阶段执行任何任务都会执行一次。3.执行阶段执行阶段就是根据当前task拓扑图进行执行task任务。需要注意以下几点:1.在项目中配置的doLast,doFirst操作,都会在任务执行阶段执行,而不会在配置阶段执行,而如果任务需要执行,需要挂接到gradle执行生命周期中,笔者开始接触gradle时就踩过这个坑。。这块后面讲解task的时候在来具体讲解2.前面也说了初始化阶段和配置阶段在每个任务执行前都会执行,所以不要在前两个阶段进行一些耗时的操作,这样可能每次编译执行你都会崩溃的5.Gradle生命周期监听:要查找Gradle是如何监听生命周期,可以到Gradle源码中看看:1.监听初始化阶段初始化阶段主要用来初始化Setting.gradle文件,获取setting实例,创建Project实例等,所以其可用下面代码监听://开始初始化Setting.gradle前 this.gradle.beforeSettings { println "beforeSettings" } //Setting.gradle配置完毕后,创建了setting实例 this.gradle.settingsEvaluated { println "settingsEvaluated" } //执行解析Setting.gradle文件后,创建了project实例列表 this.gradle.projectsLoaded { println "projectsLoaded" }2.监听配置阶段2.1:监听当前project的配置阶段前后:在Project源码中可以看到:/** * Adds an action to execute immediately before this project is evaluated. * * @param action the action to execute. */ void beforeEvaluate(Action<? super Project> action); /** * Adds an action to execute immediately after this project is evaluated. * * @param action the action to execute. */ void afterEvaluate(Action<? super Project> action); /** * <p>Adds a closure to be called immediately before this project is evaluated. The project is passed to the closure * as a parameter.</p> * * @param closure The closure to call. */ void beforeEvaluate(Closure closure); /** * <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the * closure as a parameter. Such a listener gets notified when the build file belonging to this project has been * executed. A parent project may for example add such a listener to its child project. Such a listener can further * configure those child projects based on the state of the child projects after their build files have been * run.</p> * * @param closure The closure to call. */ void afterEvaluate(Closure closure);看这两个方法的说明就是用来监听配置阶段,传入的是一个Action或者传入一个闭包,闭包的代理为当前Project使用方式如下://监听project被配置前 this.beforeEvaluate {Project project -> println "${project.name} :beforeEvaluate" } //监听project被配置后 this.afterEvaluate {Project project -> println "${project.name}:afterEvaluate" }注意:这个监听只是针对当前Project的配置阶段而不是所有Project的配置你也可以使用:this.project.beforeEvaluate this.project.afterEvaluate那么有没有可以监听所有Project的配置阶段的api呢?安排2.2:监听每个Project的配置前后:使用this.gradle的内部方法,因为gradle是相对于整个工程作为作用域//监听所有的Project的被配置前 this.gradle.beforeProject {Project project -> println "${project.name}:beforeProject" } //监听所有的Project的被配置后 this.gradle.afterProject {Project project -> println "${project.name}:afterProject" } 编译下看看:> Configure project : gradle_source_plugin:afterProject > Configure project :app app:beforeProject do app evaluating app:afterProject > Configure project :application application:beforeProject do application evaluating application:afterProject看到当前工程所有的project都调用了一次beforeProject和afterProject那有同学又要问了,有没有监听整个project配置阶段的:当然有2.3:监听全部project配置阶段的前后this.gradle.projectsEvaluated { println "all projectsEvaluated" }这个闭包可以监听整个项目的配置完毕后的事件配置阶段还有一些监听如下:2.4:监听任务的添加操作this.project.tasks.whenTaskAdded {Task task-> println "${task.name}:whenTaskAdded" }2.5:监听任务拓扑图的执行//task拓扑图构造完毕 this.gradle.taskGraph.whenReady {TaskExecutionGraph graph-> println "taskGraph:->"+graph }监听拓扑图完毕后其实才是真正的配置阶段完毕,瞧瞧源码:在BasePlugin中:threadRecorder.record( ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE, project.getPath(), null, this::configureProject); threadRecorder.record( ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION, project.getPath(), null, this::configureExtension); threadRecorder.record( ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION, project.getPath(), null, this::createTasks);看到配置阶段最后一步才是创建Task,所以可以使用this.gradle.taskGraph.whenReady监听整个配置阶段的结束3.监听执行阶段3.1:监听任务执行:gradle.taskGraph.beforeTask { Task task -> println "${task.name}:beforeTask" } gradle.taskGraph.afterTask {Task task -> println "${task.name}:afterTask" } 执行下面任务: task clean(type: Delete) { doFirst { println 'clean:doFirst' } doLast { println 'clean:doLast' } delete rootProject.buildDir } 结果: > Task :clean clean:beforeTask clean:doFirst clean:doLast clean:afterTask可以看到在task执行前后调用了监听中的方法3.2:监听执行任务阶段开始其实可以使用配置阶段的this.gradle.taskGraph.whenReady,这个就是所有project配置完毕,且生成了task拓扑图下一步就是开始执行任务了3.3:监听执行任务阶段结束this.gradle.buildFinished {}这个可以监听所有任务执行完毕后事件回调:6.Gradle ApiGradle为我们提供了很多丰富的api操作主要有几下几种:Project apiTask api属性 api文件 api以及一些其他api由于api这块篇幅比较多,就不展开讲解了,后面会单独出一篇文章来讲解这块内容7.Gradle插件Gradle插件在我们的项目中使用的还是比较多的,在一些优秀的开源框架:如鹅厂的Tinker,滴滴的VirtualApk,阿里的Arouter等内部都使用了Gradle插件知识笔者Gradle插件开始学习的时候,也是一脸懵逼,其实你把Gradle插件理解为一个第三方jar包就可以了,只是这个jar包是用于我们apk构建的过程内部其实也是使用一些Task,挂接到我们的apk构建生命周期中。这里也不会过多讲解下面我们来讲下Gradle一个特性:8.增量更新有没发现你在构建过程中,如果修改的地方对整个任务容器影响不大情况下,你的编译速度会很快,其实就是Gradle默认支持增量更新功能。1.定义:官方:An important part of any build tool is the ability to avoid doing work that has already been done. Consider the process of compilation. Once your source files have been compiled, there should be no need to recompile them unless something has changed that affects the output, such as the modification of a source file or the removal of an output file. And compilation can take a significant amount of time, so skipping the step when it’s not needed saves a lot of time.简单点说就是Gradle目前对Task的输入和输出做了判断,如果发现文件的输入和输出没有变化, 就直接使用之前缓存的输入输出数据,不再重新执行,缩短编译时间 这里就涉及到了Task的一些知识点: Task是我们apk构建过程中给的最少单位,每个任务都有输入和输出,将输入的信息传递给下一个任务作为下一个任务的输入,这就是整个构建体系正常运行的核心。2.Task输入和输出任务的执行离不开输入和输出,和我们方法执行一样,依赖输入参数和输出返回值 Gradle中使用:TaskInputs:来管理输入TaskOutputs:来管理输出我们来看下这个两个类的内部代码:TaskInputs.java public interface TaskInputs { /** * Returns true if this task has declared the inputs that it consumes. * * @return true if this task has declared any inputs. */ boolean getHasInputs(); /** * Returns the input files of this task. * * @return The input files. Returns an empty collection if this task has no input files. */ FileCollection getFiles(); /** * Registers some input files for this task. * * @param paths The input files. The given paths are evaluated as per {@link org.gradle.api.Project#files(Object...)}. * @return a property builder to further configure the property. */ TaskInputFilePropertyBuilder files(Object... paths); /** * Registers some input file for this task. * * @param path The input file. The given path is evaluated as per {@link org.gradle.api.Project#file(Object)}. * @return a property builder to further configure the property. */ TaskInputFilePropertyBuilder file(Object path); /** * Registers an input directory hierarchy. All files found under the given directory are treated as input files for * this task. * * @param dirPath The directory. The path is evaluated as per {@link org.gradle.api.Project#file(Object)}. * @return a property builder to further configure the property. */ TaskInputFilePropertyBuilder dir(Object dirPath); /** * Returns a map of input properties for this task. * * The returned map is unmodifiable, and does not reflect further changes to the task's properties. * Trying to modify the map will result in an {@link UnsupportedOperationException} being thrown. * * @return The properties. */ Map<String, Object> getProperties(); /** * <p>Registers an input property for this task. This value is persisted when the task executes, and is compared * against the property value for later invocations of the task, to determine if the task is up-to-date.</p> * * <p>The given value for the property must be Serializable, so that it can be persisted. It should also provide a * useful {@code equals()} method.</p> * * <p>You can specify a closure or {@code Callable} as the value of the property. In which case, the closure or * {@code Callable} is executed to determine the actual property value.</p> * * @param name The name of the property. Must not be null. * @param value The value for the property. Can be null. */ TaskInputPropertyBuilder property(String name, @Nullable Object value); /** * Registers a set of input properties for this task. See {@link #property(String, Object)} for details. * * <p><strong>Note:</strong> do not use the return value to chain calls. * Instead always use call via {@link org.gradle.api.Task#getInputs()}.</p> * * @param properties The properties. */ TaskInputs properties(Map<String, ?> properties); /** * Returns true if this task has declared that it accepts source files. * * @return true if this task has source files, false if not. */ boolean getHasSourceFiles(); /** * Returns the set of source files for this task. These are the subset of input files which the task actually does work on. * A task is skipped if it has declared it accepts source files, and this collection is empty. * * @return The set of source files for this task. */ FileCollection getSourceFiles(); }源文件中我们可以看出:输入可以有以下种类:1.文件,文件夹以及一个文件集合2.普通的key value属性2.Map:传递一个Map的属性集合TaskInputs还可以通过getHasInputs判断是否有输入同理我们来看下TaskOutputs的源码,篇幅原因,这里直接看下TaskOutputs的方法框架:大部分情况和inputs类似,可以输出为文件,属性properties等注意到这里有几个关键的方法:upToDateWhen和cacheIf这两个方法就是用来对构建中的是否对输出操作进行缓存的点,用于增量构建使用总结本篇文章主要是讲解了Gradle一些基础认识,Gradle工程项目的概括以及Gradle构建生命周期管理和监听等操作。后面文章会陆续推出关于GradleApi,Gradle插件以及AGP插件的详细介绍,希望大家能从中会有一些收获。​
文章
缓存  ·  ARouter  ·  Java  ·  API  ·  Maven  ·  Android开发  ·  开发者  ·  数据格式  ·  Kotlin  ·  容器
2023-02-14
Android开源系列-组件化框架Arouter-(三)APT技术详解
theme: juejinhighlight: a11y-light携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情 >> Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!前言最近组里需要进行组件化框架的改造,用到了ARouter这个开源框架,为了更好的对项目进行改造,笔者花了一些时间去了解了下ARouterARouter是阿里巴巴团队在17年初发布的一款针对组件化模块之间无接触通讯的一个开源框架,经过多个版本的迭代,现在已经非常成熟了。ARouter主要作用:组件间通讯,组件解耦,路由跳转,涉及到我们常用的Activity,Provider,Fragment等多个场景的跳转接下来笔者会以几个阶段来对Arouter进行讲解:Android开源系列-组件化框架Arouter-(一)使用方式详解Android开源系列-组件化框架Arouter-(二)深度原理解析Android开源系列-组件化框架Arouter-(三)APT技术详解Android开源系列-组件化框架Arouter-(四)AGP插件详解这篇文章我们来讲解下:APT技术详解APT前置知识注解基础:1.元注解1.@Target:目标,表示注解修饰的目标ElementType.ANNOTIONS_TYPE: 目标是注解,给注解设置的注解ElementType.CONSTRUCTOR: 构造方法ElementType.FIELD: 属性注解ElementType.METHOD: 方法注解ElementType.Type: 类型如:类,接口,枚举ElementType.PACKAGE: 可以给一个包进行注解ElementType.PARAMETER: 可以给一个方法内的参数进行注解ElementType.LOCAL_VARIABLE: 可以给局部变量进行注解2.@Retention:表示需要在什么级别保存该注解信息RetentionPolicy.SOURCE:在编译阶段有用,编译之后会被丢弃,不会保存到字节码class文件中RetentionPolicy.CLASS:注解在class文件中可用,但是会被VM丢弃,在类加载时会被丢弃,在字节码文件处理中有用,注解默认使用这种方式RetentionPolicy.RUNTIME:运行时有效,可以通过反射获取注解信息3.@Document:将注解包含到javaDoc中4.@Inherit:运行子类继承父类的注解5.@Repeatable:定义注解可重复2.元注解的使用方式2.1:基本使用方式@Target(ElementType.METHOD) ://表示作用在方法中 @Retention(RetentionPolicy.SOURCE) ://表示只在编译器有效 public @interface Demo1 { public int id(); //注解的值,无默认值,在创建注解的时候需要设置该值 public String desc() default "no info";//注解默认值 } @Demo1(id=1) public void getData() { }2.2:重复注解使用方式定义Persons:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Persons { Person[] value(); }定义Person:@Repeatable(Persons.class) public @interface Person{ String role() default ""; }使用使用:@Person(role="CEO") @Person(role="husband") @Person(role="father") @Person(role="son") public class Man { String name=""; }调用注解if(Man.class.isAnnotationPresent(Persons.class)) {先判断是否存在这个注解 Persons p2=Man.class.getAnnotation(Persons.class);获取注解 for(Person t:p2.value()){ System.out.println(t.role()); } } 结果: 1 CEO husband father son3.运行时注解需要使用反射获取@Retention(RetentionPolicy.RUNTIME) public void getAnnoInfo() { Class clazz = GetAnno.class; //获得所有的方法 Method[] methods = clazz.getMethods(); for (Method method : methods) { method.setAccessible(true);//禁用安全机制 if (method.isAnnotationPresent(Demo1.class)) {//检查是否使用了Demo1注解 Demo1 demo1 = method.getAnnotation(Demo1.class);//获得注解实例 String name = method.getName();//获得方法名称 } }4.编译时注解需要使用到APT工具@Retention(RetentionPolicy.SOURCE)或者CLASS注解的获取可以使用编译期注解动态生成代码,很多优秀的开源库都是使用这个方式:如Arouter ButterKnife,GreenDao,EventBus3等APT知识储备1.APT是一种注解解析工具:**在编译期找出源代码中所有的注解信息,如果指定了注解器(继承AbstractProcessor),那么在编译期会调用这个注解器里面的代码,我们可以在这里面做一些处理,如根据注解信息动态生成一些代码,并将代码注入到源码中**使用到的工具类:工具类1:Element表示程序的一个元素,它只在编译期存在。可以是package,class,interface,method,成员变量,函数参数,泛型类型等。Element的子类介绍:ExecutableElement:类或者接口中的方法,构造器或者初始化器等元素PackageElement:代表一个包元素程序VariableElement:代表一个类或者接口中的属性或者常量的枚举类型,方法或者构造器的参数,局部变量,资源变量或者异常参数TypeElement:代表一个类或者接口元素TypeParameterElement:代表接口,类或者方法的泛型参数元素通过Element可以获取什么信息呢?1.asType() 返回TypeMirror: TypeMirror是元素的类型信息,包括包名,类(或方法,或参数)名/类型 TypeMirror的子类: ArrayType, DeclaredType, DisjunctiveType, ErrorType, ExecutableType, NoType, NullType, PrimitiveType, ReferenceType, TypeVariable, WildcardType getKind可以获取类型: 2.equals(Object obj) 比较两个Element利用equals方法。 3.getAnnotation(Class annotationType) 传入注解可以获取该元素上的所有注解。 4.getAnnotationMirrors() 获该元素上的注解类型。 5.getEnclosedElements() 获取该元素上的直接子元素,类似一个类中有VariableElement。 6.getEnclosingElement() 获取该元素的父元素, 如果是属性VariableElement,则其父元素为TypeElement, 如果是PackageElement则返回null, 如果是TypeElement则返回PackageElement, 如果是TypeParameterElement则返回泛型Element 7.getKind() 返回值为ElementKind,通过ElementKind可以知道是那种element,具体就是Element的那些子类。 8.getModifiers() 获取修饰该元素的访问修饰符,public,private 9.getSimpleName() 获取元素名,不带包名, 如果是变量,获取的就是变量名, 如果是定义了int age,获取到的name就是age。 如果是TypeElement返回的就是类名 10.getQualifiedName():获取类的全限定名,Element没有这个方法它的子类有,例如TypeElement,得到的就是类的全类名(包名)。 11.Elements.getPackageOf(enclosingElement).asType().toString():获取所在的包名: ​ 工具类2:ProcessingEnvironment:APT运行环境:里面提供了写新文件, 报告错误或者查找其他工具.1.getFiler():返回用于创建新的源,类或辅助文件的文件管理器。 2.getElementUtils():返回对元素进行操作的一些实用方法的实现. 3.getMessager():返回用于报告错误,警告和其他通知的信使。 4.getOptions():返回传递给注解处理工具的处理器特定选项。 5.getTypeUtils():返回一些用于对类型进行操作的实用方法的实现。 工具类3:ElementKind如何判断Element的类型呢,需要用到ElementKind,ElementKind为元素的类型,元素的类型判断不需要用instanceof去判断,而应该通过getKind()去判断对应的类型element.getKind()==ElementKind.CLASS; 工具类4:TypeKindTypeKind为类型的属性,类型的属性判断不需要用instanceof去判断,而应该通过getKind()去判断对应的属性element.asType().getKind() == TypeKind.INT javapoet:生成java文件3种生成文件的方式:1.StringBuilder·进行拼接2.模板文件进行字段替换3.javaPoet 生成StringBuilder进行拼接,模板文件进行字段替换进行简单文件生成还好,如果是复杂文件,拼接起来会相当复杂所以一般复杂的都使用Square出品的sdk:javapoetimplementation "com.squareup:javapoet:1.11.1"自己实现自定义APT工具类步骤:1.创建一个单独javalib模块lib_annotions:创建需要的注解类:@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); }2.再创建一个javalib模块lib_compilers:在模块中创建一个继承AbstractProcessor的类:@AutoService(Processor.class) public class CustomProcessorTest extends AbstractProcessor { public Filer filer; private Messager messager; private List<String> result = new ArrayList<>(); private int round; private Elements elementUtils; private Map<String, String> options; @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(); annotations.add(CustomBindAnnotation.class.getCanonicalName()); return annotations; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnvironment.getFiler(); messager = processingEnvironment.getMessager(); elementUtils = processingEnv.getElementUtils(); options = processingEnv.getOptions(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { messager.printMessage(Diagnostic.Kind.NOTE,"process"); Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = getTypeElementMap(roundEnv); messager.printMessage(Diagnostic.Kind.NOTE,"2222"); for(TypeElement key:typeElementMap.keySet()){ Map<Integer, VariableElement> variableElementMap = typeElementMap.get(key); TypeSpec typeSpec = generalTypeSpec(key,variableElementMap); String packetName = elementUtils.getPackageOf(key).getQualifiedName().toString(); messager.printMessage(Diagnostic.Kind.NOTE,"packetName:"+packetName); JavaFile javaFile = JavaFile.builder(packetName,typeSpec).build(); try { javaFile.writeTo(processingEnv.getFiler()); messager.printMessage(Diagnostic.Kind.NOTE,"3333"); } catch (IOException e) { e.printStackTrace(); } } return true; } private TypeSpec generalTypeSpec(TypeElement key,Map<Integer, VariableElement> variableElementMap) { return TypeSpec.classBuilder(key.getSimpleName().toString()+"ViewBinding") .addModifiers(Modifier.PUBLIC) .addMethod(generalMethodSpec(key,variableElementMap)) .build(); } private MethodSpec generalMethodSpec(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) { ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString()); String parameter = "_" + toLowerCaseFirstChar(className.simpleName()); MethodSpec.Builder builder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC,Modifier.STATIC) .returns(void.class) .addParameter(className,parameter); messager.printMessage(Diagnostic.Kind.NOTE,"typeElement.getQualifiedName().toString():"+typeElement.getQualifiedName().toString()); messager.printMessage(Diagnostic.Kind.NOTE,"typeElement.className():"+className.simpleName().toString()); messager.printMessage(Diagnostic.Kind.NOTE,"parameter:"+parameter); for(int viewId:variableElementMap.keySet()){ VariableElement variableElement = variableElementMap.get(viewId); String elementName = variableElement.getSimpleName().toString(); String elementType = variableElement.asType().toString(); messager.printMessage(Diagnostic.Kind.NOTE,"elementName:"+elementName); messager.printMessage(Diagnostic.Kind.NOTE,"elementType:"+elementType); // builder.addCode("$L.$L = ($L)$L.findViewById($L);\n",parameter,elementName,elementType,parameter,viewId); builder.addStatement("$L.$L = ($L)$L.findViewById($L)",parameter,elementName,elementType,parameter,viewId); } // for (int viewId : varElementMap.keySet()) { // VariableElement element = varElementMap.get(viewId); // String name = element.getSimpleName().toString(); // String type = element.asType().toString(); // String text = "{0}.{1}=({2})({3}.findViewById({4}));"; // builder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId))); // } return builder.build(); } private Map<TypeElement, Map<Integer, VariableElement>> getTypeElementMap(RoundEnvironment roundEnv) { Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = new HashMap<>(); messager.printMessage(Diagnostic.Kind.NOTE,"1111"); Set<? extends Element> variableElements = roundEnv.getElementsAnnotatedWith(CustomBindAnnotation.class); for(Element element:variableElements){ VariableElement variableElement = (VariableElement) element;//作用在字段上,可以强制转换为VariableElement TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); Map<Integer, VariableElement> varElementMap = typeElementMap.get(typeElement); if(varElementMap == null){ varElementMap = new HashMap<>(); typeElementMap.put(typeElement,varElementMap); } CustomBindAnnotation customBindAnnotation = variableElement.getAnnotation(CustomBindAnnotation.class); int viewId = customBindAnnotation.value(); varElementMap.put(viewId,variableElement); } return typeElementMap; } //将首字母转为小写 private static String toLowerCaseFirstChar(String text) { if (text == null || text.length() == 0 || Character.isLowerCase(text.charAt(0))) return text; else return String.valueOf(Character.toLowerCase(text.charAt(0))) + text.substring(1); } }这个类中:重写以下方法1.getSupportedAnnotationTypes: 该方法主要作用是:返回支持的注解类型 public Set<String> getSupportedAnnotationTypes() { Set<String> hashSet = new HashSet<>(); hashSet.add(BindView.class.getCanonicalName()); return hashSet; } 2.getSupportedSourceVersion: 作用:返回支持的jdk版本 public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } 3.init(ProcessingEnvironment processingEnvironment) 作用:返回一个ProcessingEnvironment 这个工具内部有很多处理类 1.getFiler():返回用于创建新的源,类或辅助文件的文件管理器。 2.getElementUtils():返回对元素进行操作的一些实用方法的实现. 3.getMessager():返回用于报告错误,警告和其他通知的信使。 4.getOptions():返回传递给注解处理工具的处理器特定选项。 5.getTypeUtils():返回一些用于对类型进行操作的实用方法的实现。 4.process(Set<? extends TypeElement> set, RoundEnvironment environment): 作用:apt核心处理方法,可以在这里面对收集到的注解进行处理,生成动态原文件等3.在模块的build.gradle文件中implementation "com.google.auto.service:auto-service:1.0-rc6" //使用Auto-Service来自动注册APT //Android Plugin for Gradle >= 3.4 或者 Gradle Version >=5.0 都要在自己的annotation processor工程里面增加如下的语句 annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' implementation "com.squareup:javapoet:1.11.1"//辅助生成文件的工具类 implementation project(':lib_annotionss')//该模块是注解存再的库中4.最后编译会自动生成对应的类。然后在需要的地方加上注解就可以了。编译器自动生成的文件:public class AnnotationActivityViewBinding { public static void bind(AnnotationActivity _annotationActivity) { _annotationActivity.btn1 = (android.widget.Button)_annotationActivity.findViewById(2131296347); _annotationActivity.lv = (android.widget.ListView)_annotationActivity.findViewById(2131296475); _annotationActivity.btn = (android.widget.Button)_annotationActivity.findViewById(2131296346); } }ARouter中APT的使用我们来看ARouter源码框架app:是ARouter提供的一个测试Demoarouter-annotation:这个lib模块中声明了很多注解信息和一些枚举类arouter-api:ARouter的核心api,转换过程的核心操作都在这个模块里面arouter-compiler:APT处理器,自动生成路由表的过程就是在这里面实现的arouter-gradle-plugin:这是一个编译期使用的Plugin插件,主要作用是用于编译器自动加载路由表,节省应用的启动时间。我们主要看arouter-annotation和arouter-compiler这两个模块1.arouter-annotation可以看到这里面实现了几个注解类Autowired:属性注解@Target({ElementType.FIELD}) @Retention(RetentionPolicy.CLASS) public @interface Autowired { // 标志我们外部调用使用的key String name() default ""; // 如果有要求,一定要传入,不然app会crash // Primitive type wont be check! boolean required() default false; // 注解字段描述 String desc() default ""; }@Target({ElementType.FIELD}):指定我们注解是使用在属性字段上@Retention(RetentionPolicy.CLASS):指定我们注解只在编译期存在Interceptor:拦截器注解@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Interceptor { /** * The priority of interceptor, ARouter will be excute them follow the priority. */ int priority(); /** * The name of interceptor, may be used to generate javadoc. */ String name() default "Default"; }@Target({ElementType.TYPE}):指定注解是在类上@Retention(RetentionPolicy.CLASS):指定注解在编译期存在Route:路由注解@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Route { /** * Path of route */ String path(); /** * Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!! */ String group() default ""; /** * Name of route, used to generate javadoc. */ String name() default ""; /** * Extra data, can be set by user. * Ps. U should use the integer num sign the switch, by bits. 10001010101010 */ int extras() default Integer.MIN_VALUE; /** * The priority of route. */ int priority() default -1; }@Target({ElementType.TYPE}):指定注解是使用在类上@Retention(RetentionPolicy.CLASS):指定注解是在编译期存在枚举类:RouteType:路由类型public enum RouteType { ACTIVITY(0, "android.app.Activity"), SERVICE(1, "android.app.Service"), PROVIDER(2, "com.alibaba.android.arouter.facade.template.IProvider"), CONTENT_PROVIDER(-1, "android.app.ContentProvider"), BOARDCAST(-1, ""), METHOD(-1, ""), FRAGMENT(-1, "android.app.Fragment"), UNKNOWN(-1, "Unknown route type"); } TypeKind public enum TypeKind { // Base type BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE, // Other type STRING, SERIALIZABLE, PARCELABLE, OBJECT; }model类RouteMeta:路由元数据public class RouteMeta { private RouteType type; // Type of route private Element rawType; // Raw type of route private Class<?> destination; // Destination private String path; // Path of route private String group; // Group of route private int priority = -1; // The smaller the number, the higher the priority private int extra; // Extra data private Map<String, Integer> paramsType; // Param type private String name; private Map<String, Autowired> injectConfig; // Cache inject config. } 总结下arouter-annotation:1.创建了Autowired:属性注解,Interceptor:拦截器注解,Route:路由注解2.创建了RouteType:路由类型枚举,RouteMeta:路由元数据2.arouter-compilerAutowiredProcessor:属性Autowired注解处理器InterceptorProcessor:拦截器Interceptor注解处理器RouteProcessor:路由Route注解处理器BaseProcessor:注解处理器基类,主要获取一些通用参数,上面三个都继承这个基类incremental.annotation.processors:拦截器声明,这里将我们需要使用的几个注解处理器做了声明com.alibaba.android.arouter.compiler.processor.RouteProcessor,aggregating com.alibaba.android.arouter.compiler.processor.AutowiredProcessor,aggregating com.alibaba.android.arouter.compiler.processor.InterceptorProcessor,aggregating下面依次来看:AutowiredProcessor:@AutoService(Processor.class)//使用AutoService可以将处理器自动注册到processors文件中 @SupportedAnnotationTypes({ANNOTATION_TYPE_AUTOWIRED}) //设置需要匹配的注解类:"com.alibaba.android.arouter.facade.annotation.Autowired" public class AutowiredProcessor extends BaseProcessor { private Map<TypeElement, List<Element>> parentAndChild = new HashMap<>(); // Contain field need autowired and his super class. private static final ClassName ARouterClass = ClassName.get("com.alibaba.android.arouter.launcher", "ARouter"); private static final ClassName AndroidLog = ClassName.get("android.util", "Log"); @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); logger.info(">>> AutowiredProcessor init. <<<"); } //这是注解处理器的核心方法 @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { if (CollectionUtils.isNotEmpty(set)) { try { //这里将所有声明Autowired注解的属性包括在parentAndChild中:parentAndChild的key为注解的类TypeElement //parentAndChild{List<Element>{element1,element2,element3...}} categories(roundEnvironment.getElementsAnnotatedWith(Autowired.class)); //生成帮助类 generateHelper(); } catch (Exception e) { logger.error(e); } return true; } return false; } private void generateHelper() throws IOException, IllegalAccessException { //获取com.alibaba.android.arouter.facade.template.ISyringe的TypeElement TypeElement type_ISyringe = elementUtils.getTypeElement(ISYRINGE); //获取com.alibaba.android.arouter.facade.service.SerializationService的TypeElement TypeElement type_JsonService = elementUtils.getTypeElement(JSON_SERVICE); //获取com.alibaba.android.arouter.facade.template.IProvider的TypeMirror:元素的类型信息,包括包名,类(或方法,或参数)名/类型 TypeMirror iProvider = elementUtils.getTypeElement(Consts.IPROVIDER).asType(); //获取android.app.Activity的TypeMirror:元素的类型信息,包括包名,类(或方法,或参数)名/类型 TypeMirror activityTm = elementUtils.getTypeElement(Consts.ACTIVITY).asType(); //获取android.app.Fragment的TypeMirror:元素的类型信息,包括包名,类(或方法,或参数)名/类型 TypeMirror fragmentTm = elementUtils.getTypeElement(Consts.FRAGMENT).asType(); TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType(); // 生成属性参数的辅助类 ParameterSpec objectParamSpec = ParameterSpec.builder(TypeName.OBJECT, "target").build(); if (MapUtils.isNotEmpty(parentAndChild)) { //遍历parentAndChild:每个entry使用的key为当前类的TypeElement,value为当前类内部所有使用注解Autowired标记的属性 for (Map.Entry<TypeElement, List<Element>> entry : parentAndChild.entrySet()) { //MethodSpec生成方法的辅助类 METHOD_INJECT = 'inject' /** 方法名:inject 方法注解:Override 方法权限:public 方法参数:前面objectParamSpec生成的:Object target */ MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder(METHOD_INJECT) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(objectParamSpec); //key为当前类的TypeElement TypeElement parent = entry.getKey(); //value为当前类内部所有使用注解Autowired标记的属性 List<Element> childs = entry.getValue(); //类的全限定名 String qualifiedName = parent.getQualifiedName().toString(); //类的包名 String packageName = qualifiedName.substring(0, qualifiedName.lastIndexOf(".")); //类的文件名:NAME_OF_AUTOWIRED = $$ARouter$$Autowired,完整fileName = BaseActivity$$ARouter$$Autowired String fileName = parent.getSimpleName() + NAME_OF_AUTOWIRED; //TypeSpec生成类的辅助类 /** 类名:BaseActivity$$ARouter$$Autowired 类doc:"DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER." 父类:com.alibaba.android.arouter.facade.template.ISyringe 权限:public */ TypeSpec.Builder helper = TypeSpec.classBuilder(fileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_ISyringe)) .addModifiers(PUBLIC); //生成字段属性辅助类 /** 字段类型:SerializationService 字段名:serializationService 字段属性:private */ FieldSpec jsonServiceField = FieldSpec.builder(TypeName.get(type_JsonService.asType()), "serializationService", Modifier.PRIVATE).build(); //将字段添加到类:BaseActivity$$ARouter$$Autowired中 helper.addField(jsonServiceField); /** 给inject方法添加语句:这里parent = BaseActivity 1.serializationService = ARouter.getInstance().navigation(SerializationService.class); 2.BaseActivity substitute = (BaseActivity)target; */ injectMethodBuilder.addStatement("serializationService = $T.getInstance().navigation($T.class)", ARouterClass, ClassName.get(type_JsonService)); injectMethodBuilder.addStatement("$T substitute = ($T)target", ClassName.get(parent), ClassName.get(parent)); /** 生成方法内部代码,注入属性 */ for (Element element : childs) { //获取当前element注解Autowired的属性: Autowired fieldConfig = element.getAnnotation(Autowired.class); //获取注解的名称 String fieldName = element.getSimpleName().toString(); //判断是否是iProvider的子类,说明iProvider字段如果使用Autowired注解的话,会单独处理 if (types.isSubtype(element.asType(), iProvider)) { // It's provider if ("".equals(fieldConfig.name())) { // User has not set service path, then use byType. // Getter injectMethodBuilder.addStatement( "substitute." + fieldName + " = $T.getInstance().navigation($T.class)", ARouterClass, ClassName.get(element.asType()) ); } else { // use byName // Getter injectMethodBuilder.addStatement( "substitute." + fieldName + " = ($T)$T.getInstance().build($S).navigation()", ClassName.get(element.asType()), ARouterClass, fieldConfig.name() ); } // Validator 这里如果设置了required为true,则一定要有值,否则会报错 if (fieldConfig.required()) { injectMethodBuilder.beginControlFlow("if (substitute." + fieldName + " == null)"); injectMethodBuilder.addStatement( "throw new RuntimeException(\"The field '" + fieldName + "' is null, in class '\" + $T.class.getName() + \"!\")", ClassName.get(parent)); injectMethodBuilder.endControlFlow(); } } else { // It's normal intent value //普通属性 /** 假设fieldName = "name" originalValue = "substitute.name" statement = "substitute.name = substitute." */ String originalValue = "substitute." + fieldName; String statement = "substitute." + fieldName + " = " + buildCastCode(element) + "substitute."; boolean isActivity = false; //判断是Activity 则statement += "getIntent()." if (types.isSubtype(parent.asType(), activityTm)) { // Activity, then use getIntent() isActivity = true; statement += "getIntent()."; //判断是Fragment 则statement += "getArguments()." } else if (types.isSubtype(parent.asType(), fragmentTm) || types.isSubtype(parent.asType(), fragmentTmV4)) { // Fragment, then use getArguments() statement += "getArguments()."; } else { //非Activity和Fragment,其他情况抛异常 throw new IllegalAccessException("The field [" + fieldName + "] need autowired from intent, its parent must be activity or fragment!"); } //statement = "substitute.name = substitute.getIntent().getExtras() == null ? substitute.name : substitute.getIntent().getExtras() statement = buildStatement(originalValue, statement, typeUtils.typeExchange(element), isActivity, isKtClass(parent)); if (statement.startsWith("serializationService.")) { // Not mortals injectMethodBuilder.beginControlFlow("if (null != serializationService)"); injectMethodBuilder.addStatement( "substitute." + fieldName + " = " + statement, (StringUtils.isEmpty(fieldConfig.name()) ? fieldName : fieldConfig.name()), ClassName.get(element.asType()) ); injectMethodBuilder.nextControlFlow("else"); injectMethodBuilder.addStatement( "$T.e(\"" + Consts.TAG + "\", \"You want automatic inject the field '" + fieldName + "' in class '$T' , then you should implement 'SerializationService' to support object auto inject!\")", AndroidLog, ClassName.get(parent)); injectMethodBuilder.endControlFlow(); } else { //将statement注入到injectMethodBuilder方法中 injectMethodBuilder.addStatement(statement, StringUtils.isEmpty(fieldConfig.name()) ? fieldName : fieldConfig.name()); } // 添加null判断 if (fieldConfig.required() && !element.asType().getKind().isPrimitive()) { // Primitive wont be check. injectMethodBuilder.beginControlFlow("if (null == substitute." + fieldName + ")"); injectMethodBuilder.addStatement( "$T.e(\"" + Consts.TAG + "\", \"The field '" + fieldName + "' is null, in class '\" + $T.class.getName() + \"!\")", AndroidLog, ClassName.get(parent)); injectMethodBuilder.endControlFlow(); } } } //将方法inject注入到类中 helper.addMethod(injectMethodBuilder.build()); //生成java文件 JavaFile.builder(packageName, helper.build()).build().writeTo(mFiler); logger.info(">>> " + parent.getSimpleName() + " has been processed, " + fileName + " has been generated. <<<"); } logger.info(">>> Autowired processor stop. <<<"); } } /** * Categories field, find his papa. * * @param elements Field need autowired */ private void categories(Set<? extends Element> elements) throws IllegalAccessException { if (CollectionUtils.isNotEmpty(elements)) { for (Element element : elements) { //获取element的父元素:如果是属性,父元素就是类或者接口:TypeElement TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //如果element属性是PRIVATE,则直接报错,所以对于需要依赖注入的属性,一定不能为private if (element.getModifiers().contains(Modifier.PRIVATE)) { throw new IllegalAccessException("The inject fields CAN NOT BE 'private'!!! please check field [" + element.getSimpleName() + "] in class [" + enclosingElement.getQualifiedName() + "]"); } //判断parentAndChild是否包含enclosingElement,第一次循环是空值会走到else分支,第二次才会包含 //格式:parentAndChild{List<Element>{element1,element2,element3...}} if (parentAndChild.containsKey(enclosingElement)) { // Has categries parentAndChild.get(enclosingElement).add(element); } else { List<Element> childs = new ArrayList<>(); childs.add(element); parentAndChild.put(enclosingElement, childs); } } logger.info("categories finished."); } } } 通过在编译器使用注解处理器AutowiredProcessor处理后,自动生成了以下文件BaseActivity$$ARouter$$Autowired.java/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */ public class BaseActivity$$ARouter$$Autowired implements ISyringe { private SerializationService serializationService; @Override public void inject(Object target) { serializationService = ARouter.getInstance().navigation(SerializationService.class); BaseActivity substitute = (BaseActivity)target; substitute.name = substitute.getIntent().getExtras() == null ? substitute.name : substitute.getIntent().getExtras().getString("name", substitute.name); } }生成过程:1.使用Map<TypeElement, List> parentAndChild = new HashMap<>()存储所有被Autowired注解的属性key:每个类的TypeElementvalue:当前类TypeElement中所有的Autowired注解的属性字段2.使用ParameterSpec生成参数3.使用MethodSpec生成方法:METHOD_INJECT = 'inject'方法名:inject 方法注解:Override 方法权限:public 方法参数:前面objectParamSpec生成的:Object target4.使用TypeSpec生成类:类名:BaseActivity$$ARouter$$Autowired 类doc:"DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER." 父类:com.alibaba.android.arouter.facade.template.ISyringe 权限:public5.使用addStatement给方法添加语句body6.将方法注入到帮助类中 helper.addMethod(injectMethodBuilder.build()); 7.写入java文件JavaFile.builder(packageName, helper.build()).build().writeTo(mFiler); RouteProcessor@AutoService(Processor.class) @SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED}) //这里表示我们的RouteProcessor可以处理Route和Autowired两种注解 public class RouteProcessor extends BaseProcessor { private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta. private Map<String, String> rootMap = new TreeMap<>(); // Map of root metas, used for generate class file in order. private TypeMirror iProvider = null; private Writer docWriter; // Writer used for write doc //初始化 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); //这里如果支持generateDoc,则打开docWriter,待写入文件:generateDoc字段由模块中的build.gradle文件传入 if (generateDoc) { try { docWriter = mFiler.createResource( StandardLocation.SOURCE_OUTPUT, PACKAGE_OF_GENERATE_DOCS, "arouter-map-of-" + moduleName + ".json" ).openWriter(); } catch (IOException e) { logger.error("Create doc writer failed, because " + e.getMessage()); } } //获取IPROVIDER的类型TypeMirror iProvider = elementUtils.getTypeElement(Consts.IPROVIDER).asType(); logger.info(">>> RouteProcessor init. <<<"); } //核心处理api /** * {@inheritDoc} * * @param annotations * @param roundEnv */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (CollectionUtils.isNotEmpty(annotations)) { Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class); try { logger.info(">>> Found routes, start... <<<"); //解析Routes this.parseRoutes(routeElements); } catch (Exception e) { logger.error(e); } return true; } return false; } private void parseRoutes(Set<? extends Element> routeElements) throws IOException { if (CollectionUtils.isNotEmpty(routeElements)) { // prepare the type an so on. logger.info(">>> Found routes, size is " + routeElements.size() + " <<<"); rootMap.clear(); //获取Activity的TypeMirror TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType(); //获取Service的TypeMirror TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType(); //获取Fragment的TypeMirror TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType(); TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType(); // Interface of ARouter //获取IRouteGroup的TypeElement TypeElement type_IRouteGroup = elementUtils.getTypeElement(IROUTE_GROUP); ////获取IProviderGroup的TypeElement TypeElement type_IProviderGroup = elementUtils.getTypeElement(IPROVIDER_GROUP); //获取RouteMeta的ClassName:权限定名 ClassName routeMetaCn = ClassName.get(RouteMeta.class); //获取RouteType的ClassName:权限定名 ClassName routeTypeCn = ClassName.get(RouteType.class); /*创建Map<String, Class<? extends IRouteGroup>>类型的ParameterizedTypeName Build input type, format as : ```Map<String, Class<? extends IRouteGroup>>``` */ ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ParameterizedTypeName.get( ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup)) ) ); /*创建Map<String, RouteMeta>类型的ParameterizedTypeName ```Map<String, RouteMeta>``` */ ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get( ClassName.get(Map.class), ClassName.get(String.class), ClassName.get(RouteMeta.class) ); /*创建参数类型rootParamSpec,groupParamSpec,providerParamSpec Build input param name. */ ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build(); ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build(); ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build(); // Ps. its param type same as groupParamSpec! /*创建loadInto方法的MethodSpec Build method : 'loadInto' */ MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(rootParamSpec); // Follow a sequence, find out metas of group first, generate java file, then statistics them as root. //遍历routeElements所有的path注解对象 for (Element element : routeElements) { //获取对象element的TypeMirror TypeMirror tm = element.asType(); //获取element的注解Route Route route = element.getAnnotation(Route.class); RouteMeta routeMeta; // Activity or Fragment 如果是Activity或者Fragment:根据不同情况创建不同的routeMeta路由元数据 if (types.isSubtype(tm, type_Activity) || types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) { // Get all fields annotation by @Autowired Map<String, Integer> paramsType = new HashMap<>(); Map<String, Autowired> injectConfig = new HashMap<>(); //这里是收集所有的Autowired属性参数 injectParamCollector(element, paramsType, injectConfig); if (types.isSubtype(tm, type_Activity)) { // Activity logger.info(">>> Found activity route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType); } else { // Fragment logger.info(">>> Found fragment route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), paramsType); } routeMeta.setInjectConfig(injectConfig); } else if (types.isSubtype(tm, iProvider)) { // IProvider logger.info(">>> Found provider route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null); } else if (types.isSubtype(tm, type_Service)) { // Service logger.info(">>> Found service route: " + tm.toString() + " <<<"); routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null); } else { throw new RuntimeException("The @Route is marked on unsupported class, look at [" + tm.toString() + "]."); } //收集路由元数据 categories(routeMeta); } //创建IProvider注解的loadInto方法 MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(providerParamSpec); Map<String, List<RouteDoc>> docSource = new HashMap<>(); // Start generate java source, structure is divided into upper and lower levels, used for demand initialization. for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) { String groupName = entry.getKey(); //创建IGroupRouter的loadInto方法 MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO) .addAnnotation(Override.class) .addModifiers(PUBLIC) .addParameter(groupParamSpec); List<RouteDoc> routeDocList = new ArrayList<>(); // 创建 group 方法的 body Set<RouteMeta> groupData = entry.getValue(); for (RouteMeta routeMeta : groupData) { RouteDoc routeDoc = extractDocInfo(routeMeta); ClassName className = ClassName.get((TypeElement) routeMeta.getRawType()); switch (routeMeta.getType()) { //创建PROVIDER的loadInto方法体 case PROVIDER: // Need cache provider's super class List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces(); for (TypeMirror tm : interfaces) { routeDoc.addPrototype(tm.toString()); if (types.isSameType(tm, iProvider)) { // Its implements iProvider interface himself. // This interface extend the IProvider, so it can be used for mark provider loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", (routeMeta.getRawType()).toString(), routeMetaCn, routeTypeCn, className, routeMeta.getPath(), routeMeta.getGroup()); } else if (types.isSubtype(tm, iProvider)) { // This interface extend the IProvider, so it can be used for mark provider loadIntoMethodOfProviderBuilder.addStatement( "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", tm.toString(), // So stupid, will duplicate only save class name. routeMetaCn, routeTypeCn, className, routeMeta.getPath(), routeMeta.getGroup()); } } break; default: break; } // Make map body for paramsType StringBuilder mapBodyBuilder = new StringBuilder(); Map<String, Integer> paramsType = routeMeta.getParamsType(); Map<String, Autowired> injectConfigs = routeMeta.getInjectConfig(); if (MapUtils.isNotEmpty(paramsType)) { List<RouteDoc.Param> paramList = new ArrayList<>(); for (Map.Entry<String, Integer> types : paramsType.entrySet()) { mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); "); RouteDoc.Param param = new RouteDoc.Param(); Autowired injectConfig = injectConfigs.get(types.getKey()); param.setKey(types.getKey()); param.setType(TypeKind.values()[types.getValue()].name().toLowerCase()); param.setDescription(injectConfig.desc()); param.setRequired(injectConfig.required()); paramList.add(param); } routeDoc.setParams(paramList); } String mapBody = mapBodyBuilder.toString(); //创建IGroupRouter的方法体 loadIntoMethodOfGroupBuilder.addStatement( "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))", routeMeta.getPath(), routeMetaCn, routeTypeCn, className, routeMeta.getPath().toLowerCase(), routeMeta.getGroup().toLowerCase()); routeDoc.setClassName(className.toString()); routeDocList.add(routeDoc); } // Generate groups 生成IGroupRrouter的子类文件 String groupFileName = NAME_OF_GROUP + groupName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(groupFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IRouteGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfGroupBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated group: " + groupName + "<<<"); rootMap.put(groupName, groupFileName); docSource.put(groupName, routeDocList); } if (MapUtils.isNotEmpty(rootMap)) { // Generate root meta by group name, it must be generated before root, then I can find out the class of group. for (Map.Entry<String, String> entry : rootMap.entrySet()) { loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue())); } } // Output route doc if (generateDoc) { //将path关系写入doc docWriter.append(JSON.toJSONString(docSource, SerializerFeature.PrettyFormat)); docWriter.flush(); docWriter.close(); } // Write provider into disk 写入provider String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(providerMapFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(type_IProviderGroup)) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfProviderBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<"); // Write root meta into disk.写入root meta String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName; JavaFile.builder(PACKAGE_OF_GENERATE_FILE, TypeSpec.classBuilder(rootFileName) .addJavadoc(WARNING_TIPS) .addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT))) .addModifiers(PUBLIC) .addMethod(loadIntoMethodOfRootBuilder.build()) .build() ).build().writeTo(mFiler); logger.info(">>> Generated root, name is " + rootFileName + " <<<"); } } }生成过程:和上面生成AutoWried过程类似,都是使用javapoet的api生成对应的java文件这里我们需要生成三种文件:ARouter$$Root$$xxx:xxx是当前模块名的缩写,存储当前模块路由组的信息:value是路由组的类名 /** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */ public class ARouter$$Root$$modulejava implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("m2", ARouter$$Group$$m2.class); routes.put("module", ARouter$$Group$$module.class); routes.put("test", ARouter$$Group$$test.class); routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class); } }ARouter$$Group$$xxx:xxx是当前路由组的组名,存储一个路由组内路由的信息:内部包含多个路由信息/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */ public class ARouter$$Group$$test implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648)); atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648)); atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); put("boy", 0); put("age", 3); }}, -1, -2147483648)); atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648)); atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("pac", 10); put("ch", 5); put("obj", 11); put("fl", 6); put("name", 8); put("dou", 7); put("boy", 0); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648)); atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648)); } }ARouter$$Providers$$xxx,xxx是模块名,存储的是当前模块中的IProvider信息,key是IProvider的名称,value是RouteMeta路由元数据/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */ public class ARouter$$Providers$$modulejava implements IProviderGroup { @Override public void loadInto(Map<String, RouteMeta> providers) { providers.put("com.alibaba.android.arouter.demo.service.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/yourservicegroupname/hello", "yourservicegroupname", null, -1, -2147483648)); providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/yourservicegroupname/json", "yourservicegroupname", null, -1, -2147483648)); providers.put("com.alibaba.android.arouter.demo.module1.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/yourservicegroupname/single", "yourservicegroupname", null, -1, -2147483648)); } }还有其他比如拦截器的java文件生成方式就不再描述了,和前面两个注解处理器是一样的原理。自动生成了这些帮助类之后,在编译器或者运行期,通过调用这些类的loadInto方法,可以将路由元信息加载到内存中。总结本文在开始主要讲解一些注解和注解处理器的前置知识,且带大家自己实现了一个APT自动生成文件的demo,最后讲解下在ARouter中APT是如何再编译器动态生成几种帮助类的。到这里已经是ARouter的第三篇了持续输出中。。你的关注和点赞是我最大的动力
文章
存储  ·  ARouter  ·  Java  ·  编译器  ·  API  ·  开发工具  ·  Android开发
2023-02-14
Android开源系列-组件化框架Arouter-(二)深度原理解析
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情 >> Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!前言最近组里需要进行组件化框架的改造,用到了ARouter这个开源框架,为了更好的对项目进行改造,笔者花了一些时间去了解了下ARouterARouter是阿里巴巴团队在17年初发布的一款针对组件化模块之间无接触通讯的一个开源框架,经过多个版本的迭代,现在已经非常成熟了。ARouter主要作用:组件间通讯,组件解耦,路由跳转,涉及到我们常用的Activity,Provider,Fragment等多个场景的跳转接下来笔者会以几个阶段来对Arouter进行讲解:Android开源系列-组件化框架Arouter-(一)使用方式详解Android开源系列-组件化框架Arouter-(二)深度原理解析Android开源系列-组件化框架Arouter-(三)APT技术详解Android开源系列-组件化框架Arouter-(四)AGP插件详解这篇文章我们来讲解下:Arouter的基本原理1.ARouter认知首先我们从命名来看:ARouter翻译过来就是一个路由器。官方定义:一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦那么什么是路由呢?简单理解就是:一个公共平台转发系统工作方式:1.注册服务:将我们需要对外暴露的页面或者服务注册到ARouter公共平台中2.调用服务:调用ARouter的接口,传入地址和参数,ARouter解析传入的地址和参数转发到对应的服务中通过ARouter形成了一个无接触解耦的调用过程2.ARouter架构解析我们来看下ARouter的源码架构:app:是ARouter提供的一个测试Demoarouter-annotation:这个lib模块中声明了很多注解信息和一些枚举类arouter-api:ARouter的核心api,转换过程的核心操作都在这个模块里面arouter-compiler:APT处理器,自动生成路由表的过程就是在这里面实现的arouter-gradle-plugin:这是一个编译期使用的Plugin插件,主要作用是用于编译器自动加载路由表,节省应用的启动时间。3.原理讲解这里我们不会一开始就大篇幅对源码进行讲解:我们先来介绍ARouter中的几个重要概念:有了这几个概念,后面在去看源码就会轻松多了前置基础概念:概念1:PostCard(明信片)既然是明信片要将信件寄到目的人的手上就至少需要:收件人的姓名和地址,寄件人以及电话和地址等ARouter就是使用PostCard这个类来存储寄件人和收件人信息的。public final class Postcard extends RouteMeta { // Base private Uri uri; //如果使用Uri方式发起luyou private Object tag; // A tag prepare for some thing wrong. inner params, DO NOT USE! private Bundle mBundle; // 需要传递的参数使用bundle存储 private int flags = 0; // 启动Activity的标志:如NEW_FALG private int timeout = 300; // 路由超时 private IProvider provider; // 使用IProvider的方式跳转 private boolean greenChannel; //绿色通道,可以不经过拦截器 private SerializationService serializationService; //序列化服务serializationService:需要传递Object自定义类型对象,就需要实现这个服务 private Context context; // May application or activity, check instance type before use it. private String action; //Activity跳转的Action // Animation private Bundle optionsCompat; // The transition animation of activity private int enterAnim = -1; private int exitAnim = -1; ... } PostCard继承了RouteMeta:public class RouteMeta { private RouteType type; // 路由类型:如Activity,Fragment,Provider等 private Element rawType; // 路由原始类型,在编译时用来判断 private Class<?> destination; // 目的Class对象 private String path; // 路由注册的path private String group; // 路由注册的group分组 private int priority = -1; // 路由执行优先级,priority越低,优先级越高,这个一般在拦截器中使用 private int extra; // Extra data private Map<String, Integer> paramsType; // 参数类型,例如activity中使用@Autowired的参数类型 private String name; //路由名字,用于生成javadoc private Map<String, Autowired> injectConfig; // 参数配置(对应paramsType). }RouteMeta:主要存储的是一些目的对象的信息,这些对象是在路由注册的时候才会生成。概念2:Interceptor拦截器了解OkHttp的都知道,其内部调用过程就是使用的拦截器模式,每个拦截器执行的对应的任务。而ARouter中也是如此,所有的路由调用过程在到达目的地前都会先经过自定义的一系列拦截器,实现一些AOP切面编程。public interface IInterceptor extends IProvider { /** * The operation of this interceptor. * * @param postcard meta * @param callback cb */ void process(Postcard postcard, InterceptorCallback callback); } IInterceptor是一个接口,继承了IProvider,所以其也是一个服务类型只需要实现process方法就可以实现拦截操作。概念3:greenChannel:绿色通道设置了绿色通道的跳转过程,可以不经过拦截器概念4:Warehouse:路由仓库Warehouse意为仓库,用于存放被 @Route、@Interceptor注释的 路由相关的信息,也就是我们关注的destination等信息举个例子:moduleB发起路由跳转到moduleA的activity,moduleB没有依赖moduleA,只是在moduleA的activity上增加了@Route注解。 由于进行activity跳转需要目标Activity的class对象来构建intent,所以必须有一个中间人,把路径"/test/activity"翻译成Activity的class对象,然后moduleB才能实现跳转。(因此在ARouter的使用中 moduleA、moduleB 都是需要依赖 arouter-api的)这个中间人那就是ARouter了,而这个翻译工所作用到的词典就是Warehouse,它存着所有路由信息。class Warehouse { //所有IRouteGroup实现类的class对象,是在ARouter初始化中赋值,key是path第一级 //(IRouteGroup实现类是编译时生成,代表一个组,即path第一级相同的所有路由,包括Activity和Provider服务) static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>(); //所有路由元信息,是在completion中赋值,key是path //首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务 static Map<String, RouteMeta> routes = new HashMap<>(); //所有服务provider实例,在completion中赋值,key是IProvider实现类的class static Map<Class, IProvider> providers = new HashMap<>(); //所有provider服务的元信息(实现类的class对象),是在ARouter初始化中赋值,key是IProvider实现类的全类名。 //主要用于使用IProvider实现类的class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class) static Map<String, RouteMeta> providersIndex = new HashMap<>(); //所有拦截器实现类的class对象,是在ARouter初始化时收集到,key是优先级 static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("..."); //所有拦截器实例,是在ARouter初始化完成后立即创建 static List<IInterceptor> interceptors = new ArrayList<>(); ... }Warehouse存了哪些信息呢?groupsIndex:存储所有路由组元信息:key:group的名称 value:路由组的模块类class类: 赋值时机:初始化的时候routes:存储所有路由元信息。切记和上面路由组分开,路由是单个路由,路由组是一批次路由key:路由的path value:路由元信息 赋值时机:LogisticsCenter.completion中赋值 备注:首次进行某个路由时就会加载整个group的路由,即IRouteGroup实现类中所有路由信息。包括Activity和Provider服务providers:存储所有服务provider实例。key:IProvider实现类的class value:IProvider实例 赋值时机:在LogisticsCenter.completion中赋值providersIndex:存储所有provider服务元信息(实现类的class对象)。key:IProvider实现类的全类名 value:provider服务元信息 赋值时机:ARouter初始化中赋值。 备注:用于使用IProvider实现类class发起的获取服务的路由,例如ARouter.getInstance().navigation(HelloService.class)interceptorsIndex:存储所有拦截器实现类class对象。key:优先级 value:所有拦截器实现类class对象 赋值时机:是在ARouter初始化时收集到 interceptors,所有拦截器实例。是在ARouter初始化完成后立即创建其中groupsIndex、providersIndex、interceptorsIndex是ARouter初始化时就准备好的基础信息,为业务中随时发起路由操作(Activity跳转、服务获取、拦截器处理)做好准备。概念5:APT注解处理器ARouter使用注解处理器,自动生成路由帮助类:我们使用ARouter编译后,会在对应模块下自动生成以下类:这些类的生成规则都是通过APT在编译器自动生成的,关于APT在ARouter中的使用方式,后面会单独拿一节出来讲解:Android开源系列-组件化框架Arouter-(三)APT技术详解概念6:AGP插件ARouter使用了一个可选插件:"com.alibaba:arouter-register:1.0.2"使用这个插件可以在编译器在包中自动检测以及加载路由表信息,而不需要在运行启动阶段再使用包名去dex文件中加载,提高app启动效率关于这块的,后面会在:Android开源系列-组件化框架Arouter-(四)AGP插件详解有了以上几个概念做基础现在我们再到源码中去看看ARouter是如何跨模块运行起来的源码分析:首先我们来看路由过程:步骤1:初始化ARouterARouter.init(this)步骤2:注册Activity路由@Route(path = "/test/activity1", name = "测试用 Activity") public class Test1Activity extends BaseActivity { @Autowired int age = 10; protected void onCreate(Bundle savedInstanceState) { ARouter.getInstance().inject(this); } }步骤3:通过path启动对应的ActivityARouter.getInstance().build("/test/activity2").navigation();下面我们分别来分析以上过程:步骤1分析:ARouter.init(this)/** * Init, it must be call before used router. */ public static void init(Application application) { if (!hasInit) { logger = _ARouter.logger; _ARouter.logger.info(Consts.TAG, "ARouter init start."); hasInit = _ARouter.init(application); if (hasInit) { _ARouter.afterInit(); } _ARouter.logger.info(Consts.TAG, "ARouter init over."); } } 调用了_ARouter同名init方法,进入看看protected static synchronized boolean init(Application application) { mContext = application; LogisticsCenter.init(mContext, executor); logger.info(Consts.TAG, "ARouter init success!"); hasInit = true; mHandler = new Handler(Looper.getMainLooper()); return true; }内部初始化了一些mContext,mHandler以及字段信息最重要的是LogisticsCenter.init(mContext, executor):这句进入看看:/** * LogisticsCenter init, load all metas in memory. Demand initialization */ public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { try { //使用AGP插件进行路由表的自动加载 loadRouterMap(); //如果registerByPlugin被设置为true,说明使用的是插件加载,直接跳过 if (registerByPlugin) { logger.info(TAG, "Load router map by arouter-auto-register plugin."); } else { //如果是false,则调用下面步骤加载 Set<String> routerMap; // 如果是debug模式或者是新版本的,则每次都会去加载routerMap,这会是一个耗时操作 if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) { logger.info(TAG, "Run with debug mode or new install, rebuild router map."); // These class was generated by arouter-compiler. routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); if (!routerMap.isEmpty()) { context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply(); } PackageUtils.updateVersion(context); // Save new version name when router map update finishes. } else { //如果是其他的情况,则直接去文件中读取。 logger.info(TAG, "Load router map from cache."); routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>())); } //这里循环获取routerMap中的信息 for (String className : routerMap) { //如果className = "com.alibaba.android.arouter.routes.ARouter$$Root"格式,则将路由组信息添加到Warehouse.groupsIndex中 if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { // This one of root elements, load root. ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); //如果className = "com.alibaba.android.arouter.routes.ARouter$$Interceptors"格式,则将拦截器信息添加到Warehouse.interceptorsIndex中 } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) { // Load interceptorMeta ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex); //如果className = "com.alibaba.android.arouter.routes.ARouter$$Providers"格式,则将服务Provider信息添加到Warehouse.providersIndex中 } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) { // Load providerIndex ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex); } } } } catch (Exception e) { throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]"); } }总结_ARouter的init操作:1.优先使用插件加载路由表信息到仓库中,如果没有使用插件,则使用包名com.alibaba.android.arouter.routes去dex文件中查找对应的类对象查找到后,保存到sp文件中,非debug或者新版本的情况下,下次就直接使用sp文件中缓存的类信息即可。2.查找到对应的类文件后,使用反射调用对应的类的loadInto方法,将路由组,拦截器以及服务Provider信息加载到Warehouse仓库中继续看init方法中给的_ARouter.afterInitstatic void afterInit() { // Trigger interceptor init, use byName. interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation(); }找到/arouter/service/interceptor注解处@Route(path = "/arouter/service/interceptor") public class InterceptorServiceImpl implements InterceptorService 这里给ARouter创建了一个InterceptorServiceImpl服务的实例对象,后面讲到拦截器的时候会用到步骤2分析:注册Activity路由我们注册的Activity,Provider等路由信息,会在编译器被注解处理器处理后生成对应的路由表:路由表在步骤1中ARouter初始化的时候被加载到Warehouse中步骤3分析:通过path启动对应的ActivityARouter.getInstance().build("/test/activity2").navigation();这里我们拆分成三个部分:getInstance,build,navigation3.1:getInstancepublic static ARouter getInstance() { if (!hasInit) { throw new InitException("ARouter::Init::Invoke init(context) first!"); } else { if (instance == null) { synchronized (ARouter.class) { if (instance == null) { instance = new ARouter(); } } } return instance; } }做了init检查并创建了一个ARouter对象3.2:buildpublic Postcard build(String path) { return _ARouter.getInstance().build(path); } 调用了_ARouter的同名build方法 protected Postcard build(String path) { if (TextUtils.isEmpty(path)) { throw new HandlerException(Consts.TAG + "Parameter is invalid!"); } else { PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } return build(path, extractGroup(path), true); } } 1.使用PathReplaceService,可以替换原path为新的path 继续看build方法: protected Postcard build(String path, String group, Boolean afterReplace) { if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) { throw new HandlerException(Consts.TAG + "Parameter is invalid!"); } else { if (!afterReplace) { PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } } return new Postcard(path, group); } }看到这里创建了一个Postcard,传入path和group,对Postcard前面有讲解,这里不再重复3.3:navigation最后会走到_ARouter中的同名navigation方法中:protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { //预处理服务 PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class); if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) { // Pretreatment failed, navigation canceled. return null; } try { //完善PostCard信息 留个点1 LogisticsCenter.completion(postcard); } catch (NoRouteFoundException ex) { logger.warning(Consts.TAG, ex.getMessage()); if (debuggable()) { // Show friendly tips for user. runInMainThread(new Runnable() { @Override public void run() { Toast.makeText(mContext, "There's no route matched!\n" + " Path = [" + postcard.getPath() + "]\n" + " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show(); } }); } //没有找到路由信息,则直接返回callback.onLost if (null != callback) { callback.onLost(postcard); } else { // 没有callback则调用全局降级服务DegradeService的onLost方法 DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class); if (null != degradeService) { degradeService.onLost(context, postcard); } } return null; } //回调callback.onFound提醒用户已经找到path if (null != callback) { callback.onFound(postcard); } //非绿色通道走到拦截器中 if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR. interceptorService.doInterceptions(postcard, new InterceptorCallback() { /** * Continue process * * @param postcard route meta */ @Override public void onContinue(Postcard postcard) { _navigation(postcard, requestCode, callback); } /** * Interrupt process, pipeline will be destory when this method called. * * @param exception Reson of interrupt. */ @Override public void onInterrupt(Throwable exception) { if (null != callback) { callback.onInterrupt(postcard); } logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage()); } }); } else { //绿色通道直接调用_navigation return _navigation(postcard, requestCode, callback); } return null; }方法任务:1.预处理服务2.完善PostCard信息3.如果是非绿色通道,则使用拦截器处理请求4.调用_navigation处理这里我们看下第3点:拦截器处理interceptorService.doInterceptions{ public void onContinue(Postcard postcard) { _navigation(postcard, requestCode, callback); } public void onInterrupt(Throwable exception) { if (null != callback) { callback.onInterrupt(postcard); } } }如果被拦截回调callback.onInterrupt如果没有就执行_navigation方法进入interceptorService.doInterceptions看下:前面分析过interceptorService是InterceptorServiceImpl对象@Route(path = "/arouter/service/interceptor") public class InterceptorServiceImpl implements InterceptorService { private static boolean interceptorHasInit; private static final Object interceptorInitLock = new Object(); @Override public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) { if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) { checkInterceptorsInitStatus(); if (!interceptorHasInit) { callback.onInterrupt(new HandlerException("Interceptors initialization takes too much time.")); return; } LogisticsCenter.executor.execute(new Runnable() { @Override public void run() { //使用CancelableCountDownLatch计数器 CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size()); try { _execute(0, interceptorCounter, postcard); interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS); if (interceptorCounter.getCount() > 0) { // Cancel the navigation this time, if it hasn't return anythings. //拦截器处理超时 callback.onInterrupt(new HandlerException("The interceptor processing timed out.")); } else if (null != postcard.getTag()) { // Maybe some exception in the tag. //拦截器过程出现异常 callback.onInterrupt((Throwable) postcard.getTag()); } else { //继续执行下面任务onContinue callback.onContinue(postcard); } } catch (Exception e) { callback.onInterrupt(e); } } }); } else { callback.onContinue(postcard); } } private static void _execute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) { if (index < Warehouse.interceptors.size()) { IInterceptor iInterceptor = Warehouse.interceptors.get(index); iInterceptor.process(postcard, new InterceptorCallback() { @Override public void onContinue(Postcard postcard) { // Last interceptor excute over with no exception. counter.countDown(); //递归调用_execute执行拦截器 _execute(index + 1, counter, postcard); // When counter is down, it will be execute continue ,but index bigger than interceptors size, then U know. } @Override public void onInterrupt(Throwable exception) { // Last interceptor execute over with fatal exception. postcard.setTag(null == exception ? new HandlerException("No message.") : exception); // save the exception message for backup. counter.cancel(); // Be attention, maybe the thread in callback has been changed, // then the catch block(L207) will be invalid. // The worst is the thread changed to main thread, then the app will be crash, if you throw this exception! // if (!Looper.getMainLooper().equals(Looper.myLooper())) { // You shouldn't throw the exception if the thread is main thread. // throw new HandlerException(exception.getMessage()); // } } }); } } }拦截器总结:1.使用计数器对拦截器技术,执行开始计数器+1,执行结束计数器-1,如果拦截器执行时间到,计数器数大于0,则说明还有未执行完成的拦截器,这个时候就超时了退出2.拦截器执行使用递归的方式进行3.拦截器执行完成继续执行_navigation方法我们来看_navigation方法:private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) { final Context currentContext = postcard.getContext(); switch (postcard.getType()) { case ACTIVITY: // Build intent final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); // Set flags. int flags = postcard.getFlags(); if (0 != flags) { intent.setFlags(flags); } // Non activity, need FLAG_ACTIVITY_NEW_TASK if (!(currentContext instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // Set Actions String action = postcard.getAction(); if (!TextUtils.isEmpty(action)) { intent.setAction(action); } // Navigation in main looper. runInMainThread(new Runnable() { @Override public void run() { startActivity(requestCode, currentContext, intent, postcard, callback); } }); break; case PROVIDER: return postcard.getProvider(); case BOARDCAST: case CONTENT_PROVIDER: case FRAGMENT: Class<?> fragmentMeta = postcard.getDestination(); try { Object instance = fragmentMeta.getConstructor().newInstance(); if (instance instanceof Fragment) { ((Fragment) instance).setArguments(postcard.getExtras()); } else if (instance instanceof android.support.v4.app.Fragment) { ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras()); } return instance; } catch (Exception ex) { logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace())); } case METHOD: case SERVICE: default: return null; } return null; } 这个方法其实就是根据PostCard的type来处理不同的请求了1.Activity,直接跳转2.Fragment,Provider,BroadcaseReceiver和ContentProvider,直接返回类的实例对象。整个过程我们就基本了解了。上面还留了一个点:留的点1:ARouter是如何完善PostCard信息看LogisticsCenter.completion(postcard);进入这个方法:public synchronized static void completion(Postcard postcard) { if (null == postcard) { throw new NoRouteFoundException(TAG + "No postcard!"); } //去Warehouse.routes去取路由元数据,开始肯定是没有的 RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath()); //没获取到 if (null == routeMeta) { // Maybe its does't exist, or didn't load. //判断Warehouse.groupsIndex路由组中是否有这个group if (!Warehouse.groupsIndex.containsKey(postcard.getGroup())) { throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]"); } else { try { //动态添加路由元信息到路由中 addRouteGroupDynamic(postcard.getGroup(), null); } catch (Exception e) { } //重新加载。这个时候就会有路由元信息了 completion(postcard); // Reload } } else { //给postcard设置目的地,设置类型,设置优先级,设置Extra等信息 postcard.setDestination(routeMeta.getDestination()); postcard.setType(routeMeta.getType()); postcard.setPriority(routeMeta.getPriority()); postcard.setExtra(routeMeta.getExtra()); Uri rawUri = postcard.getUri(); ... switch (routeMeta.getType()) { case PROVIDER: // if the route is provider, should find its instance // Its provider, so it must implement IProvider Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination(); IProvider instance = Warehouse.providers.get(providerMeta); if (null == instance) { // There's no instance of this provider IProvider provider; try { provider = providerMeta.getConstructor().newInstance(); provider.init(mContext); Warehouse.providers.put(providerMeta, provider); instance = provider; } catch (Exception e) { logger.error(TAG, "Init provider failed!", e); throw new HandlerException("Init provider failed!"); } } postcard.setProvider(instance); postcard.greenChannel(); // Provider should skip all of interceptors break; case FRAGMENT: postcard.greenChannel(); // Fragment needn't interceptors default: break; } } } 进入addRouteGroupDynamic public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { if (Warehouse.groupsIndex.containsKey(groupName)){ // If this group is included, but it has not been loaded // load this group first, because dynamic route has high priority. Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes); Warehouse.groupsIndex.remove(groupName); } // cover old group. if (null != group) { group.loadInto(Warehouse.routes); } }看上面代码可知:**数据完善过程是通过组名group去groupsIndex获取对应的组的class对象,然后调用class对象的loadInto方法,将路由元数据加载到Warehouse.routes然后重新调用completion完善方法去Warehouse.routes中取出路由信息并加载到PostCard中,这样PostCard中就获取到了目的地址信息。**下面我画了一张图描述了上面的调用过程一图胜千言总结本文先介绍了ARouter使用过程中 的一些基本概念,理解了这些概念后,我们再从使用步骤触发,对每个使用节点进行了介绍。最后使用一张图总结了整个使用原理过程:这里我们还有一些悬念:1.ARouter帮助类是如何生成的,这里使用到了APT注解处理器的技术关于APT我们会在下一章:Android开源系列-组件化框架Arouter-(三)APT技术详解这里还有个有趣的现象,我们在调用路由表加载的时候:使用了loadRouterMap加载,但是查看里面代码:private static void loadRouterMap() { registerByPlugin = false; // auto generate register code by gradle plugin: arouter-auto-register // looks like below: // registerRouteRoot(new ARouter..Root..modulejava()); // registerRouteRoot(new ARouter..Root..modulekotlin()); }居然是空的。。呃呃呃没关系看注解:auto generate register code by gradle plugin: arouter-auto-register可以看到这里使用了arouter-auto-register插件中自动生成注册代码的方式:这里其实就是使用到了字节码插庄技术,动态添加了代码,这里留到:Android开源系列-组件化框架Arouter-(四)AGP插件详解好了,本篇就到这里了。> 持续输出中。。你的点赞,关注是我最大的动力。​
文章
存储  ·  缓存  ·  ARouter  ·  自然语言处理  ·  编译器  ·  API  ·  Android开发  ·  网络架构
2023-02-14
Android开源系列-组件化框架Arouter-(一)使用方式详解
Hi,我是小余。本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!前言:最近组里需要进行组件化框架的改造,用到了Arouter这个开源框架,为了更好的对项目进行改造,笔者花了一些时间去了解了下ArouterARouter是阿里巴巴团队在17年初发布的一款针对组件化模块之间无接触通讯的一个开源框架,经过多个版本的迭代,现在已经非常成熟了。ARouter主要作用:组件间通讯,组件解耦,路由跳转,涉及到我们常用的Activity,Provider,Fragment等多个场景的跳转接下来笔者会以几个阶段来对Arouter进行讲解:Android开源系列-组件化框架Arouter-(一)使用方式详解Android开源系列-组件化框架Arouter-(二)基本原理详解Android开源系列-组件化框架Arouter-(三)APT技术详解Android开源系列-组件化框架Arouter-(三)AGP插件详解这篇文章是Arouter的第一篇:基本使用方式前期准备我们先来新建一个项目:项目架构如下:app:app模块build.gradle:dependencies { ... implementation 'com.alibaba:arouter-api:1.5.2' implementation project(':module_java') implementation project(':module_kotlin') //annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' }引入arouter的核心api和引用需要调用的模块即可这里如果需要使用依赖注入的方式查找class对象,则需要添加声明注解处理器:annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'module_java:java语言lib模块build.gradle:android { javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"] } } } dependencies { ... implementation 'com.alibaba:arouter-api:1.5.2' annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' }1.引入arouter-api核心api和arouter-compiler注解处理器api2.设置主机处理器的外部参数AROUTER_MODULE_NAME和AROUTER_GENERATE_DOC,这两个参数会在编译器生成Arouter路由表的时候使用到module_kotlin:kotli语言lib模块build.gradle:apply plugin: 'kotlin-kapt' kapt { arguments { arg("AROUTER_MODULE_NAME", project.getName()) } } dependencies { implementation 'com.alibaba:arouter-api:1.5.2' kapt 'com.alibaba:arouter-compiler:1.5.2' }这里的设置方式和java一样,区别:java使用关键字annotationProcessor,而kotlin使用kapt。同步项目后。就可以使用Arouter的api啦。ARouter api1.初始化Arouter建议在application中初始化:ARouter.init(this);2.日志开启ARouter.openLog();3.打开调试模式ARouter.openDebug();调试模式不是必须开启,但是为了防止有用户开启了InstantRun,忘了开调试模式,导致无法使用Demo,如果使用了InstantRun,必须在初始化之前开启调试模式,但是上线前需要关闭,InstantRun仅用于开发阶段,线上开启调试模式有安全风险,可以使用BuildConfig.DEBUG来区分环境4.Activity模块的调用:4.1:注解声明@Route(path = "/gtest/test") //或者@Route(path = "/gtest/test",group = "gtest") public class ArouterTestActivity extends AppCompatActivity { }4.2:组件跨模块调用ARouter.getInstance().build("/gtest/test").navigation();记住:如果注解没有指明group,arouter默认会以path的第一个节点gtest为组名进行分组5.属性依赖注入5.1:注解声明@Route(path = "/test/activity1", name = "测试用 Activity") public class Test1Activity extends BaseActivity { @Autowired int age = 10; @Autowired int height = 175; @Autowired(name = "boy", required = true) boolean girl; @Autowired char ch = 'A'; @Autowired float fl = 12.00f; @Autowired double dou = 12.01d; @Autowired TestSerializable ser; @Autowired TestParcelable pac; @Autowired TestObj obj; @Autowired List<TestObj> objList; @Autowired Map<String, List<TestObj>> map; private long high; @Autowired String url; @Autowired HelloService helloService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test1); ARouter.getInstance().inject(this); } } 5.2:注解跨模块调用ARouter.getInstance().build("/test/activity1") .withString("name", "老王") .withInt("age", 18) .withBoolean("boy", true) .withLong("high", 180) .withString("url", "https://a.b.c") .withSerializable("ser", testSerializable) .withParcelable("pac", testParcelable) .withObject("obj", testObj) .withObject("objList", objList) .withObject("map", map) .navigation();传入的Object需要使用Serializable序列化public class TestSerializable implements Serializable { public String name; public int id; public TestSerializable() { } public TestSerializable(String name, int id) { this.name = name; this.id = id; } }注意点:1.常用数据类型直接使用:Autowired注解即可2.如果需要指定外部传入的数据key:可以使用name指定,如果这个字段是必须传入的,则需要指定required = true,外部未传入可能会报错3.传入自定义类的属性则需要先对自定义进行序列化,且需要指定一个SerializationService:这个类是用于Object对象和json进行转化用@Route(path = "/yourservicegroupname/json") public class JsonServiceImpl implements SerializationService { @Override public void init(Context context) { } @Override public <T> T json2Object(String text, Class<T> clazz) { return JSON.parseObject(text, clazz); } @Override public String object2Json(Object instance) { return JSON.toJSONString(instance); } @Override public <T> T parseObject(String input, Type clazz) { return JSON.parseObject(input, clazz); } } 6.Service模块的调用6.1:声明接口,其他组件通过接口来调用服务public interface HelloService extends IProvider { String sayHello(String name); } 6.2:实现接口@Route(path = "/yourservicegroupname/hello", name = "测试服务") public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "hello, " + name; } @Override public void init(Context context) { } }6.3:使用接口6.3.1:使用依赖查找的方式发现服务,主动去发现服务并使用,下面两种方式分别是byName和byType1.使用class类型查找helloService3 = ARouter.getInstance().navigation(HelloService.class); helloService3.sayHello("Vergil");2.使用path查找helloService4 = (HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation(); helloService4.sayHello("Vergil");6.3.2:(推荐)使用依赖注入的方式发现服务,通过注解标注字段,即可使用,无需主动获取@Autowired HelloService helloService; @Autowired(name = "/yourservicegroupname/hello") HelloService helloService2; helloService.sayHello("mikeaaaa"); helloService2.sayHello("mikeaaaa");7.Fragment和BOARDCAST以及CONTENT_PROVIDER使用方式和Activity类似,但是这三者都是直接获取对应的Class类对象,拿到对象后,可以操作对象,这里不再讲解8.拦截器的使用拦截器就是在跳转的过程中,设置的对跳转的检测和预处理等操作。8.1:拦截器声明可以添加拦截器的优先级,优先级越高,越优先执行@Interceptor(priority = 7) public class Test1Interceptor implements IInterceptor { /** * The operation of this interceptor. * * @param postcard meta * @param callback cb */ @Override public void process(final Postcard postcard, final InterceptorCallback callback) { if ("/test/activity4".equals(postcard.getPath())) { // 这里的弹窗仅做举例,代码写法不具有可参考价值 final AlertDialog.Builder ab = new AlertDialog.Builder(postcard.getContext()); ab.setCancelable(false); ab.setTitle("温馨提醒"); ab.setMessage("想要跳转到Test4Activity么?(触发了\"/inter/test1\"拦截器,拦截了本次跳转)"); ab.setNegativeButton("继续", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { callback.onContinue(postcard); } }); ab.setNeutralButton("算了", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { callback.onInterrupt(null); } }); ab.setPositiveButton("加点料", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { postcard.withString("extra", "我是在拦截器中附加的参数"); callback.onContinue(postcard); } }); MainLooper.runOnUiThread(new Runnable() { @Override public void run() { ab.create().show(); } }); } else { callback.onContinue(postcard); } } /** * Do your init work in this method, it well be call when processor has been load. * * @param context ctx */ @Override public void init(Context context) { Log.e("testService", Test1Interceptor.class.getName() + " has init."); } }可以设置绿色通道模式:greenChannel()不做拦截器处理9.监听跳转状态.navigation(this, new NavCallback() { @Override public void onArrival(Postcard postcard) { Log.d("ARouter", "onArrival"); Log.d("ARouter", "成功跳转了"); } @Override public void onInterrupt(Postcard postcard) { super.onInterrupt(postcard); Log.d("ARouter", "被拦截了"); } @Override public void onFound(Postcard postcard) { super.onFound(postcard); Log.d("ARouter", "onFound:path:"+postcard.getPath()); Log.d("ARouter", "找到了对应的path"); } @Override public void onLost(Postcard postcard) { super.onLost(postcard); Log.d("ARouter", "onLost:path:"+postcard.getPath()); Log.d("ARouter", "没找到了对应的path"); } });10.自定义全局降级策略// 实现DegradeService接口,并加上一个Path内容任意的注解即可 @Route(path = "/xxx/xxx") public class DegradeServiceImpl implements DegradeService { @Override public void onLost(Context context, Postcard postcard) { // do something. } @Override public void init(Context context) { } }11.动态注册路由信息适用于部分插件化架构的App以及需要动态注册路由信息的场景,可以通过 ARouter 提供的接口实现动态注册 路由信息,目标页面和服务可以不标注 @Route 注解,注意:同一批次仅允许相同 group 的路由信息注册ARouter.getInstance().addRouteGroup(new IRouteGroup() { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/dynamic/activity", // path RouteMeta.build( RouteType.ACTIVITY, // 路由信息 TestDynamicActivity.class, // 目标的 Class "/dynamic/activity", // Path "dynamic", // Group, 尽量保持和 path 的第一段相同 0, // 优先级,暂未使用 0 // Extra,用于给页面打标 ) ); } });12:使用自己的日志工具打印日志ARouter.setLogger();13.使用自己提供的线程池ARouter.setExecutor();14.重写跳转URL// 实现PathReplaceService接口,并加上一个Path内容任意的注解即可 @Route(path = "/xxx/xxx") // 必须标明注解 public class PathReplaceServiceImpl implements PathReplaceService { /** * For normal path. * * @param path raw path */ String forString(String path) { return path; // 按照一定的规则处理之后返回处理后的结果 } /** * For uri type. * * @param uri raw uri */ Uri forUri(Uri uri) { return url; // 按照一定的规则处理之后返回处理后的结果 } }15.使用agp插件动态路由表加载1.在项目级build.gradle中:buildscript { dependencies { classpath "com.alibaba:arouter-register:$arouter_register_version" } }2.在模块级app下引入路由表自动加载插件id 'com.alibaba.arouter'同步代码后,在编译期会自动进行路由表的加载,可以加快arouter的第一次初始化时间,为什么是第一次呢,因为arouter初始化会缓存路由表信息到文件中。Q&A1."W/ARouter::: ARouter::No postcard![ ]"这个Log正常的情况下也会打印出来,如果您的代码中没有实现DegradeService和PathReplaceService的话,因为ARouter本身的一些功能也依赖 自己提供的Service管理功能,ARouter在跳转的时候会尝试寻找用户实现的PathReplaceService,用于对路径进行重写(可选功能),所以如果您没有 实现这个服务的话,也会抛出这个日志推荐在app中实现DegradeService、PathReplaceService2."W/ARouter::: ARouter::There is no route match the path [/xxx/xxx], in group xxx"通常来说这种情况是没有找到目标页面,目标不存在如果这个页面是存在的,那么您可以按照下面的步骤进行排查a.检查目标页面的注解是否配置正确,正确的注解形式应该是 (@Route(path="/test/test"), 如没有特殊需求,请勿指定group字段,废弃功能)b.检查目标页面所在的模块的gradle脚本中是否依赖了 arouter-compiler sdk (需要注意的是,要使用apt依赖,而不是compile关键字依赖)c.检查编译打包日志,是否出现了形如 ARouter::�Compiler >>> xxxxx 的日志,日志中会打印出发现的路由目标d.启动App的时候,开启debug、log(openDebug/openLog), 查看映射表是否已经被扫描出来,形如 D/ARouter::: LogisticsCenter has already been loaded, GroupIndex[4],GroupIndex > 03.开启InstantRun之后无法跳转(高版本Gradle插件下无法跳转)?因为开启InstantRun之后,很多类文件不会放在原本的dex中,需要单独去加载,ARouter默认不会去加载这些文件,因为安全原因,只有在开启了openDebug之后 ARouter才回去加载InstantRun产生的文件,所以在以上的情况下,需要在init之前调用openDebug4.TransformException:java.util.zip.ZipException: duplicate entry ....ARouter有按组加载的机制,关于分组可以参考 6-1 部分,ARouter允许一个module中存在多个分组,但是不允许多个module中存在相同的分组,会导致映射文件冲突5.Kotlin类中的字段无法注入如何解决?首先,Kotlin中的字段是可以自动注入的,但是注入代码为了减少反射,使用的字段赋值的方式来注入的,Kotlin默认会生成set/get方法,并把属性设置为private 所以只要保证Kotlin中字段可见性不是private即可,简单解决可以在字段上添加 @JvmField6.通过URL跳转之后,在intent中拿不到参数如何解决?需要注意的是,如果不使用自动注入,那么可以不写 ARouter.getInstance().inject(this),但是需要取值的字段仍然需要标上 @Autowired 注解,因为 只有标上注解之后,ARouter才能知道以哪一种数据类型提取URL中的参数并放入Intent中,这样您才能在intent中获取到对应的参数7.新增页面之后,无法跳转?ARouter加载Dex中的映射文件会有一定耗时,所以ARouter会缓存映射文件,直到新版本升级(版本号或者versionCode变化),而如果是开发版本(ARouter.openDebug()), ARouter 每次启动都会重新加载映射文件,开发阶段一定要打开 Debug 功能参考Arouter官方文档
文章
缓存  ·  JSON  ·  ARouter  ·  安全  ·  Java  ·  编译器  ·  API  ·  Android开发  ·  数据格式  ·  Kotlin
2023-02-14
【已解决】Error: Invariant failed: You should not use <withRouter(RouterView) /> outside a <Router>
Error: Invariant failed: You should not use <withRouter(RouterView) /> outside a <Router>问题代码:import React, {Component} from 'react'; import {BrowserRouter as Router, Route, Switch, withRouter} from 'react-router-dom'; import * as A from '../a'; import * as B from '../b'; class RouterView extends Component{ render() { return ( <Router> <Switch> <Route path="/" component={A}/> <Route path="/b" component={B}/> </Switch> </Router> ); } } export default withRouter(RouterView)解决:将BrowserRouter摘出来即可over。。。
文章
ARouter
2023-01-10
Android组件化演进-第一篇
背景近年来,组件化一直是业界积极探索和实践的方向,越来越多的公司使用组件化来构建项目,我们公司在组件化实践方向也有了一些实践,但目前还没有一个标准,这也是我们为什么要整理这个文档的目的,确定一下组件化的方案,为未来的复杂业务助力。组件化带来的优势首先组件化的一些优势是我们应用它的核心价值,那么都有哪些优势呢?大致总结如下:1.加快项目编译速度,提高开发效率,因为模块可以独立编译、测试、打包和部署2.提高组件的复用3.避免了模块之间的交叉依赖,做到低耦合、高内聚4.引用的第三方库代码统一管理,避免版本不统一,减少引入冗余库组件化目标首先设立一个目标,来保证我们后续不偏离方向,不能为了组件化而组件化,复杂的设计必定会造成大量的学习成本,反而会降低开发效率,其实我们设计组件化的初衷也是为了能更加简单化,而且目标促使我们价值观统一,大家朝一个方向努力,共同创造价值。 如下:1.生成组件化模版项目,能够快速的迭代新项目 2.组件化脚手架,可定制化组件化规范1.代码尽可能的解耦对项目进行模块拆分,模块分为两种类型,一种是功能组件模块,封装一些公共的方法服务等,作为依赖库对外提供;另一种是业务组件模块,专门处理业务逻辑等功能,这些业务组件模块最终负责组装APP。2.每个组件都可单独运行每个组件都是高度内聚的,是一个完整的整体通过 Gradle脚本配置方式,进行不同环境切换3.组件间通信通过路由的方式进行通信(路由框架待选择)4.规范组件生命周期生命周期指的是组件在应用中存在的时间,组件是否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等5.严格限制公共基础组件的增长随着开发不断进行,要注意不要往基础公共组件加入太多内容。而是应该减小体积!倘若是基础组件过于庞大,那么运行组件也是比较缓慢的组件化常见问题以及解决方案1.通信 通信这个需要考虑两个方面,一个是同进程之间,一个是跨进程之间,那么就会有两种解决方案如下: 同进程: 接口依赖调用 跨进程:github.com/iqiyi/Andro…Andromeda提供了接口式的组件间通信管理,包括同进程的本地接口调用和跨进程接口调用。2.初始化developer.android.com/topic/libra… google官方github.com/bingoogolap… AppInit 用于解决美团收银 B 端 App 在业务演进过程中的实际问题3.代码隔离,如何处理依赖关系 这里推荐美团的组件化方案:把每个业务组件都拆分成了一个Export Module和Implement Module。这样的话,如果Module A需要调用Module B提供的接口,同时Module B需要调用Module A的接口,只需要Module A依赖Module B Export,Module B依赖Module A Export就可以了。4.页面跳转ARouterNavigationNavigation这里单独拿出来讲一下,其实在用到Navigation时有很多好处,如下:可以实现单Activity+多Fragment架构,好处就在于Fragment比Activity更轻量级,从而减少内存占用,其实Activity与Activity之间存在进程通信以及其他开销。结合Flutter这种框架,我们发现,它的一个页面由多个Widget组成,页面更加灵活,同样的Fragment也是一样,它需要放在Activity中,且灵活可配。官方支持,放心使用Navigation 组件化项目:github.com/VMadalin/an…组件化架构图设计以上设计图,遵循组件化规范。详细介绍请往下看。模块介绍1.业务组件这一层,主要是能否独立业务的组件,如登陆模块可以抽离出一个完整的组件,并可以打包出独立的app包。2.功能组件 • Common:包含公共业务接口定义,数据模型,数据库相关。• 自定义View:一些特殊样式,或者组合使用的自定义View • 日志:日志框架组件,如logan • 推送:push消息等组件,独立于业务• 分享:分享SDK • 视频:视频相关功能组件• 即时通讯:聊天实时消息相关组件3.Core基础层:网络请求(采用 Retrofit+协程)图片加载(策略模式,Glide 与 Picasso或者coil 之间可以切换)基类 Adapter 的封装(支持 item动画、多布局item、下拉和加载更多、item点击事件)通用的工具类,Kotlin版本的动态扩展(Context、Fragment、Activity、Service、Intent等)协程封装其他等等4.BuildSrc + Composite Builds Module BuildSrc的运用更好的Gradle依赖关系管理,Composite Builds提升构建速度,编写自定义插件。且统一管理框架版本信息,防止组件之间框架依赖版本冲突问题组件化脚手架介绍首先说下为什么要设计脚手架,这个源于SpringBoot脚手架的灵感,因为组件化后,项目各个模块相对独立,如果我们开发新项目就会遇到各种各样的功能,且符合我们的实际场景需求,如: 咨询师没有数据库,而其他app都需要数据库存储。小猫大部分是离线处理,而ESA又都是后台处理,每个业务都有不同的特点,所以我们想做一个可定制化的组件化项目,而不是一成不变。 脚手架的开发也是我们设计的一个目标,设计如下:此脚手架我们做的是在线的,傻瓜式操作,只需要在页面中选择已经配置好的模版,可定制如下:包名语言最低兼容版本架构选择组件依赖当然这个页面的设计还不够完善,没有体现可以依赖三方框架,这个我们也会在未来的设计中加入进去,结合我们公司实际的业务场景,来做到定制化生成。讨论结论目前待确认框架选型如下:不使用单Activity+多Fragment架构(此点考虑到C端已经在用多Activity,且单Activity并没有更好的实践结果,所以保持原有架构)数据库考虑升级为moshi,或者kotlin官方serialization,去除Gson逻辑,后续专门做专题讨论网络层抛弃Rxjava方式,未来用okhttp+协程方式,后续专门做专题讨论页面跳转依然适用Arouter组件初始化目前有贤论大佬自开发方案和google官方或美团相关方案,待后续专门专题讨论gradle build方式更新为 kotlin dsl gradle参考项目github.com/hegaojian/J…github.com/goldze/MVVM…github.com/getActivity…github.com/android/sun…github.com/KunMinX/Jet…github.com/skydoves/Po…github.com/VMadalin/an…github.com/qingmei2/MV…
文章
存储  ·  ARouter  ·  前端开发  ·  Java  ·  数据库  ·  开发工具  ·  Android开发  ·  Kotlin
2023-01-07
Android 组件化(二)注解与注解处理器、组件通讯
前言  在上一篇文章中,我们完成了组件的创建、gradle统一管理、组件模式管理和切换,那么这一篇文章,我们需要做的就是组件之间的通讯了。正文  组件化是将原来复杂的App分解成一个个组件,在调试运行的时候各个组件之间可以单独测试,而在打包的时候需要将其他的组件打包在app组件中,作为一个apk时,肯定会有不同组件之前的通讯,举一个简单的例子,我们在app组件中写一个启动页,如果之前用户有登录过,则进入personal组件的PersonalActivity,如果没有登录过则进入login组件的LoginActivity。而LoginActivity登录成功之后要进入personal组件的PersonalActivity,要实现这样一个简单的例子,我们需要做的就是组件之间的相互通讯,而在通讯之前首先要找到通讯的目标。这里需要用到编译时技术,在之前的学习注解和注解处理器中我提到过,而组件中用到的就是类似于ARouter的路由框架,下面我们简单来写一下。一、注解  还是之前的StudyComponent项目,这里我们再创建一个Module,这里要注意创建的是java Module,注意我选择的模式。创建Java Module的时候需要创建一个默认的类,这里我们改变一下类名为BindPath,稍后还将改成注解类。① 创建注解类Module创建好之后修改这个BindPath,代码如下:@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface BindPath { String value(); }这个库里面代码其实就这么一点,那么我们怎么使用这个注解呢?② 使用注解类  要使用这个注解类,首先要依赖这个注解库,那么我们之前所写的config.gradle就排上用场了,还记得它的作用吗?管理工程中所有的gradle,那么添加依赖库自然是可以的,何况我们之前还添加过,还记得吗?帮你回忆一下,我们的app、login、personal都需要依赖basic库,之前通过config.gradle中配置可以一步到位,那么这个注解库也是一样的道理,所以我们只需要改动一个地方就可以完成所有组件对于注解库的依赖,现在Sync Now同步一下就可以了,我们分别在app、login、personal组件中使用这个注解,如下图所示:  注意看,这里在Activity上面添加注解,然后里面的值就是当前的模块名斜杠再加上当前的类名,好了下面你可以暂且运行一下,看看报不报错,无论报不报错都继续往后走。二、路由  这里我们做注解是要标记一个Activity,然后保存到路由中,那么这个路由就负责组件之间通讯,这里的路由,你可以单独创建一个library库,也可以写在basic中,这里我就写在basic模块中,在com.llw.basic包下新建一个router包,router包下新建IRouter接口,代码如下:public interface IRouter { void putActivity(); }然后我们再创建一个ARouter类,代码如下:public class ARouter { @SuppressLint("StaticFieldLeak") private static final ARouter aRouter = new ARouter(); private final Map<String, Class<? extends Activity>> map; private Context context; private ARouter() { map = new HashMap<>(); } public static ARouter getInstance() { return aRouter; } /** * 初始化 */ public void init(Context context) { this.context = context; //执行生成的工具类中的方法 将Activity的类对象加入到路由表中 List<String> classNames = getClassName(); for (String className : classNames) { try { Class<?> utilClass = Class.forName(className); if (IRouter.class.isAssignableFrom(utilClass)) { IRouter iRouter = (IRouter) utilClass.newInstance(); iRouter.putActivity(); } } catch (Exception e) { e.printStackTrace(); } } } /** * 添加Activity * @param key 注解中的值 例如 "main/MainActivity" * @param clazz 目标Activity */ public void addActivity(String key, Class<? extends Activity> clazz) { //如果Key不会空,activity不为空,且map中没有这个key if (key != null && clazz != null && !map.containsKey(key)) { map.put(key, clazz); } } /** * 跳转Activity * @param key 注解中的值 例如 "main/MainActivity" */ public void jumpActivity(String key) { jumpActivity(key, null); } /** * 跳转Activity 带参数 * @param key 注解中的值 例如 "main/MainActivity" * @param bundle 参数包 */ public void jumpActivity(String key, Bundle bundle) { Class<? extends Activity> aClass = map.get(key); if (aClass == null) { return; } Intent intent = new Intent(context, aClass); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (bundle != null) { intent.putExtras(bundle); } context.startActivity(intent); } /** * 通过包名获取这个包下面的所有的类名 */ private List<String> getClassName() { //创建一个class对象的集合 List<String> classList = new ArrayList<>(); try { DexFile df = new DexFile(context.getPackageCodePath()); Enumeration<String> entries = df.entries(); while (entries.hasMoreElements()) { String className = entries.nextElement(); if (className.contains("com.llw.util")) { classList.add(className); } } } catch (IOException e) { e.printStackTrace(); } return classList; } }  这里面的代码就是存放和使用Activity、组件之间跳转Activity操作。现在注解和路由都有了,要使我们的注解能够生效,还需要一个注解处理器,顾名思义就是用来处理被注解的类型。三、注解处理器这里我们再创建一个Module,这里要注意创建的是java Module,注意我选择的模式。这里修改模块名和包名和类名,等待注解处理器这个库创建完成。① 添加依赖  这里的注解处理器相较于注解稍稍有一些不同,首先我们改动一下注解处理器模块的build.gradle,添加代码如下:dependencies { implementation 'com.google.auto.service:auto-service:1.0-rc7' annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7' implementation 'com.squareup:javapoet:1.13.0' implementation project(path: ':annotation') }添加位置如下图所示  这里前面两句依赖是添加注解处理器,然后就是生成编译时文件需要用到的库,最后就是依赖注解库,这里和之前稍有不同,我们不使用config.gradle中的配置,这也是注解处理器的特殊之处,添加完依赖之后点击Sync Now。② 注解处理器编写同步完成之后我们可以编写AnnotationCompiler类的代码,如下所示:@AutoService(Processor.class) public class AnnotationCompiler extends AbstractProcessor { // 定义用来生成APT目录下面的文件的对象(例如:ActivityRouterUtil1668396026324) Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); filer = processingEnv.getFiler(); } /** * 支持的注解类型 */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new HashSet<>(); types.add(BindPath.class.getCanonicalName()); return types; } /** * 支持版本 */ @Override public SourceVersion getSupportedSourceVersion() { return processingEnv.getSourceVersion(); } /** * 通过注解处理器处理注解,生成代码到build文件夹中 */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //获取注解 例如 :@BindPath("main/MainActivity") Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindPath.class); Map<String, String> map = new HashMap<>(); for (Element element : elementsAnnotatedWith) { TypeElement typeElement = (TypeElement) element; //key为注解的Activity 例如:MainActivity String key = typeElement.getQualifiedName().toString() + ".class"; //value为注解方法中的值 例如:"main/MainActivity" String value = typeElement.getAnnotation(BindPath.class).value(); map.put(key, value); } makefile(map); return false; } private void makefile(Map<String, String> map) { if (map.size() > 0) { //定义编译时类生成时的包名 String packageName = "com.llw.util"; //定义处理器的包名 String routerPackageName = "com.llw.basic.router"; //获取接口名IRouter ClassName interfaceName = ClassName.get(routerPackageName, "IRouter"); //获取类名 ARouter ClassName className = ClassName.get(routerPackageName, "ARouter"); //创建类构造器,例如ActivityRouterUtil 加上时间戳是为了防止生成的编译时类名重复报错 TypeSpec.Builder classBuilder = TypeSpec.classBuilder("ActivityRouterUtil" + System.currentTimeMillis()) //添加修饰符 public .addModifiers(Modifier.PUBLIC) //添加实现接口,例如 implements IArouter .addSuperinterface(interfaceName); //创建方法构造器 方法名putActivity() MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("putActivity") //添加注解 .addAnnotation(Override.class) //添加修饰符 .addModifiers(Modifier.PUBLIC); //这里遍历是为了给每一个添加了注解进行代码生成 for (String activityName : map.keySet()) { String value = map.get(activityName); //例如 com.llw.arouter.ARouter.getInstance().addActivity("login/LoginActivity",com.llw.login.LoginActivity.class); methodBuilder.addStatement("$L.getInstance().addActivity($S, $L)", className, value, activityName); } //在类构造器中添加方法 classBuilder.addMethod(methodBuilder.build()); try { //最后写入文件 JavaFile.builder(packageName, classBuilder.build()) .build() .writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } } }代码中的注解已经很清楚了,就是生成一个编译时类,编译时类的代码如下图:③ 注解处理器使用要使这个注解处理器生效,需要分别在app、login、personal的build.gradle中的denpendencies{}下添加如下所示代码:annotationProcessor project(path: ':annotation_compiler')添加的位置如下面三图所示:添加好之后Sync Now,然后运行一下,运行之后在app模块下会生成一个build文件夹,然后层层打开,最终如下图所示:我们刚才的AnnotationCompiler中所写的代码就是为了生成这个编译时文件,如果你没有找到这个文件,点击这个刷新按钮,刷新一下项目文件。  Android Studio有时候文件检查不是很及时,所以手动刷新一下,看有没有生成这个文件。如果文件生成了,那么你再依次检查一下login、personal组件中的build文件夹中有没有生成相关文件。四、使用路由  下面要做的就是能够进行组件之间的Activity跳转,例如从app的MainActivity跳转到login的LoginActivity,再从LoginActivity跳转到personal的PersonalActivity,要做到这一步我们需要对路由进行初始化,可以在basic模块中的BaseApplication中完成。  而为了使BaseApplication生效,我们需要在各自组件中的AndroidManifest.xml进行注册,实际上我们各个组件应该自己写一个Application类继承自BaseApplication,但是目前我们的功能比较简单,所以就不这样写了,直接使用BaseApplication进行注册即可,下面我在app组件的AndroidManifest.xml中注册。其他的组件自己去注册一下。① 页面跳转然后我们在MainActivity中添加这样一行代码。ARouter.getInstance().jumpActivity("login/LoginActivity");这里意图很明显,我要跳转到LoginActivity,那么我们在LoginActivity的onCreate()方法中添加ARouter.getInstance().jumpActivity("personal/PersonalActivity");我相信你知道怎么添加这行代码,这样就能跳转到PersonalActivity中了,下面我们运行测试一下。  这里可以看到,直接就进入了PersonalActivity,但是你会发现还有LoginActivity的Toast显示出来,这证明确实是从MainActivity过来的,最终到达PersonalActivity,你要是延时跳转那就会很明显,自行尝试吧。② 页面带参跳转修改一下LoginActivity的onCreate()方法,进行传参,代码如下: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); showMsg("LoginActivity"); Bundle bundle = new Bundle(); bundle.putString("data","Very Good!"); ARouter.getInstance().jumpActivity("personal/PersonalActivity", bundle); }然后修改PersonalActivity中的onCreate()方法,接收参数,代码如下: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_personal); String data = getIntent().getExtras().getString("data"); if (data != null) { showMsg(data); } }下面重新运行一下:OK,现在页面的组件通讯就初步完成了。五、源码欢迎 Star 和 Fork源码地址:StudyComponent
文章
ARouter  ·  Java  ·  Android开发
2022-11-16
来开源吧!发布开源组件到 MavenCentral 仓库超详细攻略
前言当一个开发者的水平提升到一定程度时,会有由内向外输出价值的需求,包括发布开源项目。而要发布开源组件,则需要将组件发布到公开的远程仓库,如 Jitpack、JenCenter 和 MavenCentral。其中,MavenCentral 是最流行的中央仓库,也是 Gradle 默认使用的仓库之一。在这篇文章里,我将手把手带你发布组件到 MavenCentral 中央仓库。本文的示例程序使用小彭的开源项目 ModularEventBus 有用请给 Star,谢谢。这不仅仅是一份攻略,还带着踩过一个个坑留下的泪和挠掉一根根落的宝贵发丝~~~操作指引:1. 概念梳理1.1 什么是 POM?POM(Project Object Model)指项目对象模型,用于描述项目构件的基本信息。一个有效的 POM 节点中主要包含以下参数:参数描述举例groupId组织 / 公司名io.github.pengxuruiartifactId组件名modular-eventbus-annotationversion组件版本1.0.0packaging格式jar1.2 什么是仓库(repository)在项目中,我们会需要依赖各种各样的二方库或三方库,这些依赖一定会存放在某个位置(Place),这个 “位置” 就叫做仓库。使用仓库可以帮助我们管理项目构件,例如 jar、aar 等等。主流的构建工具都有 2 个层次的仓库概念:1、本地仓库: 无论使用 Linux 还是 Window,计算机中会有一个目录用来存放从中央仓库或远程仓库下载的依赖文件;2、远程仓库: 包括中央仓库和私有仓库。中央仓库是开源社区提供的仓库,是绝大多数开源库的存放位置。比如 Maven 社区的中央仓库 Maven Central;私有仓库是公司或组织的自定义仓库,可以理解为二方库的存放位置。1.3 Sonatype、Nexus 和 Maven 的关系:Sonatype: 完整名称是 Sonatype OSSRH(OSS Repository Hosting),为开源项目提供免费的中央存储仓库服务。其中需要用到 Nexus 作为仓库管理器;Nexus: 完整名称是 Sonatype Nexus Repository Manager,是 Sonatype 的另一款产品,用作提供仓库管理器。Sonatype 基于 Nexus 提供中央仓库,各个公司也可以使用 Nexus 搭建私有仓库;Maven: 完整名称是 Apache Maven,是一种构建系统。除了 Maven 之外,Apache Ant 和 Gradle 都可以发布组件。2. 新建 Sonatype 项目从这一节开始,我将带你一步步完成发布组件到中央仓库的操作(带你踩坑)2.1 准备 Sonatype JIRA 账号进入 Sonatype 仪表盘界面,登录或注册新账号:issues.sonatype.org:2.2 新建工单点击仪表盘面板右上角的 ”新建“ 按钮,按照以下步骤向 Sonotype 提交新建项目的工单:填写方法总结如下:项目: 使用默认选项 Community Support - Open Source Project Repository Hosting (OSSRH);问题类型: 使用默认选项 New Project;概要: 填写 Github 仓库相同的名称,以方便查找;GroupId 组织名: 填写发布组件时使用的 groupId,后续步骤中会检查你是否真实拥有该 groupId,所以不可以随便填写,有 2 种填写方式:**使用 Github 账号:**按照 io.github.[Github 用户名] 的格式填写,后续步骤中 Sonatype 通过要求我们在个人 Github 仓库中新建指定名称的临时代码库的方式来做身份验证;使用个人域名: 按照逆序域名的格式填写,例如个人域名为 oss.sonotype.org ,则填写 org.sonotype.oss 。Project URL 项目地址: 填写 Github 项目地址,例如: https://github.com/pengxurui/ModularEventBus ;SCM url 版本控制地址: 在 Github 项目地址后加 .git ,例如 https://github.com/pengxurui/ModularEventBus.git 。2.3 验证 GroupId 所有权点击弹出的消息进入工单详情页,刚新建的工单要等待 Sonotype 机器人回复,等待大概十几分钟后,在工单底部的评论区会告诉我们怎么操作。至此,Sonotype 项目准备完毕。3. 新建 GPG 密钥对GPG(GNU Privacy Guard) 是基于 OpenPGP 标准实现的加密软件,它提供了对文件的非对称加密和签名验证功能。所有发布到 Maven 仓库的文件都需要进行 GPG 签名,以验证文件的合法性。3.1 安装 GPG 软件安装 GPG 软件有两种方式:方式 1 - 下载安装包: 通过 GPG 官方 下载安装包,这个我没试过;方式 2 - 通过 Homebrew 安装: 使用 Homebrew 执行以下命令:命令行# 通过 Homebrew 安装 gpg brew install gpg 复制代码如果本地没有 Homebrew 环境则需要先安装,这里也容易踩坑。小彭本地原本就有 Homebrew 环境,但是安装 gpg 的过程中各种报错,最后还是用了最暴力的解法才解决 —— 卸载重装 Homebrew:(参考资料: MacOS下开发环境配置--homebrew的安装3.2 生成 GPG 密钥对使用 --generate-key 参数,按照指引填写相关信息和 passphrase 私钥口令。另外,使用 --list-keys 参数可以查看当前系统中生成过的密钥。命令行# 密钥生成命令 gpg --generate-key # 密钥查看命令 gpg --list-keys 复制代码命令行演示GPG 在生成密钥对时,会要求开发者做一些随机的举动,以给随机数加入足够多的扰动,稍等片刻就会生成完成了。完成后可以随时使用 —list-keys 参数查看密钥对信息:命令行演示解释一下其中的信息:/Users/pengxurui/.gnupg/pubring.kbx ----------------------------------- pub ed25519 2022-08-23 [SC] [expires: 2024-08-22] D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA uid [ultimate] test <test@gmail.com> sub cv25519 2022-08-23 [E] [expires: 2024-08-22] # pubring.kbx:本地存储公钥的文件 # 2022-08-23 [SC] [expires: 2024-08-22]:表示密钥对的创建时间和失效时间 # test <test@gmail.com>:用户名和邮箱 # ed25519:表示生成公钥的算法 # cv25519:表示生成私钥的算法 # D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA:密钥指纹 / KeyId 复制代码至此,你已经在本地生成一串新的密钥对,现在你手上有:密钥指纹 / KeyId: 密钥指纹是密钥对的唯一标识,即上面 D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA 这一串。有时也可以使用较短的格式,取其最后 8 个字符,即 B54DE73AA 这一串;公钥: 该密钥指纹对应的公钥;私钥: 该密钥指纹对应的私钥;passphrase 密钥口令: 生成密钥对时输入的口令,私钥与密钥口令共同组成密钥对的私有信息。3.3 删除密钥对有时候需要删除密钥对,可以使用以下命令:# 先删除私钥后,才能删除公钥 # 删除私钥 gpg --delete-secret-keys [密钥指纹] # 删除公钥 gpg --delete-keys [密钥指纹] 复制代码3.4 上传公钥密钥对中的公钥信息需要公开,其他人才能拿到公钥来验证你签名的数据,公开的方法就是上传到公钥服务器。公钥服务器是专门储存用户公钥的服务器,并且会用交换机制将数据同步给其它公钥服务器,因此你只要上传到其中一个服务器即可。我最后是上传到 hkp://keyserver.ubuntu.com 服务器的。以下服务器都可以尝试:pool.sks-keyservers.netkeys.openpgp.orgkeyserver.ubuntu.compgp.mit.edu命令行// 上传公钥 gpg --keyserver 【服务器地址】:11371 --send-keys 【密钥指纹】 // 验证公钥 gpg --keyserver 【服务器地址】:11371 --recv-keys 【密钥指纹】 复制代码3.5 导出密钥文件后文发布组件的时候需要用到密钥口令和私钥文件,可以使用以下参数导出命令行# 默认导出到本地目录 /User/[用户名]/ # 导出公钥 gpg --export 【密钥指纹】 > xiaopeng_pub.gpg # 导出私钥 gpg --export-secret-keys 【密钥指纹】 > xiaopeng_pri.gpg 复制代码3.6 踩坑:PGPException: unknown public key algorithm encountered我在发布组件时遇到 PGPException: unknown public key algorithm encountered 报错,最后排查下来是使用了 Gradle signing 插件不支持 EDDSA 算法,需要使用 RSA 算法。可以看到上文 3.1 节生成的公钥,可以看到是 ed 开头的,表示使用的是 EDDSA 算法,应该是不同版本中的 --generate-key 参数使用的默认算法不一样。3.1 节生成的公钥信息pub ed25519 2022-08-23 [SC] [expires: 2024-08-22] 复制代码解决方法是使用 --full-generate-key 参数选择使用 RSA 算法生成密钥对:命令行演示至此,密钥对准备完毕。4. 配置发布脚本完成 Sonatype 项目和密钥对的准备工作后,现在着手配置项目的 Gradle 脚本了。Gradle 提供了两个 Maven 插件:maven 插件: 旧版发布插件,从 Gradle 7.0 开始无法使用;maven-publish 插件: 新版发布插件。我最初的想法是分别整理出这两个插件的通用脚本,一开始是参考 ARouter 项目里的 publish.gradle 脚本,过程中也遇到各种问题,例如 Javadoc generation failed ,可能是因为 ARouter 是纯 Java 实现的,所以暴露的问题较少。耽搁了一周后,刚好这两天在看 LeakCanary 源码,果然在 LeakCanary 里发现宝藏 —— vanniktech 的发布插件!报错Execution failed for task ':eventbus_api:androidJavadocs'. > Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting): '/Users/pengxurui/workspace/public/ModularEventBus/eventbus_api/build/tmp/androidJavadocs/javadoc.options' 复制代码4.1 使用 maven 插件发布这块脚本是参考 ARouter 项目中 publish.gradle 脚本的,我在此基础上增加了注释和少量改动,如果遇到生成 Javadoc 出现问题,可以把 archives androidJavadocsJar 这一行注释掉。maven_sonatype.gradle// 在 ARouter 项目的 publish.gradle 上修改 apply plugin: 'maven' apply plugin: 'signing' version = VERSION_NAME group = GROUP // 是否 Release 发布(根据是否包含 SNAPSHOT 判断) def isReleaseBuild() { return VERSION_NAME.contains("SNAPSHOT") == false } // Central Repository: https://central.sonatype.org/publish/publish-guide/ // Release 仓库地址(默认先发布到 staging 暂存库,需要手动发布到中央仓库) def getReleaseRepositoryUrl() { return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" } // Snapshot 仓库地址 def getSnapshotRepositoryUrl() { return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : "https://s01.oss.sonatype.org/content/repositories/snapshots/" } // 仓库账号 def getRepositoryUsername() { return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" } // 仓库密码 def getRepositoryPassword() { return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" } // 组件配置 def configurePom(pom) { // 组织名 pom.groupId = GROUP // 组件名 pom.artifactId = POM_ARTIFACT_ID // 组件版本 pom.version = VERSION_NAME pom.project { // 名称 name POM_NAME // 发布格式 packaging POM_PACKAGING // 描述信息 description POM_DESCRIPTION // 主页 url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } // Licenses 信息 licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } // 开发者信息 developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } afterEvaluate { project -> // 配置 Maven 插件的 uploadArchives 任务 uploadArchives { repositories { mavenDeployer { // 配置发布前需要签名 beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } // 配置 Release 仓库地址与账号密码 repository(url: getReleaseRepositoryUrl()) { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } // 配置 Snapshot 仓库地址与账号密码 snapshotRepository(url: getSnapshotRepositoryUrl()) { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } // 配置 POM 信息 configurePom(pom) } } } // 配置 Maven 本地发布任务 tasks.create("installLocally", Upload) { configuration = configurations.archives repositories { mavenDeployer { // 本地仓库地址 repository(url: "file://${rootProject.buildDir}/localMaven") // 配置 POM 信息 configurePom(pom) } } } // 配置签名参数,部分需要在 local.properties 中配置 signing { required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { // Android 类型组件 task install(type: Upload, dependsOn: assemble) { // 依赖于 AGP assemble 任务 repositories.mavenInstaller { configuration = configurations.archives configurePom(pom) } } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.source classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } // 生成源码产物 task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.source } } else { // 纯 Java / Kotlin 类型组件(如 Gradle 插件、APT 组件) install { repositories.mavenInstaller { configurePom(pom) } } // 生成源码产物 task sourcesJar(type: Jar, dependsOn: classes) { classifier = 'sources' from sourceSets.main.allSource } // 生成 javadoc 产物 task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from javadoc.destinationDir } } // Java8 适配 if (JavaVersion.current().isJava8Compatible()) { allprojects { tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') } } } // 配置源码和 Javadoc 发布产物 if (!isReleaseBuild()) { // 快照版本跳过,提高效率 artifacts { if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { // Android 类型组件 archives androidSourcesJar // 源码 archives androidJavadocsJar // Javadoc,如果报错需要把这一行注释掉 } else { // 纯 Java / Kotlin 类型组件(如 Gradle 插件、APT 组件) archives sourcesJar // 源码 archives javadocJar // Javadoc } } } } 复制代码在需要发布的组件里应用这个脚本后,在 gradle.properties 里配置相关参数后就可以发布了。具体可以参考示例程序 ModularEventBus 中被注释掉的参数,也可以参考 ARouter 项目,这里就不展开了,建议用 4.2 节 vanniktech 的发布插件。项目级 gradle.properties###################################################################### # for maven_sonatype.gradle ###################################################################### # GROUP=io.github.pengxurui # # POM_URL=https://github.com/pengxurui/ModularEventBus/ # POM_SCM_URL=https://github.com/pengxurui/ModularEventBus/ # POM_SCM_CONNECTION=scm:git:git:github.com/pengxurui/ModularEventBus.git # POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/pengxurui/ModularEventBus.git # # POM_LICENCE_NAME=The Apache Software License, Version 2.0 # POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt # POM_LICENCE_DIST=repo # # POM_DEVELOPER_ID=pengxurui # POM_DEVELOPER_NAME=Peng Xurui # # SONATYPE_NEXUS_USERNAME=[provide your Sonatype user name] # SONATYPE_NEXUS_PASSWORD=[provide your Sonatype password] # # signing.keyId=[provide you gpg key] # signing.password=[provide you gpg passphrase] # signing.secretKeyRingFile=[provide you gpg secret file] 复制代码模块级 gradle.properties###################################################################### # for maven_sonatype.gradle ###################################################################### # POM_NAME=ModularEventBus Annotations # POM_ARTIFACT_ID=modular-eventbus-annotation # POM_PACKAGING=jar # POM_DESCRIPTION=The annotation used in ModularEventBus api # VERSION_NAME=1.0.0 复制代码4.2 使用 vanniktech 的发布插件(推荐)gradle-maven-publish-plugin 是一个外国大佬 vanniktech 开源的 Gradle 插件,需要使用 Gradle 7.2.0 以上的 Gradle 环境。它会创建一个 publish Task,支持将 Java、Kotlin 或 Android 组件发布到任何 Maven 仓库,同时也支持发布携带 Java / Kotlin 代码的 Javadoc 产物和 Sources 产物。虽然目前(2022/08/24)这个项目的最新版本只是 0.21.0,不过既然已经在 LeakCanary 上验证过,大胆用起来吧。以下为配置步骤:在项目级 build.gradle 中添加插件地址,在模块级 build.gradle 中应用插件:项目级 build.gradlebuildscript { repositories { mavenCentral() } dependencies { // vanniktech 发布插件 classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0' // Kotlin Javadoc,非必须 classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.20" // 最新版 1.7.10 和 0.21.0 组合有问题,应该是没兼容好。上面两个版本组合我验证过是可以的。 } } 复制代码模块级 build.gradleapply plugin: "com.vanniktech.maven.publish" // Kotlin Javadoc,非必须。如果有这个插件,发布时会生成 Javadoc,会延长发布时间。建议在 snapshot 阶段关闭 apply plugin: "org.jetbrains.dokka" 复制代码Sync 项目后,插件会为模块增加两个 Task 任务:publish: 发布到远程 Maven 仓库,默认是 Sonatype 中央仓库;publishToMavenLocal: 发布到当前机器的本地 Maven 仓库,即 ~/.m2/repository。Gradle 面板4.3 配置 vanniktech 插件的发布参数分别在项目级 gradle.properties 和模块级 gradle.properties 中配置以下参数:项目级 gradle.properties###################################################################### # for vanniktech ###################################################################### # 服务器地址 SONATYPE_HOST=S01 # 发布 release 组件时是否签名 RELEASE_SIGNING_ENABLED=true # 组织名 GROUP=io.github.pengxurui # 主页 POM_URL=https://github.com/pengxurui/ModularEventBus/ # 版本控制信息 POM_SCM_URL=https://github.com/pengxurui/ModularEventBus/ POM_SCM_CONNECTION=scm:git:git:github.com/pengxurui/ModularEventBus.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/pengxurui/ModularEventBus.git # Licenses 信息 POM_LICENSE_NAME=The Apache Software License, Version 2.0 POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENSE_DIST=repo # 开发者信息 POM_DEVELOPER_ID=pengxurui POM_DEVELOPER_NAME=Peng Xurui POM_DEVELOPER_URL=https://github.com/pengxurui/ mavenCentralUsername=[填 Sonatype 账号名] mavenCentralPassword=[填 Sonatype 密码] signing.keyId=[密钥指纹,取后 8 位即可] signing.password=[passphrase 密钥口令] signing.secretKeyRingFile=[导出的私钥文件路径,如 /Users/pengxurui/xxx.gpg] 复制代码模块级 gradle.propertiesPOM_NAME=ModularEventBus Annotations POM_ARTIFACT_ID=modular-eventbus-annotation POM_PACKAGING=jar POM_DESCRIPTION=The annotation used in ModularEventBus api VERSION_NAME=1.0.0 复制代码特别注意:私有信息不要提交到 git 版本管理中,可以写在 local.properties 中,等到要发布组件时再复制到 gradle.properties 中。而私钥文件也不要保存在当前工程的目录里,可以统一放到工程外的一个目录。至此,所有准备工作完成。4.4 浅尝一下 vanniktech 插件的源码毕竟发布逻辑都被人家封装在插件里了,有必要知道它背后的工作,浅尝一下。支持的 Snoatype 服务器:SonatypeHost.ktenum class SonatypeHost( internal val rootUrl: String ) { DEFAULT("https://oss.sonatype.org"), S01("https://s01.oss.sonatype.org"), } 复制代码支持 Dokka 插件,需要手动依赖:MavenPublishPlugin.ktprivate fun Project.defaultJavaDocOption(): JavadocJar? { return if (plugins.hasPlugin("org.jetbrains.dokka") || plugins.hasPlugin("org.jetbrains.dokka-android")) { JavadocJar.Dokka(findDokkaTask()) } else { null } } 复制代码支持多种模块类型:MavenPublishPlugin.ktafterEvaluate { when { plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") -> {} // Handled above. plugins.hasPlugin("com.android.library") -> {} // Handled above. plugins.hasPlugin("java-gradle-plugin") -> baseExtension.configure(GradlePlugin(defaultJavaDocOption() ?: javadoc())) plugins.hasPlugin("org.jetbrains.kotlin.jvm") -> baseExtension.configure(KotlinJvm(defaultJavaDocOption() ?: javadoc())) plugins.hasPlugin("org.jetbrains.kotlin.js") -> baseExtension.configure(KotlinJs(defaultJavaDocOption() ?: JavadocJar.Empty())) plugins.hasPlugin("java-library") -> baseExtension.configure(JavaLibrary(defaultJavaDocOption() ?: javadoc())) plugins.hasPlugin("java") -> baseExtension.configure(JavaLibrary(defaultJavaDocOption() ?: javadoc())) else -> logger.warn("No compatible plugin found in project $name for publishing") } } 复制代码5. 发布组件到 MavenCentral 仓库终于终于,所有准备和配置工作都完成了!在发布之前,有必要先解释下 Sonatype 中用到的仓库地址:5.1 仓库地址如果你没有自定义发布的 Maven 仓库,vanniktech 插件默认会发布到 Sonatype 管理的中央仓库中。由于历史原因,Sonatype 中央仓库有 2 个域名:s01.oss.sonatype.org/oss.sonatype.org/按照 官方的说法 ,oss.sonatype.org 是过时的,从 2021 年 2 月开始启用 s01.oss.sonatype.org/截图官方也会提示目前最新的仓库地址:5.2 Staging 暂存库细心的朋友会发现官方提供的 snapshot 仓库和 release 仓库的格式不一样,为什么呢?—— 这是因为发布 release 组件是敏感操作,一旦组件发布 release 版本到中央仓库,就永远无法修改或删除这个版本的组件内容(这个规则是出于稳定性和可靠性考虑,如果可以修改,那些本地已经下载过组件的用户就得不到最新内容了)。所以 Sonatype 对发布 snapshot 组件和 release 组件采取了不同策略:snapshot 组件: 直接发布到 snapshot 中央仓库;release 组件: 使用 Staging 暂存策略,release 组件需要先发布到暂存库,经过测试验证通过后,再由开发者手动提升到 release 中央仓库。中央 release 仓库:"https://s01.oss.sonatype.org/content/repositories/releases" 中央 snapshot 仓库:"https://s01.oss.sonatype.org/content/repositories/snapshots" 暂存库:"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2" 复制代码vanniktech 插件默认也是按照 Sonatype 的策略走的,浅看一下源码:MavenPublishBaseExtension.kt// 暂存库: if (stagingRepositoryId != null) { repo.setUrl("${host.rootUrl}/service/local/staging/deployByRepositoryId/$stagingRepositoryId/") } else { repo.setUrl("${host.rootUrl}/service/local/staging/deploy/maven2/") } // snapshot 库: if (it.version.toString().endsWith("SNAPSHOT")) { if (stagingRepositoryId != null) { throw IllegalArgumentException("Staging repositories are not supported for SNAPSHOT versions.") } repo.setUrl("${host.rootUrl}/content/repositories/snapshots/") } 复制代码5.3 发布 snapshot 组件版本号中带 SNAPSHOT 将被视为 snapshot 组件,会直接发布到 snapshot 中央仓库。经过小彭验证,确实在前端发布后,立马可以在 snapshot 中央仓库搜索到,例如 小彭的组件。验证截图5.4 发布 release 组件到 Staging 暂存库版本号中未带 SNAPSHOT 将视为 release 组件,发布 release 组件后,进入 Nexus 面板查看暂存库(右上角 Log in 登录):操作截图5.5 发布 release 组件到中央仓库确认要发布组件后,先点击 Close,再点击 Release 即可发布:操作截图Close 的过程会对组件进行验证,验证失败的话就会报错了。你可以直接从 Activity 面板中查看报错提示,我遇到的几次问题都是参数缺失的小问题。报错提示点击 Drop 按钮删除有问题的组件:操作截图如果验证通过,Release 按钮就会高亮,点击按钮就终于终于发布了。操作截图5.6 查看已发布的 release 组件发布成功后,有 3 种方式查看自己的组件:方法 1 - 在 Sonatype Nexus 面板上查看:操作截图方法 2 - 在 release 中央仓库的文件目录中查看,例如 小彭的 release 组件 :操作截图方式 3 - 在 MavenCentral 搜索栏 查找: 这是最正式的方式,缺点是不实时更新,大概有 的延迟,而前两种方式在发布后立即更新:操作截图按照 官方的说法 ,发布后的组件会在 30 分钟内同步到中央仓库,但搜索功能需要达到 4 个小时:Upon release, your component will be published to Central: this typically occurs within 30 minutes, though updates to search can take up to four hours. 复制代码5.6 依赖已发布的组件怎么依赖大家都懂。讲一下仓库吧,如果是已经发布到 release 中央仓库,你的工程只要包含 mavenCentral() 这个仓库地址就可以了。示例程序repositories { // 中央仓库(不包含 snapshot 中央仓库) mavenCentral() // release 中央仓库 maven { url 'https://s01.oss.sonatype.org/content/repositories/releases'} // snapshot 中央仓库 maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'} // 暂存库,用于验证 maven { url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2"} } 复制代码6. 报错记录Sonatype 账号密码错误:Failed to publish publication 'maven' to repository 'mavenCentral' > Could not PUT 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/io/github/pengxurui/modular-eventbus-annotation/1.0.2/modular-eventbus-annotation-1.0.2.jar'. Received status code 401 from server: Unauthorized 复制代码GPG 密钥错误:Execution failed for task ':eventbus_annotation:signMavenPublication'. > Error while evaluating property 'signatory' of task ':eventbus_annotation:signMavenPublication' > org.bouncycastle.openpgp.PGPException: checksum mismatch at 0 of 20 复制代码GPG 密钥算法错误:Execution failed for task ':eventbus_annotation:signMavenPublication'. > Error while evaluating property 'signatory' of task ':eventbus_annotation:signMavenPublication' > org.bouncycastle.openpgp.PGPException: unknown public key algorithm encountered 复制代码Javadoc 生成报错:Execution failed for task ':eventbus_api:androidJavadocs'. > Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting): '/Users/pengxurui/workspace/public/ModularEventBus/eventbus_api/build/tmp/androidJavadocs/javadoc.options' 复制代码vanniktech 插件与 Dokka 插件兼容问题:Execution failed for task ':eventbus_api:javaDocReleaseGeneration'. > 'void org.jetbrains.dokka.DokkaSourceSetImpl.<init>(java.lang.String, org.jetbrains.dokka.DokkaSourceSetID, ... 复制代码POM 验证错误:7. 寻求 Sonatype 官方帮助如果你在使用 Sonatype 的过程中遇到任何问题,可以尝试向官方提问。我试过一次,10 分钟后就收到回复了,还是很 Nice 的。操作截图操作截图8. 总结恭喜,到这里,我们已经能够实现发布开源项目到 MavenCentral 中央仓库。还没完,引出两个问题:Github Action: 每次发布都需要我们手动执行 upload 任务,Github 仓库中的 Releases 面板也不会同步显示手动发布的版本记录。 我们期望的效果是在 Github 仓库上发布一个 Release 版本时,自动触发将该版本发布到 MavenCentral 中央仓库。 这需要用到 Github 提供的 CI/CD 服务 —— Github Action;ModularEventBus: 本文的示例程序,它是做什么的呢?关注我,带你了解更多。
文章
ARouter  ·  算法  ·  Java  ·  Apache  ·  Maven  ·  开发工具  ·  数据安全/隐私保护  ·  开发者  ·  git  ·  Kotlin
2022-10-30
1 2 3 4 5 6 7 8 9
...
11
跳转至:
开发与运维
5786 人关注 | 133444 讨论 | 319524 内容
+ 订阅
  • 算法题学习链路简要分析与面向 ChatGPT 编程
  • Linux的IPtables可以阻挡ddos攻击吗?底层原理是什么?
  • Codeup的实用性评价
查看更多 >
大数据
188713 人关注 | 30991 讨论 | 83955 内容
+ 订阅
  • Yii2如何进行代码审查?具体怎么做?底层原理是什么?
  • 什么是 WebSocket 协议?底层原理是什么?
  • office全版本软件安装包(win+mac版本)——2016office软件下载
查看更多 >
安全
1247 人关注 | 24148 讨论 | 85898 内容
+ 订阅
  • MySQL的字符型数据类型是干什么的?使用场景是什么?底层原理是什么?
  • MySQL的SSL/TLS支持是什么意思?具体如何使用?底层原理是什么?
  • Linux的IPtables可以阻挡ddos攻击吗?底层原理是什么?
查看更多 >
人工智能
2875 人关注 | 12395 讨论 | 102680 内容
+ 订阅
  • 算法题学习链路简要分析与面向 ChatGPT 编程
  • Codeup的实用性评价
  • 基于PSO三维极点搜索matlab仿真
查看更多 >
数据库
252947 人关注 | 52318 讨论 | 99273 内容
+ 订阅
  • MySQL的数值型数据类型是干什么的?使用场景是什么?底层原理是什么?
  • MySQL的日期/时间型数据类型是干什么的?使用场景是什么?底层原理是什么?
  • MySQL的字符型数据类型是干什么的?使用场景是什么?底层原理是什么?
查看更多 >