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

Posted 战国剑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架手写系列---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。

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

以上是关于框架手写系列---apt方式实现ARouter框架的主要内容,如果未能解决你的问题,请参考以下文章

框架手写系列---apt注解处理器方式实现ButterKnife框架

框架手写系列---通过反射手写EventBus框架

Arouter 源码学习 1

揭秘ARouter路由机制,源码+原理+手写框架

框架手写系列---AspectJ方式实现埋点上传框架

框架手写系列---Asm方式实现日志插入