本文将帮助你全面掌握Android组件化的核心!

 2024-02-21 05:01:24  阅读 0

路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

我个人的理解是,在前端开发中,路由就是能够将一个字符串映射到对应的业务。 APP的路由盒首先可以收集各个组件的路由,并生成路由表。 然后,它可以根据外部输入的字符串匹配路由表中相应的页面或服务,进行跳转或调用,并提供返回值等,表示如下:

安卓应用开发框架_安卓开发不用new的框架_安卓开发框架mvvm

因此,一个基本的路由框架必须具备以下能力:

1、APP路由扫描注册逻辑。

2.路由跳转页面能力。

3、路由呼叫服务能力。

在APP中,路由页面时,往往需要判断是否登录等额外的认证逻辑。 因此,还需要提供拦截逻辑,比如登录。

2、第三方路由框架对APP的要求是否强烈?

答:不会,系统原生提供路由功能,但功能较少。 稍大的应用程序使用三方路由框架。

系统本身提供页面跳转能力:比如对于工具类APP或者独立APP来说,这种方式完全足够了,不需要专门的路由框架。 那么为什么很多APP仍然使用路由框架呢? 这与APP的本质以及路由框架的优势有关。 例如,淘宝、京东、美团等大型APP无论是APP功能还是研发团队规模都非常庞大。 不同的业务往往由不同的团队维护,并采用基于组件的开发方式。 ,最后集成到APK中。

多个团队往往涉及业务之间的互动,比如从电影票业务跳到食品业务。 但两个业务是两个独立的研发团队,代码实现完全隔离。 那么如何沟通呢? 首先想到的就是将其引入到代码中,但这会打破低耦合的初衷,还可能引入各种问题。

比如部分业务是外包团队完成的,这就涉及到代码安全问题,所以我们还是希望以黑匣子的方式来调用目标业务,这就需要中转路由支持,所以国内很多APP都采用框架式路由。 其次,我们的各种跳转规则不希望与具体的实现类相关。 比如当跳转比较详细的时候,我们并不想知道到底是实现了哪一个。 我们只需要将一个字符串映射到它即可。 这个适合H5,或者后端开发处理跳转的时候,非常标准。

3、原生路由的局限性:功能单一、扩展灵活性差、协作困难

传统的路由基本上仅限于路由或者启动服务。 比如传统路由有什么缺点:有两种用法,一种是显式的,一种是隐式的。 显式调用如下:


import com.snail.activityforresultexample.test.SecondActivity;

public class MainActivity extends AppCompatActivity {

    void jumpSecondActivityUseClassName(){
    
        Intent intent =new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
    }

显式调用的缺点很明显,那就是必须严重依赖目标类的实现。 在某些场景下,尤其是开发大型APP组件时,出于安全考虑,某些业务逻辑不希望依赖于源码或aar。 在这种情况下,显式依赖就没有办法解决了。 我们来看看隐式调用方法。

步骤1:在配置中至少配置一个-。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.snail.activityforresultexample">

    <application
       ...
    <activity android:name=".test.SecondActivity">

            <intent-filter>
            
                   <category android:name="android.intent.category.DEFAULT"/>
            
                <action android:name="com.snail.activityforresultexample.SecondActivity" />
                
  
            intent-filter>

        activity>
    application>
manifest>

第二步:打电话。

void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
}

如果涉及到数据传输写入,那就比较复杂了。 隐式调用的缺点如下:

首先,定义复杂,会导致暴露的协议变得复杂,难以维护和扩展。

其次,需要不同的配置,每次添加或删除都会很麻烦。 这对于开发者来说非常不友好,并且使得协作变得更加困难。

最后,不建议将所有属性设置为 True。 这是降低风险的一种方法。 一般都是归为一类,统一处理跳转。 在这种场景下,它既具有路由功能,又具有隐式调用场景。 这种情况下,新增和删除必然需要每次调整路由表,从而导致开发效率降低、风险增加。

可见,系统原生的路由框架并没有过多考虑团队协作的开发模式。 主要限于模块内多个业务之间的直接相互引用。 它基本上需要代码级的依赖,这对代码和业务隔离非常不友好。 如果不考虑之前Dex方法树的过度限制,你可以认为三方路由框架完全是为了团队协作而创建的。

4、APP三方路由框架所需的能力

目前市面上的大部分路由框架都可以解决上述问题。 简单总结一下目前的三方路由能力可以总结如下:

路由表生成能力:业务组件【UI业务和服务】自动扫描注册逻辑,需要良好的扩展性,且不需要侵入原有代码逻辑。

逻辑与业务映射:无需依赖具体实现实现代码隔离。

基本路由跳转能力:支持页面跳转能力。

对服务组件的支持:比如去某个服务组件获取一些配置等。

[扩展] 路由拦截逻辑:如登录、统一认证等。

可定制的降级逻辑:找不到组件时的解决方案。

可以看到接下来的典型用法,第一步:在新页面中添加语句。

@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
     ...
}

在构建阶段,将根据注释收集路由并生成路由表。 第二步使用:

ARouter.getInstance()
        .build("/test/activity2")
        .navigation(this);

如上,在该框架下,只需要字符串,无需依赖任何东西就可以实现路由跳转。

5、APP路由框架实现

路由框架实现的核心是能够与组件[或其他服务]建立映射关系,即路由表,并根据路由表路由到相应的组件。 其实分为两部分,第一部分是路由表的生成,第二部分是路由表的查询。

自动生成路由表

生成路由表的方法有很多种。 最简单的是维护一个公共文件或类,其中映射每个实现组件。

安卓开发不用new的框架_安卓应用开发框架_安卓开发框架mvvm

但这种方式的缺点也很明显:每次增删改查都必须修改表,这对协作非常不友好,不符合解决协作问题的初衷。 然而,最终的路由表遵循这一路径,即将所有内容收集到一个对象中。 唯一的区别是实现方法。 目前三方路由框架几乎都采用注解+APT【Tool】工具+AOP(--,面向切面编程),基本流程如下:

安卓开发不用new的框架_安卓开发框架mvvm_安卓应用开发框架

涉及的技术包括注解、APT(工具)、AOP(面向切面编程)。 APT比较常用,主要是遍历所有类,找到带注释的Java类,然后聚合生成路由表。 由于可能有很多组件,因此可能有多个路由表。 之后,这些生成的辅助类将包含在源代码中。 它被编译成class文件,然后使用AOP技术[比如ASM或者]扫描这些生成的类,聚合路由表,填充到前面的占位符方法中,完成自动注册的逻辑。

如何收集并生成路由表集合?

以框架为例,首先定义框架所需的注解如:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    /**
     * Path of route
     */

    String path();

该注解用于标记需要路由的组件。 用法如下:

@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
    @Autowired
    int age = 10;

然后使用APT扫描所有带注释的类并生成路由表。 实现参考如下:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (CollectionUtils.isNotEmpty(annotations)) {
    
        Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
        
            this.parseRoutes(routeElements);
       ...
    return false;
}

 
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
                        ...
                     // Generate groups
            String groupFileName = NAME_OF_GROUP + groupName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(groupFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(type_IRouteGroup))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfGroupBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

产品如下:包括路由表和本地注册条目。

安卓开发框架mvvm_安卓开发不用new的框架_安卓应用开发框架

自动注册:ASM收集上述路由表并将其聚合到Init代码区域中。

为了能够插入到Init代码区,首先需要预留一个位置,通常是定义一个空函数,以便稍后填充:

public class RouterInitializer {

    public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
        ...
        loadRouterTables();
    }
    //自动注册代码    
    public static void loadRouterTables() {

    }
}

首先,使用AOP工具遍历上述APT中间产品,聚合路由表,并将其注册到预留的初始化位置。 遍历过程涉及多个过程。

收集目标并聚合路由表

/**扫描jar*/
fun scanJar(jarFile: File, dest: File?) {

    val file = JarFile(jarFile)
    var enumeration = file.entries()
    while (enumeration.hasMoreElements()) {
        val jarEntry = enumeration.nextElement()
        if (jarEntry.name.endsWith("XXRouterTable.class")) {
            val inputStream = file.getInputStream(jarEntry)
            val classReader = ClassReader(inputStream)
            if (Arrays.toString(classReader.interfaces)
                    .contains("IHTRouterTBCollect")
            ) {
                tableList.add(
                    Pair(
                        classReader.className,
                        dest?.absolutePath
                    )
                )
            }
            inputStream.close()
        } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
            registerInitClass = dest
        }
    }
    file.close()
}

将路由表初始化代码注入到目标Class中

