前言:
组件化是安卓目前很流行的一门技术,其目的是避免复杂的业务逻辑交织到一起,相互影响。通过解耦,让每个子项目都是一个独立的工程,即使其余模块出现问题,也不会影响这个子模块的运行。
一.为什么要实现通用接口实现类的解耦
我们首先抛出第一个问题,什么要实现通用接口实现类的解耦?不解耦可以吗?
既然这样,那我们先来看一下如果不实用APT解耦,我们该怎么做?举一个现实的场景:应用启动时,各个模块需要初始化。
首先构造ServiceProviderInterface接口,定义如下两个方法,分别代表主线程要执行的初始化任务和子线程要执行的初始化任务。
public interface ServiceProviderInterface {
public void onMainThread(Context context);
public void onSelfThread(Context context);
}
然后每个模块去实现这个接口,我们这里就举两个例子:
AServiceProviderImpl:
public class AServiceProviderImpl implements ServiceProviderInterface {
@Override
public void onMainThread(Context context) {
//A模块主线程初始化任务
Log.i("lxltest", "onMainThread A");
}
@Override
public void onSelfThread(Context context) {
/A模块子线程初始化任务
Log.i("lxltest", "onSelfThread A");
}
}
BServiceProviderImpl:
public class BServiceProviderImpl implements ServiceProviderInterface {
@Override
public void onMainThread(Context context) {
Log.i("lxltest", "onMainThread B");
}
@Override
public void onSelfThread(Context context) {
Log.i("lxltest", "onSelfThread B");
}
}
然后我们应该在Application中启动时完成如下操作:
//Application.onCreate时调用
private void normalLaunch() {
ServiceProviderInterface aServiceProvider = new AServiceProviderImpl();
ServiceProviderInterface bServiceProvider = new BServiceProviderImpl();
List<ServiceProviderInterface> list = new ArrayList<>();
list.add(aServiceProvider);
list.add(bServiceProvider);
for (ServiceProviderInterface serviceProviderInterface : list) {
serviceProviderInterface.onMainThread(this);
}
new Thread(() -> {
for (ServiceProviderInterface serviceProviderInterface : list) {
serviceProviderInterface.onSelfThread(instance);
}
}).start();
}
首先把A,B都new出来,然后加入到集合中,在启动时去遍历调用。
看起来也挺简单,但是其实却存在这样几个问题:
1.由于项目之间实现了解藕,而Application一般在主项目中,而A,B在子项目中。主项目是不会依赖子项目的,所以如果实现了组件化,这里很有可能无法直接引用AServiceProviderImpl和BServiceProviderImpl
2.每次创建一个新的ServiceProviderInterface接口的实现类,都要改一下Application中的代码,容易忘记,一旦忘了就会报错。
3.有的公司模块化比较彻底,很有可能主项目和子项目直接是两个项目,代码提交权限都受限制。
4.这里只举了两个例子,两个模块,如果是20个模块呢?甚至200个模块呢?这手写代码的成本就有点高了。
所以,有没有通用的解决方案呢?当然有,这个方案就是APT,让程序帮助我们来写代码。
二.APT实现路由类的解耦
主要分为以下几步:
1.创建相关注解类
2.使用APT处理注解标记的类
3.利用ServiceLoader完成相关接口类的加载。
2.1 创建相关注解类
首先创建一个注解library,其余模块都依赖这个注解library。
然后在library中创建注解类,注解的作用就是在编译时主动去识别相关被标记的类,知道该处理哪些类。
2.2 使用APT处理注解标记的类
创建router_processor模块,这个模块类型选择java项目。
引入auto-service,build.gradle中配置如下:
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
dependencies {
implementation 'com.squareup:javapoet:1.11.1'
api 'com.google.auto.service:auto-service:1.0-rc5'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
implementation project(path: ':annotationLibrary')
}
最后创建注解处理类:ApplicationInitProcessor,代码如下:
/**
* 注解处理类,处理com.xt.router_api.ApplicationInit类型注解。
* 凡是被@ApplicationInit注解声明的类,加入到services/com.xt.client.inter.ServiceProviderInterface中,启动自动实现初始化。
*/
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.xt.router_api.ApplicationInit")
@SupportedOptions("moduleName")
public class ApplicationInitProcessor extends AbstractProcessor {
Filer filer;
String TAG = "ApplicationInitProcessor";
boolean isAdd = false;
String servicesPath;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
if (annotations.isEmpty()) {
return false;
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "TAG");
//所有带ApplicationInit注解的类
Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(ApplicationInit.class);
for (Element e : set) {
String name = e.getSimpleName().toString();
Element enclosingElement = e.getEnclosingElement();
String packageName;
if (enclosingElement instanceof PackageElement) {
packageName = ((PackageElement) enclosingElement).getQualifiedName().toString();
} else {
packageName = ((TypeElement) enclosingElement).getQualifiedName().toString();
}
addToList(packageName + "." + name);
}
return true;
}
//针对注解中的内容生成类
private void addToList(String name) {
/**
* 临时文件目录
* DemoClient/app/build/intermediates/java_res/debug/out/META-INF/services/com.xt.client.inter.ServiceProviderInterface
*/
try {
if (servicesPath == null) {
FileObject resource = filer.createResource(StandardLocation.SOURCE_OUTPUT, "a", "a");
String path = resource.toUri().getPath();
servicesPath = path.substring(0, path.indexOf("app") + 3);
servicesPath += "/build/intermediates/java_res/debug/out/META-INF/services";
}
File folder = new File(servicesPath);
if (!folder.exists()) {
folder.mkdir();
}
File file = new File(folder.getAbsolutePath() + File.separator + "com.xt.client.inter.ServiceProviderInterface");
StringBuilder builder = new StringBuilder();
if (isAdd) {
builder.append("\n");
}
//第一次的的时候覆盖,后面追加
FileWriter fileWriter = new FileWriter(file, isAdd);
isAdd = true;
builder.append(name);
System.out.println(TAG + ":append:" + builder);
fileWriter.append(builder.toString());
fileWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
里面主要有两个方法:
init:项目开始编译时会调用这个方法。
process:每次遇到ApplicationInit类型注解时,都会调用这个方法进行处理。
实际项目中,这里我遇到一个问题,就是每次遇到ApplicationInit注解类,都会调用process方法,但是 com.xt.client.inter.ServiceProviderInterface文件我只需要创建一份。但是又不能直接判断如果文件不为空直接追加,万一是上一次的缓存文件呢?
所以我这里采用了一个变通的方法,判断如果process是第一次执行,则执行覆盖逻辑,后续全部执行追加逻辑。(如果有更好的方式,也欢迎给我建议)
//第一次的的时候覆盖,后面追加
FileWriter fileWriter = new FileWriter(file, isAdd);
这样重新rebuild一下项目,我们就可以发现在下面文件夹中,会生成一份文件。
app/build/intermediates/java_res/debug/out/META-INF/services/
这份文件内容如下:
说明文件生成成功了。我们在看最终的APK文件中,也果然存在这份文件:
2.3 利用ServiceLoader完成相关接口类的加载
最终的用法就很简单了,直接使用ServiceLoader加载对应接口类就好,ServiceLoader会自动通过反射生成相关的实现类,并且加入到返回集合中。
这里稍微扩展一点,ServiceLoader其实去读取的com.xt.client.inter.ServiceProviderInterface文件中的内容,所以上一步的生成时很必要的。
private void aptLaunch() {
ServiceLoader<ServiceProviderInterface> load = ServiceLoader.load(ServiceProviderInterface.class);
for (ServiceProviderInterface serviceProviderInterface : load) {
serviceProviderInterface.onMainThread(this);
}
new Thread(() -> {
for (ServiceProviderInterface serviceProviderInterface : load) {
serviceProviderInterface.onSelfThread(instance);
}
}).start();
}
三.相关代码:
DEMO工程:
https://github.com/aa5279aa/android_all_demo
router_processor库:
https://github.com/aa5279aa/android_all_demo/tree/master/DemoClient/router_processor
Application调用处代码:
四.扩展:
其实利用APT还可以做很多事情,甚至可以在编译的时候动态生成JAVA文件并且进行编译。
我们下一篇就来讲解如果动态的生成java文件并且编译打包到APK中。