Android:路由的实现

本文详细介绍了Android项目中路由的实现,从初级路由的简单实现到中级路由的注解处理器和JavaPoet技术的应用,展示了如何通过动态生成代码来管理组件间的跳转,降低耦合。通过注解处理器收集所有需要路由的Activity,并在编译期间自动生成路由表,提高了代码的自动化程度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

路由的用途

使用路由是因为项目实现了组件化,组件化一般分三层,app主层,业务层,基础组件层,层层之间是向下依赖,平级之间不进行依赖,保证了分层隔离,避免代码的耦合。

路由分为三个级别:初级路由,中级路由,高级路由

初级路由

一:初级路由:通过map保存所有的activity页面,跳转时候通过key获取对应的Activity,然后进行跳转。

1)首先我们需要在基础层module里,写Router:

public class MyRouter {

    private final static HashMap<String, Class<? extends Activity>> map 
= new HashMap<>();

    public static void add(String key, Class<? extends Activity> clzss) {
        map.put(key, clzss);
    }

    public static void go(Activity activity, String key) {
        Class<? extends Activity> clzss = map.get(key);
        activity.startActivity(new Intent(activity, clzss));
    }
}

2)然后在 Application 中,将所有的Activity添加到map中。app module 层可以获取所有业务层的Activity

private void initRouter() {
     MyRouter.add("/demo/BBSActivity", BBSActivity.class);
     MyRouter.add("/demo/MainActivity", MainActivity.class);
}

3)使用

MyRouter.go(BBSActivity.this, "/demo/MainActivity");

非常的简单,唯一的缺点是需要手动的添加页面。

中级路由

中级路由:通过 apt + javaPoet 技术完成

技术文章支持:

https://blog.csdn.net/cpcpcp123/article/details/107891817

https://blog.csdn.net/LVXIANGAN/article/details/88350717

https://juejin.cn/post/6881116198889586701

https://www.jianshu.com/p/a2c900373cb8

apt: 是注解处理器,可以在编译器获取项目中的所有注解,然后对注解的地方进行加工。

javaPoet:是动态生成代码的技术,我们获取的注解,通过javapoet生成我们想要编写的代码。

首先我们需要创建三个moudel:

1)router_annotation:注解模块,用来声明需要路由的页面

2)router_compiler:注解处理器,为了拿到注解信息因此需要依赖各个模块,在编译期间根据各个模块声明的注解,利用javaPoet产生路由信息

3)router_core:存储各个module中声明过注解后,在项目编译期间生成的路由表信息

1)首先我们需要创建一个java moudel 的router_annotation,用来编写注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)//编译时注解
public @interface RouterPath {
    String path();
}

2)然后创建一个java moudel 的 router_compiler,依赖 router_annotation 模块,用来写注解编译器,生成代码逻辑

第一步:在gradle中添加包依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.squareup:javapoet:1.7.0' //这个库的主要作用就是帮助我们通过类调用的形式来生成代码
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    implementation project(path: ':router_annotation')
}

第二步:新建一个 AnnotationCompiler 类,依赖 AbstractProcessor 类,然后实现其中的方法:

@AutoService(Processor.class)
public class AnnotationCompiler extends AbstractProcessor {

    private Filer filer; //生成文件对象(新建文件就靠他了)

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {

        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    //声明返回要处理哪个注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(RouterPath.class.getCanonicalName());
        return types;
    }

    //支持Java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //注解处理器的核心
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        ....
        return false;
    }
}

第三步:编写注解处理器核心逻辑,核心逻辑包括两个部分,一个是获取 对应注解所在的类,然后拿到注解里面的值,然后放入map中。

// 获取项目中所有的RouterPath注解元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(RouterPath.class);

Map<String, String> map = new HashMap<>();

for (Element element : elementsAnnotatedWith){

     //com.qiyi.comicflutter.TwoActivity
     TypeElement typeElement = (TypeElement) element;
     System.out.println("MALEI :"  + typeElement.toString());

     //@com.qiyi.router_animation.RouterPath(path=/demo/TwoActivity)
     RouterPath annotation = typeElement.getAnnotation(RouterPath.class);
     System.out.println("MALEI :"  + annotation.toString());

     // /demo/TwoActivity
     String key = annotation.path(); //注解内部值
     System.out.println("MALEI :"  + key);

     // com.qiyi.comicflutter.TwoActivity
     String activityName = typeElement.getQualifiedName().toString(); //包名+类名
     System.out.println("MALEI :"  + activityName);
      //( " /demo/TwoActivity " ,com.qiyi.comicflutter.TwoActivity );
     map.put(key, activityName);
}

 // 以上的代码的工作就是通过注解处理器,获取全局注解,然后将 注解内部的值做为key,类作为value,放入map中

