关于Android开发组件化的一些思考

前言

组件化开发如今基本上属于基础操做了,你们通常都会使用 ARouter 、LiveDataBus 做为组件化通讯的解决方案,那为何会选择ARouter,ARouter又是怎么实现的呢?这篇文章主要就 搭建组件化开发的准备工做 、组件化跳转分析,若是理解了这篇文章,对于查看ARouter源码应该是会有很大的帮助的。至于ARouter等分析,网上有不少的讲解,这里就不分析ARouter源码了,文章末尾会给出ARouter源码时序图和总结,可忽略。html

ps: 为何写本文,由于笔者最近被问道,为何要用ARouter,ARouter它究竟是解决什么问题,你能就一个点来分析吗?被问到该问题了?笔者是从它的跳转回答的,毕竟跳转简单。恰好记录并回忆一下。java

参考资料android

Android APT 实践git

谈谈APT和JavaPoet的一些使用技巧和要点github

目录

1、组件化优点

组件化的优点想必你们都知道,能够总结为四点数组

  • 编译速度 咱们能够按需求测试单一业务模块,而不须要总体打包运行,节约了实践,有效的提高了咱们的开发速度markdown

  • 解耦 极度的下降了模块之间的耦合,便于后期维护与更新,当产品提出一个新业务时,彻底能够新建一个业务组件,集成和摒弃都很方便架构

  • 功能重用 某一块的功能在另外的组件化项目中使用只须要单独依赖这一模块便可app

  • 团队开发效率 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协做ide

2、组件化开发准备工做

组件化开发,通常能够分为三层,分别为 壳工程、业务组件、基础依赖库,业务组件间互不关联,而且业务组件须要能够单独运行测试,总体都是围绕解耦来开展的,下面开始进行组件化开发前所须要作的准备工做

一、包名和资源文件命名冲突问题

须要制定规范,对包名和项目模块的划分规范化,不一样模块内不能有相同名字的类文件,避免打包失败等冲突问题

二、Gradle中的版本好统一管理

接下来的写法是最广泛的,就是有点小瑕疵:不支持 AS 的自动补充功能,也没法使用代码自动跟踪,所以能够考虑使用 buildSrc。buildSrc 是 Android 项目中一个比较特殊的 project,在 buildSrc 中能够编写 Groovy 语言。

在咱们建立的模块中,有一些,例如 compileSdkVersion 、buildToolsVersion 或者是集成的第三方依赖库,它们都有对应的版本号,若是不进行统一管理,后续维护很麻烦,总不能对全部模块一个个手动修改版本。因此咱们能够在gradle.properties文件中,添加配置,例如

gradle.properties
CompileSdkVersion = 30// 这里不能和compileSdkVersion 同样,会报错
​
模块的build.gradle
android{
    compileSdkVersion CompileSdkVersion.toInteger()
}
复制代码

全部模块版本号都按照上面的写,每次改版本号都按照gradle.properties里面定义的修改就好。可是,细心的你必定会发现,如今网上的例子,这些写的不多,既然这样写也能作到统一管理,为何不推荐呢?答案就在 CompileSdkVersion.toInteger() 这里,这里拿到CompileSdkVersion后还须要转换,若是使用下面建立gradle文件的作法,彻底能够省去。

在项目根目录下新建一个conffig.gradle 文件,和全局build.gradle同一层级

config.gradle
​
ext{
    android=[
            compileSdkVersion:29,
            buildToolsVersion:'29.0.2',
            targetSdkVersion:29,
    ]
    dependencies  = [
         appCompact : 'androidx.appcompat:appcompat:1.0.2'
    ]
}
根目录的build.gradle中,顶部加入
apply from:"config.gradle"
​
使用的时候以下
compileSdkVersion rootProject.ext.android.compileSdkVersion
implementation rootProject.ext.dependencies.appCompact
复制代码

注意,在implementation dependencies 时候是能够这样写的

implementation 'androidx.test.ext:junit:1.1.0','androidx.test.espresso:espresso-core:3.1.1'
复制代码

