框架手写系列---apt方式实现ARouter框架

一、ARouter

ARouter是阿里开源的组件通讯框架,在组件化开发上也是十分常用的框架之一。

它的主要作用是各个activity之间,无需直接依赖,就可以直接跳转与传参。主要用处是为组件化的解耦,添砖加瓦。

二、ARouter原理

ARouter的核心原理,十分简单:用注解标识各个页面,注解处理器将该注解对应的页面存储到一个统一的map集合中。当需要页面跳转时,根据跳转的入参,从该map集合中取到对应的页面和传参,并跳转。

核心在于,如何构建一个合理的map集合。

三、手写实现

根据原理分析,我们手写一个BRouter(ARouter的核心实现)。用一个单例类BRouter存储map集合,并在该类中,实现跳转与传参。

1、首先还是注解的定义

//针对的是最外层的类
@Target(ElementType.TYPE)
//编译时
@Retention(RetentionPolicy.CLASS)
public @interface Path {
    String value() default "";
}

2、注解处理器的编写

//注解处理器的依赖,此处有注意点:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(path: ':brouter-annotation')
    //3.6+的android studio需要按以下方式依赖auto-service
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
}

@AutoService(Processor.class)
public class BRouterProcessor extends AbstractProcessor {
    //定义包名
    private static final String CREATE_PACKAGE_NAME = "com.sunny.brouter.custom";
    //定义基础类名
    private static final String CREATE_CLASS_NAME = "RouterUtil";
    //后续文件操作使用
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //从外部传入参数中,获取filer
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //获取注解的类
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Path.class);
        if(elementsAnnotatedWith.size() < 1){
            return false;
        }
        Map<String,String> collectMap = new HashMap<>();
        for(Element element : elementsAnnotatedWith){
            //类节点
            TypeElement typeElement = (TypeElement) element;
            String className = typeElement.getQualifiedName().toString();
            String key = element.getAnnotation(Path.class).value();
            if(collectMap.get(key)==null){
                //注解内容作为key,类名作为value,存入map中--此map是单个module的map
                collectMap.put(key,className+".class");
            }
        }
        
        Writer writer = null;
        try {
            //为避免类名重复,生成的类名加上动态时间戳---此处实现与ARouter本身不一致,但更简单。
            //避免了从build.gradle中传递参数的步骤
            String activityName = CREATE_CLASS_NAME + System.currentTimeMillis();
            JavaFileObject sourceFile = filer.createSourceFile(CREATE_PACKAGE_NAME + "." + activityName);
            writer = sourceFile.openWriter();
            //代码生成
            StringBuilder routerBuild = new StringBuilder();
            for (String key : collectMap.keySet()) {
                routerBuild.append("        BRouter.getInstance().addRouter(\""+key+"\", "+collectMap.get(key)+");\n");
            }


            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("package "+CREATE_PACKAGE_NAME+";\n");
            stringBuilder.append("import com.sunny.brouter.BRouter;\n" +
                    "import com.sunny.brouter.IRouter;\n" +
                    "\n" +
                    "public class "+activityName+" implements IRouter {\n" +
                    "\n" +
                    "    @Override\n" +
                    "    public void addRouter() {\n" +
                    routerBuild.toString() +

                    "    }\n" +
                    "}");
            writer.write(stringBuilder.toString());

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(writer != null){
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return processingEnv.getSourceVersion();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(Path.class.getCanonicalName());
        return types;
    }
}

 此处的核心有两点:(1)如何生成一个map  (2)生成的类的类名处理,避免重复

3、BRouter单例类的实现

至此,各个module中,已经生成了对应的类,并把各个的map信息,添加到了BRouter这个类中。

BRouter的具体实现也很简单:

(1)收集各个module中map的key与value。

(2)加上页面跳转方法。

(3)Context参数的传递(startActivity需要用到该参数)。

public class BRouter {
    private static final String TAG = "BRouter";
    private static final String CREATE_PACKAGE_NAME = "com.sunny.brouter.custom";
    private static volatile BRouter router;
    private Map<String, Class<?>> routerMap;
    private Context context;

    private BRouter() {
        routerMap = new HashMap<>();
    }

    public static BRouter getInstance() {
        if (null == router) {
            synchronized (BRouter.class) {
                router = new BRouter();
            }
        }
        return router;
    }
    //该方法用于:(1)传入context (2)调用各个module组件中的addRouter方法
    public void init(Context context) {
        this.context = context;
        try {
            //根据包名查找所有的class
            Set<String> classes = getFileNameByPackageName(context, CREATE_PACKAGE_NAME);

            if (classes.size() > 0) {
                for (String classStr : classes) {
                    Class<?> aClass = Class.forName(classStr);
                    Object o = aClass.newInstance();
                    if (o instanceof IRouter) {
                        ((IRouter) o).addRouter();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //各个Module中调用该方法,把key,value存入map
    public void addRouter(String key, Class<?> activityClass) {
        if (routerMap.get(key) == null) {
            routerMap.put(key, activityClass);
        }
    }
    //页面的跳转与传参
    public void jump(String key, Bundle bundle) {
        Class<?> jumpToClass = routerMap.get(key);
        if (jumpToClass == null) {
            return;
        }
        Log.e(TAG, "jump: " + jumpToClass.getName());
        Intent intent = new Intent(context, jumpToClass);
        if (bundle != null) {
            intent.putExtras(bundle);
        }
        if (context != null) {
            context.startActivity(intent);
        }
    }

...
}

4、补充说明:BRouter中根据包名查找所有的class

 可以参考ARouter中的做法,在BRouter中添加以下代码,附录如下:

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());
        ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size());
        for (final String path : paths) {
            threadPoolExecutor.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) {
                        Log.e("ARouter", "Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null != dexfile) {
                            try {
                                dexfile.close();
                            } catch (Throwable ignore) {
                            }
                        }

                        parserCtl.countDown();
                    }
                }
            });
        }

        parserCtl.await();

        Log.d(TAG, "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() + "'");
                }
            }
        }
        return sourcePaths;
    }

    /**
     * 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) {

        }

        Log.i(TAG, "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功能与原理都十分类似的BRouter。

  它的关键词是解耦与组件化应用。

猜你喜欢

转载自blog.csdn.net/yangzhaomuma/article/details/107005791