Android组件化架构学习笔记——组件化分发2

1.组件化列表配置:

1.1:Javapoet语法基础:

Javapoet是Java编译时注解开发的工具类库,Javapoet提供编写Java代码的接口,在编译器中自动生成源代码。

Javappoet中有五种常用的类:

  • ParamterSpec:参数声明;
  • MethodSpec:构造函数或方法声明;
  • TypeSpec:类/接口或者枚举声明;
  • FieldSpec:成员变量;
  • JavaFile:包含拥有一个类对象的Java文件。
public static void main(String[] args) throws Exception {
  MethodSpec main = MethodSpec.methodBuilder("main")
      //Modifiers是修饰关键词
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
       //添加String[]类型的名为args
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

    //    HelloWorld是类名
  TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)    //在类中添加方法
    .build();

  JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();    //HelloWorld是类名

  javaFile.writeTo(System.out);    //输出到调试面板中
}
  • XXXSpec类是使用典型的建造者设计模式;
  • xxxBuilder是声明命名,如classBuilder类构造/constructorBuilder构造器/enumBuilder枚举/interfaceBuilder接口构造。
  • addModifier是添加修饰的关键词。

1.2JavaPeot中常用方法和属性:

$L/$T/$S/$N都是占位符。

  • $L:常量;
  • $T:String类型;
  • $S:变量制定类型,可以通过ClassName来指定外部类名;
  • $N:生成的方法名或变量名。

FieldSpec

  • Initializer:添加变量初始化值。

MethodSpec:

  • addParameter:添加方法中的参数;
  • addStatement:添加一行中显示的内容;
  • addCode:可以通过拼接字符串来编写代码;

另一种方式,可以通过SpannableBuilder来完成代码拼接。

在编写for/while时需要用到一下关键字:

  • beginControlFlow:在行的末尾添加大括号开始符“{”和代码缩进;
  • endControlFlow:在末尾添加结束大括号结束符“}”和缩进;
  • addJavadoc("XXX"):在方法上添加注解;
  • addAnnotation(Overrde.class):在方法上添加注解。

TypeSpec:

  • addStaticImport:引用需要的Java包;
  • addMehod:在类中添加方法;
  • addField:添加参数;
  • build:创建编写代码。

JavaFile:

  • build:创建Java文件;
  • writeTo:将代码块输入到某个地址中。

编写顺序必须遵循:

FieldSpec -> ParameterSpec -> MethodSpec -> TypeSpec ->JavaFile

2.编译时注解配置:

Android studio并没有提供一个关于编译时注解的开发模版给开发者用,使用IntellijIDEA可以完全支持编译时注解的编写。Android studio只能通过JavaLibarary模块来进行改造。

ModuleBus为例子。

配置编译时注解运行入口有两种方式:

  • 使用Google的AutoService,通过编译时注解来添加javax.annotation.processing.Processor文件;
  • 在根目录在新建resource文件夹,在里面建立META-INF.service文件夹,然后定义javax.annotation.processing.Processor文件。在此文件中添加运行javapeot代码的包名和地址。

如:ModuleBus的compile库配置的processor是:com.cangwang.process.ModuleUnitProcessor

配置此入口索引,Javapeot框架才能在程序编译时先运行processor。

  • 需要在Base module中添加注解饮用的annotation模块:
compile project(':annotation')
  • 在业务module的build.gradle中配置annotation模块:
dependencies{
    annotationProcessor project(":compile")
}

3.集成配置列表:

在编写前需要在compiler中添加build.gradle来引用一些库:

apply plugin: 'com.android.application'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
   
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementaion 'com.squareup:javapeot:1.7.0'
    implementation project(':annotation')
    implementation 'org.apache.commons:commons-lang3:3.4'
    implementation 'org.apache.commons:commons-collections4:4.1'
}

build.gradle中需要引用Java基础环境和auto-service.javapoet两个库,以及自定义annotation库。对配置列表使用编译时注解的步骤如下所述:

  • ModuleUnit用于记录每个module配置相关的信息;
  • ModuleUnitProcessor用于读取注解,生成Java代码文件库;
  • app启动时,application汇总所有模块的信息;
  • ModuleCenter将所有模块的信息保存为模块列表;
  • 当分发的Activity/Fragment启动后,通过ModuleBus抽取不同模版配置的module列表,完成启动加载。