可是你在config.gradle中千万不能也相似这样写

dependencies  = [
         appCompact : '\'androidx.appcompat:appcompat:1.0.2\',\'androidx.test.espresso:espresso-core:3.1.1\''
    ]
复制代码

由于在build.gradle中你把全部依赖放到implementation后面,用逗号分隔,这个逗号和字符串的逗号不同,你在config.gradle中那样写的其实至关于在build.gradle implementation dependencies 时这样写

implementation 'androidx.test.ext:junit:1.1.0,androidx.test.espresso:espresso-core:3.1.1'
复制代码

那你可能会问,这样写不行的话,那我怎么在config.gradle中实现对全部模块须要的公共依赖库集中管理呢?能够按照下面这样写

ext {
    ....
    dependencies = [
            publicImplementation: [
                    'androidx.test.ext:junit:1.1.0',
                    'androidx.test.espresso:espresso-core:3.1.1'
            ],
            appCompact          : 'androidx.appcompat:appcompat:1.0.2'
    ]
}
​
implementation rootProject.ext.dependencies.publicImplementation //每一个模块都写上这句话就行了
复制代码

这样就完了吗?还有咱们本身写的的公共库也要集中管理,通常咱们都会在模块的build.gradle中一个个这样写

implementation project(path: ':basic')
复制代码

如今咱们经过gradle来管理,以下

ext {
    ....
    dependencies = [
            other:[
                ':basic',
            ]
    ]
}
​
rootProject.ext.dependencies.other.each{
    implementation project(it)
}
复制代码

三、组件在Application和Library之间随意切换

Library不能在Gradle文件中有applicationId

AndroidManifest.xml文件区分

在开发过程当中,须要独立测试,避免不了常常在Application和Library之间随意切换。在模块,包括壳工程app模块运行时,Application类只能有一个。

首先咱们在config.gradle中配置,为何不在gradle.properties中配置,以前也说了

ext {
    android = [
            compileSdkVersion: 29,
            buildToolsVersion: '29.0.2',
            targetSdkVersion : 29,
            isApplication:false,
    ]
....
}
复制代码

而后在各个模块的build.gradle文件顶部加入如下判断