fun asmInsertMethod(originFile: File?) {

    val optJar = File(originFile?.parent, originFile?.name + ".opt")
    if (optJar.exists())
        optJar.delete()
    val jarFile = JarFile(originFile)
    val enumeration = jarFile.entries()
    val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

    while (enumeration.hasMoreElements()) {
        val jarEntry = enumeration.nextElement()
        val entryName = jarEntry.getName()
        val zipEntry = ZipEntry(entryName)
        val inputStream = jarFile.getInputStream(jarEntry)
        //插桩class
        if (entryName.endsWith("RouterInitializer.class")) {
            //class文件处理
            jarOutputStream.putNextEntry(zipEntry)
            val classReader = ClassReader(IOUtils.toByteArray(inputStream))
            val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
            val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
            classReader.accept(cv, EXPAND_FRAMES)
            val code = classWriter.toByteArray()
            jarOutputStream.write(code)
        } else {
            jarOutputStream.putNextEntry(zipEntry)
            jarOutputStream.write(IOUtils.toByteArray(inputStream))
        }
        jarOutputStream.closeEntry()
    }
    //结束
    jarOutputStream.close()
    jarFile.close()
    if (originFile?.exists() == true) {
        Files.delete(originFile.toPath())
    }
    optJar.renameTo(originFile)
}

最后将.class修改为如下填充代码:

public static void loadRouterTables() {

  
  register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
  register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
  register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
  register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
  ...
}

这样就完成了路由表的收集和注册,这就是大致的流程。 当然,支持服务等方面略有差异,但大体相似。

服务组件的框架支持

通过路由获取服务是APP路由独有的能力。 比如有一个用户中心组件,我们可以通过路由查询用户是否登录。 这并不是狭义上的页面路由的概念。 如何通过a找到字符串中对应的组件并调用其方法? 实现这一点的方法有很多种,每种实现都有自己的优点和缺点。

一种是将服务抽象成接口下沉到底层,上层通过路由实现映射对象。

一种是直接通过路由来映射实现方法。

让我们看看第一种实现方法。 它的优点是所有对外暴露的服务都暴露接口类【沉到底】,这对于外部的调用者,也就是服务的使用者来说非常友好。 示例如下:

先定义抽象服务,下沉到底层

public interface HelloService extends IProvider {
    void sayHello(String name);
}

实现服务并用注释标记它们。


@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
    Context mContext;

    @Override
    public void sayHello(String name) {
        Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }


使用方法:使用Add获取服务实例,映射到抽象类,然后直接调用方法。

((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

这种实现方式对于用户来说其实是非常方便的,尤其是当一个服务有多种可操作方法的时候,但是缺点是可扩展性。 如果要扩展方法,则需要更改底层库。

我们看第二种:直接通过路由映射实现方法

服务调用必须落在方法上。 参考页面路由,也可以支持方法路由。 两者是并行关系,所以我们主要添加一个方法路由表。 实现原理与Page 类似。 与上面相比,不需要定义抽象层。 直接定义实现即可:

定义


public class HelloService {

    
    @MethodRouter(url = {"arouter://sayhello"})
    public void sayHello(String name) {
        Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }


只需使用它

RouterCall.callMethod("arouter://sayhello?name=hello");

上述缺点是外部调用有些复杂,尤其是处理参数时,需要严格按照协议处理。 优点是没有抽象层。 如果需要扩展服务方法,不需要改变底层。

以上两种方法各有优缺点。 不过,如果从制作服务组件的初衷出发,第一种方法更好:对调用者更友好。 另外,对于支持来说,处理方式可能更方便,可以更方便地留给服务提供商来定义。 如果是第二种,直接通过路由映射来处理服务会比较麻烦,尤其是里面的参数可能需要统一封装成JSON并维护解析的协议。 这可能不太好。

路由表匹配

路由表的匹配比较简单,就是根据全局映射中的输入来匹配目标组件,然后依靠反射等常用操作来定位目标。

6.组件化与路由的关系

组件化是一种开发集成模式,更像是一种开发规范,更方便团队协作开发。 组件化的最终实现是独立的业务和功能组件。 这些组件可能由不同的团队出于不同的目的进行维护,甚至需要代码隔离。 如果涉及到组件之间的调用和通信,就不能避免使用路由,因为要实现隔离,只能使用公共字符串进行通信。 这就是路由的功能范围。

组件化需要路由支持的根本原因:组件之间代码实现的隔离。

总结

路由不是APP的必备功能,但大型跨团队的APP基本需要。

路由框架基础能力:路由自动注册、路由表收集、服务和UI界面路由拦截等核心功能。

组件化与路由的关系:组件化的代码隔离使得路由框架成为必要。

标签: 路由 组件 框架

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码