第四步:动态生成代码(先随便生成一个类,看一下)

 if(map.size() > 0){

       Writer writer = null;
       String utilsName = "ActivityUtils" ;
       try {
          JavaFileObject javaFileObject = filer.createSourceFile("com.qiyi.router_core." + utilsName);
          writer = javaFileObject.openWriter();  //创建一个空类
          //编写类中代码
          writer.write("package com.qiyi.router_core;\n" +

                            "\n"
                            + "import com.qiyi.router_core.ARouter;\n"
                            + "import com.qiyi.router_core.IRoute;\n"
                            + "\n"
                            + "public class " + utilsName + " implements IRoute {\n"
                            + "\n" +
                            " @Override\n" +
                            " public void putActivity() {"
                            + "\n");

            writer.write("}\n}");
  } catch (IOException e) {

        e.printStackTrace();

  }finally {

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

        }
     }
  }
 }

第五步:新建 router_core 执行我们动态生成文件的代码

1)创建接口IRouter

public interface IRoute {
    void putActivity();
}

2)创建ARouter

public class ARouter {
    private Map<String, Class<? extends Activity>> activityList;
    private Application context;

    private static class Holder {
        private static ARouter aRouter = new ARouter();
    }

    private ARouter() {
        activityList = new HashMap<>();
    }

    public static ARouter getInstance() {
        return Holder.aRouter;
    }

    //activity对象存入List
    public void putActivity(String path, Class<? extends Activity> clazz) {
        if (path == null || clazz == null) {
            return;
        }
        activityList.put(path, clazz);
    }

    public void init(Application application) {
        this.context = application;
        try {
            Set<String> className = ClassUtils.getFileNameByPackageName(context,"com.qiyi.router_core");
            for (String name : className) {
                try {
                    Class<?> aClass = Class.forName(name);
                    //判断当前类是否是IRouter的实现类
                    if(IRoute.class.isAssignableFrom(aClass)){
                        IRoute iRoute= (IRoute) aClass.newInstance();
                        iRoute.putActivity();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //跳转
    public void jupmActivity(String path, Bundle bundle) {
        Class<? extends Activity> aClass = activityList.get(path);
        if (aClass == null) {
            return;
        }
        Intent intent = new Intent().setClass(context, aClass);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (bundle != null) {
            intent.putExtra("bundle", bundle);
        }
        context.startActivity(intent);
    }
}

3)创建ClassUtils

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) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexfile = null;

                    try {
                        if (path.endsWith(EXTRACTED_SUFFIX)) {
                            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();
        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() + "'");
                }
            }
        }
        sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
       /* 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));
            //Log.d(Consts.TAG, "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());
                        }
                    }
                    // Log.d(Consts.TAG, "Found InstantRun support");
                }

            } catch (Exception e) {
                //Log.e(Consts.TAG, "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) {
        }
        //Log.i(Consts.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;
        }
    }
}

4)创建DedalutPoolExecuter

public class DefaultPoolExecutor extends ThreadPoolExecutor {
    //    Thread args
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int INIT_THREAD_COUNT = CPU_COUNT + 1;
    private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT;
    private static final long SURPLUS_THREAD_LIFE = 30L;

    private static DefaultPoolExecutor instance;

    public static DefaultPoolExecutor getInstance() {
        if (null == instance) {
            synchronized (DefaultPoolExecutor.class) {
                if (null == instance) {
                    instance = new DefaultPoolExecutor(
                            INIT_THREAD_COUNT,
                            MAX_THREAD_COUNT,
                            SURPLUS_THREAD_LIFE,
                            TimeUnit.SECONDS,
                            new ArrayBlockingQueue<Runnable>(64),
                            new DefaultThreadFactory());
                }
            }
        }
        return instance;
    }

    private DefaultPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            }
        });
    }

    /*
     *  线程执行结束,顺便看一下有么有什么乱七八糟的异常
     *
     * @param r the runnable that has completed
     * @param t the exception that caused termination, or null if
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }
        if (t != null) {
        }
    }
}

5)创建 DefalutThreadFactory

public class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);

    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final String namePrefix;

    public DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "ARouter task pool No." + poolNumber.getAndIncrement() + ", thread No.";
    }

    public Thread newThread(@NonNull Runnable runnable) {
        String threadName = namePrefix + threadNumber.getAndIncrement();
        Thread thread = new Thread(group, runnable, threadName, 0);
        if (thread.isDaemon()) {   //设为非后台线程
            thread.setDaemon(false);
        }
        if (thread.getPriority() != Thread.NORM_PRIORITY) { //优先级为normal
            thread.setPriority(Thread.NORM_PRIORITY);
        }

        // 捕获多线程处理中的异常
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
            }
        });
        return thread;
    }
}

使用,给每个module 添加依赖

  implementation project(path: ':router_annotation')
    implementation project(path: ':router_core')
    annotationProcessor project(path: ':router_compiler') //注解处理器使用 annotationProcessor

build下,我们看到生成的代码:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值