if(rootProject.ext.android.isApplication) {
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}
​
复制代码
  • Library不能在Gradle文件中有applicationId

    android { defaultConfig { if(rootProject.ext.android.isApplication){ applicationId "com.cov.moduletest" //做为依赖库,是不能有applicationId的 } .... }

  • 在app模块的gradle中也须要有区分

    dependencies { ..... if(!rootProject.ext.android.isApplication){ implementation project(path: ':customer') //只有当业务模块是依赖的时候去依赖 ,看业务需求 } }

  • AndroidManifest.xml文件区分

    在各个模块的build.gradle中区分

    sourceSets {
        main{
            if(rootProject.ext.android.isApplication){
                manifest.srcFile '/src/main/AndroidManifest.xml'
            }else{
                manifest.srcFile "src/main/manifest/AndroidManifest.xml"
            }
        }
    }
    复制代码
  • Application配置

由于咱们会在Application中作一些初始化操做,若是模块单独运行的话,那么这些操做须要放到模块的Application中,因此这里须要单独配置一下,新建module 文件夹,配置好下面文件时,新建自定义的Application类,而后在manifest文件夹下的清单文件内指定Application。这样做为依赖库运行时,module 文件夹下的文件不会进行编译。

main{
              if(rootProject.ext.android.isApplication){
                  manifest.srcFile '/src/main/AndroidManifest.xml'
              }else{
                  manifest.srcFile "src/main/manifest/AndroidManifest.xml"
                  java.srcDirs 'src/main/module','src/main/java'
              }
          }
复制代码

以上是配置单独模块时,Application能够这样写,但这里还须要考虑Application的初始化问题,壳工程的Application初始化完成后须要分别初始化依赖组件的Application。能够这样写

basic 模块中定义
public interface IApp{
    void init(Application app);
}
而后各个模块相似这样写
public AModuleApplication implements IApp{
    public void init(Application app){ 初始化操做 }
}
在壳工程的Application里维护一个数组 {"com.cv.AModuleApplication.class","xx"} 
可是这样不优雅,建议在basic中建个类专门维护

接下来,做为一个独立AP运行P时,只须要在壳工程Application的onCreate方法中对该数组的类所有进行反射构造,
调用init方法便可。
复制代码

上面这样写确实能够,惟一不足的是须要维护一个包含各个模块做为Library时须要初始化的类,**有没有更好的方法呢?**答案确定是有的,使用注解,对每一个模块中,须要在Application初始化调用的类,即上述数组中维护的类,加上注解,编译期收集起来,Application的onCreate方法调用,没理解的同窗能够看下面的组件化跳转分析,道理相似。

四、在Java代码中判断是否独立运行仍是集成运行

在运行时,每一个模块都会生成一个对应的BuildConfig类,存放包路径可能不一样,那咱们怎么作呢?

在basic模块的build.gradle中加入如下代码

buildTypes {
        release {
            buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
        }
        debug {
            buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
        }
}
复制代码

为何要在basic模块下加入呢?就是由于BuildConfig每一个模块都会有,总不能在全部模块都加入这句话吧。在basic模块加入后,其它模块依赖这个模块,而后经过在basic模块中定义的BaseActivity中,添加获取该值的方法便可,其余模块继承BaseActivity,就能够拿到父类方法进行判断了,这只是一种,具体要看业务进行分析。

3、组件化跳转分析

一、自定义组件化跳转模块

按照上述配置后,接下啦第一步就须要解决组件化通讯问题,其中第一类问题就是跳转相关。由于业务组件之间不能耦合,因此咱们只能经过自定义一个新的 router 模块,各个业务组件内经过继承该依赖,而后实现跳转。

咱们只须要在router模块中定义一个ARouter容器类,而后各个模块进行注册Activity,就可使用了,代码以下

public class ARouter {
    private static ARouter aRouter = new ARouter();
    private HashMap<String, Class<? extends Activity>> map = new HashMap<>();
    private Context mContext;
​
    private ARouter(){
    }
    public static ARouter getInstance(){
        return aRouter;
    }
​
    public void init(Context context){
        this.mContext = context;
    }
    /**
     * 将类对象添加到容器中
     * @param key
     * @param clazz
     */
    public void registerActivity(String key,Class<?extends  Activity> clazz){
        if(key != null && clazz != null && !map.containsKey(key)){
            map.put(key,clazz);
        }
    }
    public void navigation(String key){
        navigation(key,null);
    }
​
    public void navigation(String key, Bundle bundle){
        if(mContext == null){
            return;
        }
        Class<?extends  Activity > clazz = map.get(key);
        if(clazz != null){
            Intent intent = new Intent(mContext,clazz);
            if(bundle != null){
                intent.putExtras(bundle);
            }
            mContext.startActivity(intent);
        }
    }
}
复制代码

经过ARouter.getInstance().navigation("key") 就能跳转了,可是前提是须要调用registerActivity将每一个Activity和对应路径注册进来,那不可能在每一个Activity中都调用该方法将类对象加到ARouter路由表吧?咱们可能会想到在BasicActivity里面加一个抽象方法,将全部类对象返回,而后你拿到后调用registerActivity方法注册,可是这个前提是 须要你继承BasicActivity的类已经建立了,已经实例化了,因此这不可能在没启动Activity时进行注册。那怎么样才能在Activity没启动时,将全部类对象添加到ARouter容器内呢?有什么方法能够在Application建立时候能够收集到全部未启动的Activity呢?

可能你们还会想到,在每个模块里面新建一个ActivityUtils类,而后定义一个方法,里面调用ARouter.registerActivity ,注册该模块全部须要注册的类,而后在Application类里触发该方法。模块少还好说,能够一个个手动敲,模块一多,每一个模块都得写,维护太麻烦了,可不能够自动生成这样的方法,自动找到须要注册的类,收集起来呢?

这就须要使用APT技术来实现了,经过对须要跳转的Activity进行注解,而后在编译时生成类文件及类方法,该类方法内利用Map收集对应的注解了的类,在Application建立时,执行这些类文件相关方法,收集到ARouter容器内。

二、组件化跳转实现方案升级

不了解如何操做APT的同窗能够参考

Android APT 实践

谈谈APT和JavaPoet的一些使用技巧和要点

要实现上述说的方案,须要了解一下APT(Annotation Processing Tool)技术,即注解处理器,它是Javac的一个工具,主要用来在编译时扫描和处理注解。

  • 建立注解,对须要注册的Activity类用注解标记 (annotation模块)

    @Target 声明注解的做用域

    @Retention 生命注解的生命周期

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ActivityPath {
        String value();
    }
    复制代码
  • 建立注解处理器 并生成类 (annotation_compiler模块)

    @AutoService(Processor.class) 虚拟机在编译的时候,会经过这个判断AnnotationCompiler是注解处理器, 是固定的写法,加个注解便可,经过auto-service中的@AutoService能够自动生成AutoService注解处理器,用来注册用来生成 META-INF/services/javax.annotation.processing.Processor 文件

    @SupportedSourceVersion(SourceVersion.RELEASE_7) 指定JDK编译版本

    @SupportedAnnotationTypes({Constant.ACTIVITY_PATH}) 指定注解,这里填写ActivityPath的类的全限定名称 包名.ActivityPath

    Filer 对象,用来生成Java文件的工具

    Element 官方解释 表示程序元素,如程序包,类或方法,TypeElement表示一个类或接口程序元素,VariableElement表示一个字段、枚举常量或构造函数参数、局部变量,TypeParameterElement表示通用类、接口、方法、或构造函数元素的正式类型参数,这里简单举个例子

    package  com.example  //PackageElement
    public class A{ //TypeElement
        private int a;//VariableElement
        private A mA;//VariableElement
        public A(){}  // ExecuteableElement
        public  void setA(int a){ // ExecuteableElement   参数a是VariableElement
            
        }
        
    }
    复制代码

    还须要注意一点,为了在编译时不出现GBK编码错误等问题,须要在gradle中添加

    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
    }
    复制代码

    接下来就开始真正实现了,如今annotation_compile的依赖中添加

    implementation'com.google.auto.service:auto-service:1.0-rc4'
        annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
        implementation 'com.squareup:javapoet:1.11.1'
    复制代码

    而后实现注解处理器类

    @AutoService(Processor.class)
    @SupportedAnnotationTypes({Constant.ACTIVITY_PATH})
    // 注解处理器接收的参数
    @SupportedOptions(Constant.MODULE_NAME)
    public class AnnotationCompiler extends AbstractProcessor {
    ​
        //生成java文件的工具
        private Filer filer;
        private String moudleName;
    ​
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            filer = processingEnv.getFiler();
            moudleName = processingEnv.getOptions().get(Constant.MODULE_NAME);
        }
    ​
        /**
         * 获得最新的Java版本
         *
         * @return
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return processingEnv.getSourceVersion();
        }
    ​
        /**
         * 找注解 生成类
         *
         * @param set
         * @param roundEnvironment
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            if (moudleName == null) {
                return false;
            }
            //获得模块中标记了ActivityPath的注解
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ActivityPath.class);
            //存放 路径 类文件名称
            Map<String, String> map = new HashMap<>();
            //TypeElement 类节点
            for (Element element : elements) {
                TypeElement typeElement = (TypeElement) element;
                ActivityPath activityPath = typeElement.getAnnotation(ActivityPath.class);
                String key = activityPath.value();
                String activityName = typeElement.getQualifiedName().toString();//获得此类型元素的彻底限定名称
                map.put(key, activityName + ".class");
            }
    ​
            //生成文件
            if (map.size() > 0) {
                createClassFile(map);
            }
            return false;
        }
    ​
        private void createClassFile(Map<String, String> map) {
            //1.建立方法
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("registerActivity")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class);
    ​
            Iterator<String> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                String key = iterator.next();
                String className = map.get(key);
    ​
                //2.添加方法体
                methodBuilder.addStatement(Constant.AROUTER_NAME + ".getInstance().registerActivity(\"" + key + "\"," + className + ")");
    ​
            }
            //3.生成方法
            MethodSpec methodSpec = methodBuilder.build();
    ​
            //4.获取接口类
            ClassName iRouter = ClassName.get(Constant.PACKAGE_NAME, Constant.IROUTER);
            //5.建立工具类
            TypeSpec typeSpec = TypeSpec.classBuilder(Constant.CLASS_NAME + "?" + moudleName)
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(iRouter) //父类
                    .addMethod(methodSpec) //添加方法
                    .build();
    ​
            //6.指定目录构建
            JavaFile javaFile = JavaFile.builder(Constant.PACKAGE_NAME, typeSpec).build();
    ​
            //7.写道文件
            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
    ​
            }
        }
    ​
    ​
    }
    复制代码

    生成的文件效果以下

    public class RouterGroup$$moduletest implements IRouter {
      public void registerActivity() {
     com.cv.router.ARouter.getInstance().registerActivity("/main/login",com.cv.moduletest.LoginActivity.class);
      }
    }
    复制代码
  • 在ARouter中实现init方法,触发类文件的方法

    public void init(Context context){
            this.mContext = context;
            //1.获得生成的RouterGroup?.. 相关文件 找到这些类
            try {
                List<String> clazzes = getClassName();
                if(clazzes.size() > 0){
                    for(String className:clazzes){
                        Class<?> activityClazz = Class.forName(className);
                        if(IRouter.class.isAssignableFrom(activityClazz)){
                            //2.是不是IRouter 子类
                            IRouter router = (IRouter) activityClazz.newInstance();
                            router.registerActivity();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        private List<String> getClassName() throws IOException {
            List<String> clazzList = new ArrayList<>();
            //加载apk存储路径给DexFile
            DexFile df = new DexFile(mContext.getPackageCodePath());
            Enumeration<String> enumeration = df.entries();
            while (enumeration.hasMoreElements()){
                String className = enumeration.nextElement();
                if(className.contains(Constant.CLASS_NAME)){
                    clazzList.add(className);
                }
            }
            return clazzList;
        }
    复制代码

    到此就实现了自动化收集类信息。

4、ARouter总结

当你了解了上述方法时,你再去看ARouter的源码,会轻松点,跳转实现原理,都差很少。固然ARouter也支持拦截等功能,想要查看ARouter源码,能够自行在掘金上搜索。这里给出之前看ARouter时作的笔记,只针对客户端使用ARouter时的时序图和文字描述,可能总结写得不全很差,不喜勿喷

  1. 首先在ARouter.getInstance().init()中会调用_ARouter的init()方法,而后回调用after方法,after方法是经过byName形式获取的拦截器Service。

  2. 这里主要是init()方法,里面会构建一个Handler,主要用来启动应用组件跳转和在debug模式下显示提示,而后还有一个线程池,主要是用于执行后续拦截器拦截逻辑,而后这个init中,最重要的应该就是LogisticsCenter.init()方法,在这里面,他会获取arouter.router包名下的全部类文件名,而后加载到Set集合中,而后遍历这些class,Root相关的类反射调用loadInto方法加载到groupIndex集合中,Interceptors相关的类加载到interceptorsIndex中,Providers相关的类加载都providersIndex中。这些类文件都是arouter-compile根据注解生成的,文件名规则是ARouter Root ? 模块名或者是ARouter ?Provider 模块名,或者是ARouter ? Group ? group名,例如Root相关类的loadInto方法就是把group值和group 相关类匹配放在groupIndex中,而后在须要使用时再去加group相关类的信息。

  3. 咱们使用ARouter.getInstance().build().navigation获取Fragment或者跳转时,它先是_ARouter的build方法, 这个方法里,他会bayType形式调用PathReplaceService,对build()方法传入的路径path作修改,而后若是使用RouterPath注解时没有指定group,会获取path中第一个/后面的字符串做为group并返回一个Poscard,内部有一个bundle用于接收传入的参数,而后调用自身的navigation方法,最后仍是回调到了 _ARouter的navigation()方法,这个方法内会按需获取加载指定path对应的类信息,首先是从groupIndex里面须要group组名对应的类信息,而后经过反射调用loadInto方法,将该组名下的全部路径对应关系保存到routes Map中,而后去完善传入的Path对应的RouteMeta信息,最后根据元信息的类型,构建对应的信息,并指定provider和fragment的开启绿色通道。而后接下来,就是若是没有开启绿色通道,将利用CountDownlaunch和线程池将全部拦截器按需进行处理,而后通行后,会根据元信息类型,构造相应参数,启动Activity或者反射构建Fragment返回。

@Path(path)

  • 对应于ARouter$$Root$$模块名.class,内部生成loadTo方法,主要用来将 路径group 和对应的映射关系存入MAP中,在ARouter初始化时,会将这些信息存到静态的group Map结构里。拦截器interceptorsproviders相似,providers的话,map存储关系是接口名和RouteMeta的映射关系,RouteMeta中会包含实现类,path等信息。

  • ARouter$$Group$$组名.class,内部也有loadTo方法,主要是将路径group组下的全部路由路径和对应的RouteMeta信息存入Map

navigation跳转或者获得实例过程当中

  • 会先去看跳转的路径是不是大于等于2级的,而后看 PathReplaceService 路径替换接口有没有实现类,需不须要替换。只有一个

  • 而后会检查是否须要预处理,也就是跳转前的处理,例如是不是针对某个路径,本身处理跳转

  • 而后就去group map里面去找,路径组名group对应的类,而后调用该类的方法,将该组下的全部路径名和包含跳转目标Activity或者IProvider的一些实现类等的信息的RouteMeta存到新的routes map里,而后将以前的group map 下该组信息删除,节省内存。

  • 若是是Provider的话,会将Provider 实现类的Class对象和反射构造的实例,经过providers map 存起来,而后调用init方法。

  • FragmentProvider默认开启绿色通道,不会执行拦截器。

  • 拦截器是在ARouter初始化时,会默认获取到系统设置的拦截器

  • 而后在这个拦截器内,会经过线程池 和 CountDownLatch方式将全部开发者自定义的拦截器,进行执行调用。控制是否放行

5、最长公共子字符串

这周被问到一个问题,android列表上显示的全部数据,如何找出最长公共子标签,我立马想到动态规划,可是总感受会有更好的实现方式,毕竟LCS问题大多都是给定两个字符串,总不能每两个比较后 (O(n2)),再跟第三个、第四个比较,这样时间复杂度不是很好。最后回过头想一想,其实思路应该就是这样的,经过系统API操做,也要这样比较。

/**
 * str1的长度为M,str2的长度为N,生成大小为M*N的矩阵dp
 * dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度 
 * 若是dp[i][0] dp[0][i] 为1 后面就都为1
 * @author xxx
 *
 */
public class DemoFive {
    //dp[i][j] 的含义是str1[0....i] 与 str2[0......j]的公共子序列的长度
    public static String findLCS(String A,int n,String B,int m) {
        char[] arrA = A.toCharArray();
        char[] arrB = B.toCharArray();
        // n * m 矩阵 A * B
        int[][] dp = new int[n][m];
​
        int length = 0;
        int start = 0;
        for(int i = 1;i<n;i++) {
            for(int j = 1;j<m;j++) {
                if(arrA[i]== arrB[j] ) {
                    dp[i][j] = dp[i-1][j-1]+1;
                   if(dp[i][j] > length) {
                       length = dp[i][j];
                       start = i - length+1 ; //注意这里 下标是从0开始的
                   }
                }
            
            }
        }
        String result = A.substring(start,start + length);
        return result;
        
    }
}
复制代码

笔记八

相关文章
相关标签/搜索