4.加载优化:

优化的核心是时间和空间之间的转换,可以使用三种加载优化策略:

  • 预加载;
  • 线程加载;
  • 懒加载。

编译时注解属于预加载处理,其原理是通过编译时注解来优化配置列表,通过application启动时进行初始化来配置组件化列表。编译时注解可以说是外部优化的非常有效的方式,很多先进的框架都在使用。其余的两种是Android和Java内部提供的有效的加载方式----使用线程加载和懒加载对分发架构进行优化。

4.1 线程加载:

app中的UI更新只能在UI线程(主线程)中进行。多模块涉及UI界面渲染加载时,只能通过UI线程上进行串行的处理。可以只使用for循环直接在主线程中加载。

使用for循环直接在主线程中加载,单页面加载少量模块时,这样的加载并不会出现问题。

但在开发迭代时,模块越来越多,全部模块加载出来的时间会越来越长,究其原因是模块初始化需要在UI线程中串行加载。特别是低端的机型中,在CPU和GPU运行不理想的主线程加载缓慢非常容易造成ANR和初始化界面卡顿等问题。

解决方案:抽取可以在后台线程运行的代码,保留模块在UI线程中进行初始化,这里就涉及到Java线程的操作和Android中的线程切换。

UI线程在app初始化是一直存在的,Android提供handler机制可以将切换任务抛给UI线程上进行。在分发模块设置起初,各个module中的init方法用于初始化界面工作。尽量不在init中做过多主线程的操作。

工作线程(非UI线程)可以通过新建线程或获取线程池中缓存的方式实现。使用工作线程操作一些UI相关的任务。

Java线程池相关知识

是否也可以使用Android原生的AsynTask来进行切换呢?AsynTask也可以设置串行操作,如模块过多,加载时间长。突然使用横竖屏等重新构建activity操作,容易导致引用未释放,从而造成内存泄露。如AsynTask使用被声明为activity的非静态的内部类,那么AsynTask会保留一个创建AsynTask的对activity的引用。如activity已经被销毁,AsynTask的后台线程还执行,它将继续在内存中保存这个引用,导致activity无法被回收引起内存泄露。handler使用的时候也需要注意内存泄露的问题,在destroy页面销毁时一定要释放handler和持有的context。

进一步可以使用RxJava的方式来进行逻辑优化,RxJava的线程切换核心思想也是线程池+handler组合。

4.2 模块懒加载:

app界面设计不会让所有的内容全部呈现,不然会内容过多/用户关注重点被干扰等问题。不需要立刻显示的模块可以使用懒加载的形式来进行加载。

view中显示状态:

  • Visible: 有界面布局占位且显示在界面中;
  • Invisible:有界面布局占位,但未渲染在界面中显示;
  • Gone:只创建了实例,并未在界面布局占位,也未渲染在界面中显示。

view的绘制机制是:

创建view -> 界面布局占位 -> 界面渲染

因为有层级的显示顺序是可以通过addView来完成布局的动态加载的,那么加载就必须占位来保证显示顺序。

使用Invisible就可以保证占用布局,并且不显示在界面上。但是使用Invisible并不能进行懒加载。

使用懒加载有两种不同的方式:

  • 先添加一个空ViewGroup占位,在收到事件或其他触发任务时,使用真正的布局替换空的ViewGroup,然后进行初始化;
  • 通过使用ViewStub的形式来完成占位,通过收到事件获取其他触发任务时用真正的ViewStub,然后进行初始化。

第一种方式是添加,第二种方式是替换,不同之处在于第一种方式比第二种方式多了一个ViewGroup的层级。

ViewStub拥有一个layout属性和inflated属性。在布局xml文件中填写需要加载的layout布局和布局对应inflated,ViewStub在被setVisible时会自动加载layout属性种的布局,findViewById绑定inflated就能绑定layout控件。

ViewStub的运行原理:视图加载时,在onMeasure方法中调用setMeasureDimension传递的参数MeasureWidth和measureHeight的值为0,在draw方法被置空,ViewStub只要被inflated后,会实例化定制的布局,并添加到ViewStub的父视图中显示。ViewStub的setVisibility()方法被调用时,指定的layout布局同样也可以被实例化。

