背景(为什么做)
在做组件化的过程中发现一个问题,之前我们都是把第三方库和一些初始化的逻辑写在Application,ok这样在单Module的项目是没有问题的,但是当拆成多Module项目的时候这个初始化逻辑在壳App的Application中得维护一份,然后在每个Module自己独立运行的时候也得维护一份,那么假如哪天需要修改初始化逻辑,这将是一个灾难。
为了解决这个问题,我们能不能把初始化逻辑划分给每个业务module自己维护自己所需的部分,在module独立运行或者组成app运行的时候自动触发初始化功能,答案是可以的。
怎么做
1. 方案选定
既然是把初始化逻辑划分到每个Module自己维护,然后在Application#onCreate()的时候自动触发,那我们要解决两个问题
- 如何找到每个Module需要执行的初始化逻辑
- 如何把找到的初始化逻辑在Application#onCreate()中触发
对于第一个问题很容易就能想到注解,用注解标注需要执行初始化逻辑的方法,便于找到
第二个问题的话则可以通过Transform + ASM或者APT将需要执行的逻辑在Application#onCreate()触发,这里我选择的是APT原因是ASM不太好上手而且是对字节码修改不好调试。
2. 大体思路
确定方案后,再来说说大体思路,每个业务Module创建一个类用来做初始化逻辑,方法名随意只需要标注上我们定义的注解,然后在编译期利用APT找到这些需要初始化的方法,利用JavaPoes生成的类去记录需要初始化的方法和所在类,再在运行时Application#onCreate()找到这些生成的类利用反射执行初始化逻辑。
3. 定义注解
接下来第一步是定义注解用来标注需要执行初始化操作的方法,考虑到不同模块的初始化逻辑可能有先后顺序和线程的要求,这里我在注解中加了两个参数
- priority:优先级配置,越大优先级越高,默认为1
- thread:运行线程配置,有MAIN, BACKGROUND可选,默认MAIN
/**
* 标记需要初始化的方法
* Created by rocketzly on 2019/7/17.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Init {
/**
* 初始化优先级,越大优先级越大
*
* @return
*/
int priority() default 1;
/**
* 初始化线程,默认主线程
*
* @return
*/
ThreadMode thread() default ThreadMode.MAIN;
}
4. 自定义AbstractProcessor
注解定义好了后,接下来要自定义用来处理该注解的AnnotationProcess
/**
* 组件初始化注解处理器
* Created by rocketzly on 2019/7/17.
*/
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions(InitConstant.INITIALIZER_MODULE_NAME)
@SupportedAnnotationTypes(InitConstant.SUPPORT_ANNOTATION_QUALIFIED_NAME)
public class ComponentInitializerProcessor extends AbstractProcessor {
private String moduleName;
private Filer filer;
private Messager messager;
private Types typeUtils;
private Elements elementUtils;
private final String APPLICATION_QUALIFIED_NAME = "android.app.Application";
private List<InitMethodInfo> syncList = new ArrayList<>(20);
private List<InitMethodInfo> asyncList = new ArrayList<>(20);
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
if (processingEnv.getOptions() == null) {
return;
}
moduleName = processingEnv.getOptions().get(InitConstant.INITIALIZER_MODULE_NAME);
if (moduleName == null || moduleName.isEmpty()) {
messager.printMessage(Diagnostic.Kind.ERROR, InitConstant.NO_MODULE_NAME_TIPS);
}
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations == null || annotations.size() == 0) {
return false;
}
for (Element element : roundEnv.getElementsAnnotatedWith(Init.class)) {
if (element == null) {
continue;
}
check(element);
analyze(element);
}
generate();
return true;
}
/**
* 检查element合法性
* 1. 检查element是否为ExecutableElement
* 2. 检查element是否为成员方法
* 3. 检查方法参数类型要么空参,要么只有一个参数Application
* 4. 检查方法所在类是否有空参构造方法
*/
private void check(Element methodElement) {
//1.检查element是否为ExecutableElement
if (ElementKind.METHOD != methodElement.getKind()) {
messager.printMessage(Diagnostic.Kind.ERROR, methodElement.getSimpleName() + "使用错误,@init只能用在方法上", methodElement);
}
//2.检查element是否为成员方法
if (ElementKind.CLASS != methodElement.getEnclosingElement().getKind()) {
messager.printMessage(Diagnostic.Kind.ERROR, methodElement.getSimpleName() + "方法无法使用,@init只能用在成员方法上", methodElement);
}
//3.检查方法参数类型要么空参,要么只有一个参数Application
List<? extends VariableElement> parameters = ((ExecutableElement) methodElement).getParameters();
if (parameters.size() > 1) {
messager.printMessage(Diagnostic.Kind.ERROR, methodElement.getSimpleName() + "方法参数个数有误,@init最多只支持一个参数的方法", methodElement);
}
if (parameters.size() != 0) {
TypeElement typeElement = elementUtils.getTypeElement(APPLICATION_QUALIFIED_NAME);
if (!typeUtils.isSameType(parameters.get(0).asType(), typeElement.asType())) {
messager.printMessage(Diagnostic.Kind.ERROR, methodElement.getSimpleName() + "方法参数类型有误,@init标注的方法只支持一个参数并且类型必须为Application", methodElement);
}
}
//4.检查方法所在类是否有空参构造方法
List<? extends Element> allMembers = elementUtils.getAllMembers((TypeElement) methodElement.getEnclosingElement());
boolean hasEmptyConstructor = false;
for (Element e : allMembers) {
if (ElementKind.CONSTRUCTOR == e.getKind() && ((ExecutableElement) e).getParameters().size() == 0) {
hasEmptyConstructor = true;
break;
}
}
if (!hasEmptyConstructor)
messager.printMessage(Diagnostic.Kind.ERROR, methodElement.getEnclosingElement().getSimpleName() + "没有空参构造方法,@init标注方法所在类必须有空参构造方法", methodElement.getEnclosingElement());
}
/**
* 分析被标注的方法,获取相关信息
* 1. 方法所在类的完全路径名
* 2. 方法的名字
* 3. 方法是否有参数
* 4. 方法上注解中的调用线程和优先级信息
* 封装为 {@link InitMethodInfo}
* 再根据线程区分是存储在同步或者异步list中
*
* @param element
*/
private void analyze(Element element) {
//获取该方法所在类的完全路径名
String className = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
//返回该方法的名字
String methodName = element.getSimpleName().toString();
//确定方法是否有参数
boolean isParams = ((ExecutableElement) element).getParameters().size() > 0;
//获取到方法上的注解
Init annotation = element.getAnnotation(Init.class);
//拿到调用线程
ThreadMode thread = annotation.thread();
//拿到调用优先级
int priority = annotation.priority();
if (ThreadMode.MAIN.equals(thread)) {
syncList.add(new InitMethodInfo(className, methodName, isParams, priority, thread));
} else {
asyncList.add(new InitMethodInfo(className, methodName, isParams, priority, thread));
}
}
/**
* 生成代码
* <p>
* 例如:
* public final class ComponentInitializerHelper_moduleA implements IInitMethodContainer {
* private List syncList = new ArrayList<InitMethodInfo>();
* <p>
* private List asyncList = new ArrayList<InitMethodInfo>();
* <p>
* public ComponentInitializerHelper_moduleA() {
* syncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit","sync10",true,10,ThreadMode.MAIN));
* asyncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit","async30",true,30,ThreadMode.BACKGROUND));
* }
*
* @Override public List<InitMethodInfo> getSyncInitMethodList() {
* return syncList;
* }
* @Override public List<InitMethodInfo> getAsyncInitMethodList() {
* return asyncList;
* }
* }
*/
private void generate() {
//生成字段
FieldSpec fieldSyncList = generateField(InitConstant.GENERATE_FIELD_SYNC_LIST);
FieldSpec fieldAsyncList = generateField(InitConstant.GENERATE_FIELD_ASYNC_LIST);
//初始化构造方法
MethodSpec.Builder constructorBuild = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC);
initConstructor(constructorBuild, InitConstant.GENERATE_FIELD_SYNC_LIST);
initConstructor(constructorBuild, InitConstant.GENERATE_FIELD_ASYNC_LIST);
MethodSpec constructorMethod = constructorBuild.build();
//生成方法
MethodSpec syncMethod = generatorMethod(InitConstant.GENERATE_METHOD_GET_SYNC_NAME);
MethodSpec asyncMethod = generatorMethod(InitConstant.GENERATE_METHOD_GET_ASYNC_NAME);
TypeSpec typeSpec = TypeSpec.classBuilder(InitConstant.GENERATE_CLASS_NAME_PREFIX + moduleName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(fieldSyncList)
.addField(fieldAsyncList)
.addMethod(syncMethod)
.addMethod(asyncMethod)
.addMethod(constructorMethod)
.addSuperinterface(ClassName.get(IInitMethodContainer.class))
.build();
JavaFile javaFile = JavaFile.builder(InitConstant.GENERATE_PACKAGE_NAME, typeSpec)
.build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 生成字段
* <p>
* 例如:
* private List syncList = new ArrayList<InitMethodInfo>();
*/
private FieldSpec generateField(String fieldName) {
return FieldSpec.builder(List.class, fieldName)
.addModifiers(Modifier.PRIVATE)
.initializer("new $T<$T>()", ArrayList.class, InitMethodInfo.class)
.build();
}
/**
* 初始化构造函数
* <p>
* 例如:
* public ComponentInitializerHelper_moduleA() {
* syncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit","sync10",true,10,ThreadMode.MAIN));
* asyncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit","async30",true,30,ThreadMode.BACKGROUND));
* }
*/
private void initConstructor(MethodSpec.Builder builder, String initFieldName) {
for (InitMethodInfo methodInfo : initFieldName.equals(InitConstant.GENERATE_FIELD_SYNC_LIST) ? syncList : asyncList) {
builder.addStatement("$N.add(new $T($S,$S,$L,$L,$T.$L))",
initFieldName,
InitMethodInfo.class,
methodInfo.className,
methodInfo.methodName,
methodInfo.isParams,
methodInfo.priority,
ThreadMode.class,
methodInfo.thread
);
}
}
/**
* 生成方法
* <p>
* 例如:
* @Override public List<InitMethodInfo> getSyncInitMethodList() {
* return syncList;
* }
*/
private MethodSpec generatorMethod(String methodName) {
return MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(ParameterizedTypeName.get(ClassName.get(List.class), ClassName.get(InitMethodInfo.class)))
.addStatement("return $N", methodName.equals(InitConstant.GENERATE_METHOD_GET_SYNC_NAME) ? InitConstant.GENERATE_FIELD_SYNC_LIST : InitConstant.GENERATE_FIELD_ASYNC_LIST)
.build();
}
}
我先检查了注解的用法,如果不符合约定则报错,然后扫描到每个Module中用@init标注的方法,根据@init中线程属性的不同存放在不同的list中,然后利用javapoet生成一个类去存储该Module需要执行初始化的类和方法,这里可以大概看一下生成的代码
public final class InitializerContainer_moduleA implements IInitMethodContainer {
private List syncList = new ArrayList<InitMethodInfo>();
private List asyncList = new ArrayList<InitMethodInfo>();
public InitializerContainer_moduleA() {
syncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit", "sync10", true, 10, ThreadMode.MAIN));
asyncList.add(new InitMethodInfo("com.rocketzly.modulea.ModuleAInit", "async30", true, 30, ThreadMode.BACKGROUND));
}
@Override
public List<InitMethodInfo> getSyncInitMethodList() {
return syncList;
}
@Override
public List<InitMethodInfo> getAsyncInitMethodList() {
return asyncList;
}
}
可以发现生成的类是实现了一个IInitMethodContainer接口,重写了它的方法对外提供了获取初始化方法的能力,之所以要实现该接口也是为了后面利用反射找到该类能转换为接口进行调用。
5. 调用
既然前面把需要初始化的方法都已经找到并存储在生成的类中了,那么接下来就是找到生成类获取到需要进行初始化的方法,然后根据优先级排序再在Application#onCreate()调用,对于找到生成类、排序、调用初始化方法逻辑我全封装在ComponentInitializer类中了
public class ComponentInitializer {
private boolean isDebug;
private ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>(16);
public ComponentInitializer(boolean isDebug) {
this.isDebug = isDebug;
Logger.setDebug(isDebug);
}
public void start(Application application) {
if (application == null) {
Logger.e("Application不能为null");
return;
}
Set<String> fileNameByPackageName = null;
try {
fileNameByPackageName = ClassUtils.getFileNameByPackageName(application, InitConstant.GENERATE_PACKAGE_NAME);
Logger.i("通过包名找到的类:" + fileNameByPackageName.toString());
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (fileNameByPackageName == null) {
Logger.e("未找到初始化方法容器类");
return;
}
List<InitMethodInfo> syncMethodList = new ArrayList<>();
List<InitMethodInfo> asyncMethodList = new ArrayList<>();
for (String className : fileNameByPackageName) {
try {
IInitMethodContainer initMethodContainer = (IInitMethodContainer) Class.forName(className).newInstance();
syncMethodList.addAll(initMethodContainer.getSyncInitMethodList());
asyncMethodList.addAll(initMethodContainer.getAsyncInitMethodList());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
Collections.sort(syncMethodList);
Collections.sort(asyncMethodList);
Logger.i("同步初始化方法:" + syncMethodList.toString());
Logger.i("异步初始化方法:" + asyncMethodList.toString());
execute(application, syncMethodList, asyncMethodList);
}
private void execute(final Application application, List<InitMethodInfo> syncMethodList, final List<InitMethodInfo> asyncMethodList) {
final ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
execute(application, asyncMethodList);
executor.shutdown();
}
});
execute(application, syncMethodList);
}
private void execute(Application application, List<InitMethodInfo> list) {
for (InitMethodInfo methodInfo : list) {
Object instance = null;
if (!(map.containsKey(methodInfo.className))) {
try {
instance = Class.forName(methodInfo.className).newInstance();
map.put(methodInfo.className, instance);
} catch (Exception e) {
e.printStackTrace();
}
}
if ((instance = map.get(methodInfo.className)) == null) {
Logger.e(methodInfo.className + "实例获取失败");
continue;
}
try {
Method method = instance.getClass().getMethod(methodInfo.methodName,
methodInfo.isParams ? new Class<?>[]{Application.class} : new Class<?>[]{});
method.setAccessible(true);
method.invoke(instance, methodInfo.isParams ? new Object[]{application} : new Object[]{});
Logger.i(methodInfo.className.substring(methodInfo.className.lastIndexOf(".") + 1) + "#" + methodInfo.methodName + "()调用成功,调用线程:" + Thread.currentThread().getName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
Logger.e(methodInfo.className + "#" + methodInfo.methodName + "()方法未找到");
} catch (IllegalAccessException e) {
e.printStackTrace();
Logger.e(methodInfo.className + "#" + methodInfo.methodName + "()方法无法访问");
} catch (InvocationTargetException e) {
e.printStackTrace();
Logger.e(methodInfo.className + "#" + methodInfo.methodName + "()方法调用失败");
}
}
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private boolean isDebug = false;
public Builder debug(boolean isDebug) {
this.isDebug = isDebug;
return this;
}
public void start(Application application) {
new ComponentInitializer(isDebug).start(application);
}
}
}
外层只需要在BaseApplication#onCreate中触发下
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ComponentInitializer.builder()
.debug(true)
.start(this);
}
}
然后无论是壳App还是Module的Application都继承BaseApplication即完成了整个流程。
遇到哪些问题
1. 如何每个模块生成不同的类名
APT生成的代码是每个Module都会生成一个,对应于我们这个则是每个业务模块都会生成一个记录该模块初始化信息的类,然后在打包的时候都打进APK,所以如果类名一样会报出存在相同类名的错误,那么要想每个模块生成的类名不同,则可以利用APT能读取Gradle配置的特性,在Gradle配置该模块的名称,然后在生成的类名中加上该名称即可避免类名重复
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [INITIALIZER_MODULE_NAME: project.getName()]
}
}
}
}
在gradle中新增一个键为INITIALIZER_MODULE_NAME,值为project.getName()模块名称的map
moduleName = processingEnv.getOptions().get(InitConstant.INITIALIZER_MODULE_NAME);
然后在AnnotaionProcess中读取到这个值添加到生成的类名中即可避免重复。
2. 如何找到每个模块生成类
由于每个业务模块都会生成一个用于记录该模块初始化方法信息的类,那么要想在apk中找到这些类是比较困难的,不过好在业内有现成利用APT做组件化的例子Arouter,于是我就去他的源码中看他是如果获取到不同Module生成的类,发现他是所有生成的类在同一个包中,然后利用DexFile去加载dex然后遍历dex中class的className是不是规定包名开头的,如果是则是我们要找的类
public class ClassUtils {
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
private static final String PREFS_FILE = "multidex.version";
private static final String KEY_DEX_NUMBER = "dex.number";
private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static SharedPreferences getMultiDexPreferences(Context context) {
return context.getSharedPreferences(PREFS_FILE, Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths) {
Executors.newCachedThreadPool().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
Logger.e("Scan map file in dex files made error." + ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
Logger.d("Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
// 通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
if (!isVMMultidexCapable()) {
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
}
}
// if (ARouter.debuggable()) { // Search instant run support only debuggable
// sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
// }
return sourcePaths;
}
/**
* Get instant run dex path, used to catch the branch usingApkSplits=false.
*/
private static List<String> tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) {
List<String> instantRunSourcePaths = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs) {
// add the split apk, normally for InstantRun, and newest version.
instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs));
Logger.d("Found InstantRun support");
} else {
try {
// This man is reflection from Google instant run sdk, he will tell me where the dex files go.
Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths");
Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class);
String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName);
File instantRunFilePath = new File(instantRunDexPath);
if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) {
File[] dexFile = instantRunFilePath.listFiles();
for (File file : dexFile) {
if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex")) {
instantRunSourcePaths.add(file.getAbsolutePath());
}
}
Logger.d("Found InstantRun support");
}
} catch (Exception e) {
Logger.e("InstantRun support error, " + e.getMessage());
}
}
return instantRunSourcePaths;
}
/**
* Identifies if the current VM has a native support for multidex, meaning there is no need for
* additional installation by this library.
*
* @return true if the VM handles multidex
*/
private static boolean isVMMultidexCapable() {
boolean isMultidexCapable = false;
String vmName = null;
try {
if (isYunOS()) { // YunOS需要特殊判断
vmName = "'YunOS'";
isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
} else { // 非YunOS原生Android
vmName = "'Android'";
String versionString = System.getProperty("java.vm.version");
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException ignore) {
// let isMultidexCapable be false
}
}
}
}
} catch (Exception ignore) {
}
Logger.i("VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
return isMultidexCapable;
}
/**
* 判断系统是否为YunOS系统
*/
private static boolean isYunOS() {
try {
String version = System.getProperty("ro.yunos.version");
String vmName = System.getProperty("java.vm.name");
return (vmName != null && vmName.toLowerCase().contains("lemur"))
|| (version != null && version.trim().length() > 0);
} catch (Exception ignore) {
return false;
}
}
}
可以看到里面有不少适配的代码,要是没有Arouter提供的这个工具类,自己不知道要踩多少坑。
3. 混淆
由于我这里用到反射去调用初始化方法,所以需要混淆的配置,生成的类不能被混淆,@init注解不能被混淆,被@init注解标注的类和方法不能被混淆,一开始我是借鉴的Eventbus的混淆规则然后被-keepattributes *Annotation*
坑惨了,以为这个能保证注解不被混淆,结果无效,导致@init注解被优化掉了,从而间接导致
-keepclasseswithmembers class * {
@rocketzly.componentinitializer.annotation.Init <methods>;
}
这个配置也就失效了,然后我初始化逻辑又是在运行时通过反射调用的没有直接引用也就被优化掉了,最后借鉴了@keep注解的配置解决了这个问题
#rocketzly.componentinitializer.generate包名下的类不被混淆
-keep class rocketzly.componentinitializer.generate.** { *; }
-keep class rocketzly.componentinitializer.annotation.Init
#用@init标记的类和方法不被混淆
-keepclasseswithmembers class * {
@rocketzly.componentinitializer.annotation.Init <methods>;
}
缺陷
整个库开发完成了,现在回头看看发现有个缺陷就是每个模块自己处理自己的初始化逻辑,那不可避免的A模块可能有初始化地图,B模块也要初始化地图,而我这边记录的是每个模块需要执行的初始化方法,然后在Application#onCreate()去触发,没有办法区分出A模块和B模块都需要初始化地图,所以就会导致地图初始化两次,目前想的解决办法就是对所有第三方库的初始化进行一个封装,通过一个标记位判断是否初始化过,如果初始化过则return,如果大家有更好的办法欢迎留言。
最后
当然是放出库的地址 ComponentInitializer
然后分享下学习APT的资料
特别感谢Arouter,中间几个问题都是查看Arouter源码解决的。