利用APT實現一個組件初始化庫

背景(爲什麼做)

在做組件化的過程中發現一個問題,之前我們都是把第三方庫和一些初始化的邏輯寫在Application,ok這樣在單Module的項目是沒有問題的,但是當拆成多Module項目的時候這個初始化邏輯在殼App的Application中得維護一份,然後在每個Module自己獨立運行的時候也得維護一份,那麼假如哪天需要修改初始化邏輯,這將是一個災難。

爲了解決這個問題,我們能不能把初始化邏輯劃分給每個業務module自己維護自己所需的部分,在module獨立運行或者組成app運行的時候自動觸發初始化功能,答案是可以的。

怎麼做

1. 方案選定

既然是把初始化邏輯劃分到每個Module自己維護,然後在Application#onCreate()的時候自動觸發,那我們要解決兩個問題

  1. 如何找到每個Module需要執行的初始化邏輯
  2. 如何把找到的初始化邏輯在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源碼解決的。

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