5.层级限制:

  • 循环加载:布局是在UI线程中加载的,其原理是通过使用handler消息队列来实现线性加载。模块界面布局是有占位层级的,通过addView的方式来添加到界面中显示,就必须确定模块加载的顺序。这种方式好处在于非常直观地看清了全部模块的层级顺序,容易调整层级。缺点在于模块列表只能放置在application module中,需要完全暴露包名和路径带列表中,无法做到每个模块配置的绝对接耦。
  • 编译时注解列表:组件化列表配置中使用了编译时注解,规定了模块注解参数的排列规则,将每个模块的加载顺序和初始化配置都安排在每个模块编译时生成,Application启动时,通过注解信息排列加载顺序的方式来完成加载模块操作。
  • 懒加载:懒加载使用了ViewStub的方式来布局占位,好处在于一开始就可以给全部模块安排xml布局,优化页面加载速度和模块的加载的灵活性。
  • 层级优化策略:1.全部模块都使用ViewStub的方式来布局占位,好处在于一开始就可以给全部模块安排层级顺序,可以更合理地安排层级加载的顺序。线程池只能通过单线程顺序加载进行限制,可以有效利用多线程来优化加载速度,如选ViewGroup来布局占位,就可以通过addView的形式来添加多个view模块。2. 分发模块进化成多模版的机制。多模版的产生会让页面更加多样化,但是多样化就预示着模块会更加多变,模块间的灵活,需要充分考虑布局层级顺序和显示效果,实现局部的动态加载。

6.多模版设计:

使用脚本配置,可以把app启动时ModuleCenter汇总合成module信息列表,提前在编译期就构建完成Module信息列表。Android在运行期间可以简单读取脚本数据格式时json和xml的数据,考虑到数据传输速度,首选使用json数据,json读取数据的速度一般比xml快一倍以上。而且现在大多网络数据都使用json格式。

  • 编译时运行的compiler module是无法直接使用Java的json库的需要添加库的引用,如Gson;
  • 注解的元素不需要变更,需要编写制作json注解器。需要形成每个module的json文件;通过便利读取ModuleUnit的注解来编写一个JsonObject的对象,然后添加到JsonArray中,ModuleGroup分解为多个ModuleUnit执行以上的操作,然后将JsonArray对象信息写入到注解module的src/assets/center.json文件中;
  • 汇总各个module的center.json信息,然后保存一份到app module中;
  • 汇总各个module的center.json到app的center.json中,先读取每个module的center.json文件并转化为ModuleUnitBean数据,汇总到一个LinkedList中,然后通过列表排序,写入app module的center.json中。因为app module中并没有相应的注解,其注解器只会运行init方法,并不会process方法。
  • ModuleCenter需要添加相应的解析JsonObject的方法。

每个模块通过编译时注解生成一个关于自身布局json文件,模块被汇总到application module中编译时,在排序产生一个主json文件,记录各个层级启动/模块布局等信息,然后在app启动时读取json文件来加载模版信息。

动态加载:

脚本配置,再添加服务器配置就可以动态配置加载模版信息,这种设计可以通过服务器动态加载配置json文件来完成模块加载。只设置协议请求返回json文件就可以配置新的启动信息。

本地json文件再app加载后保存在assets目录中,json文件下载后应该保存在应用的/data/data/{packageName}/file中,再次获取时也应该在此处获取。

其流程是:

  • 启动app -> 读取沙盒缓存json文件 -> 配置模块;
  • 启动app -> 请求动态json -> 下载json -> 保存json文件到沙盒用于下次启动。

7.小结:

组件化分发在单界面中承担着非常复杂的多业务开发任务,而且提供了很好的解决方案,是代码模块接耦和人力资源接绑的一种新型高效的方案。

分发的核心思想:接耦配置和动态配置。

  • 接耦配置:将每个模块的配置信息在各自模块中生成,并且在启动初期汇总列表。模块信息对其他模块不可见,只在app启动时组装成整个列表信息。这样就算单一模块的编程也不会影响其他模块的正常配置;
  • 动态配置:模块层级的顺序是固定的,动态地实现模块乱序加载也能正常贴合到各自层级中。布局多变时,模块可以加载不同布局并且正常运行。可以使用外部信息,如服务器信息来调整分发模块的机制。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章