Android 动态页面框架(一)

2,876 阅读9分钟

版权声明

凡未经作者授权,任何媒体、网站及个人不得转载、复制、重制、改动、展示或使用局部或全部的内容或服务。如果已转载,请自行删除。同时,我们保留进一步追究相关行为主体的法律责任的权利。

© 2024 小酥肉不加辣,All rights reserved.

MAD App Architecture launch - Mobile.png

背景

想象一下,在我们的系统中存在这样一个应用。这个应用不仅仅是一个简单的应用,它实际上是一个功能丰富的集合体。这个应用涉及的范围非常广泛,涵盖了许多不同的领域和团队。不仅如此,这个应用还包含了大量的模块,涵盖了各种各样的功能和服务。由于模块众多和涉及的范围广泛,这个应用的开发和维护工作往往需要多个团队进行跨团队的合作,这无疑增加了工作的复杂性和挑战性。

再想象一下,这个应用程序的每个功能并不复杂,例如打开一个开关,点击一个按钮,展示一条信息。在表面上,这些功能可能看起来很简单,但是实际上,每一个功能背后都有大量的工作需要完成。这些功能中的每一个都需要集成其他模块或服务的API才能实现。这可能包括读取配置文件,调用系统服务,或者与其他程序的交互。尽管这些功能看起来简单,但实现它们的过程却需要对各种领域知识有深入的理解。

在面对上述问题时,我们可能会有一种疑问:是否存在一种架构能够解决这些问题?是否有一种设计方法可以帮助我们克服这些挑战,使我们能够更有效地完成任务,而不会受到这些问题的影响?

目标

分布式

  • 页面的布局和配置分布在各个不同的功能模块或服务中,而不是集中存储在一个单独的模块或应用中。这种方式能够提高架构的灵活性和效率,因为每个功能模块或服务都可以根据其特定的需求和目标进行独立的管理和调整。这样,整个系统的运行和更新可以更加流畅和高效,同时也能够更好的满足各个部分的具体需求。
  • 在我们的系统设计中,功能实现的方式是分布在各个独立的功能模块中,而不是集中在一个单一的应用或服务中。这样的设计思路有其特定的优势。首先,由于每个模块都是针对其特定的业务需求进行设计和实现的,因此,模块本身对其所涵盖的业务逻辑有深入的理解和把握。这使得我们能够在细节层面做出更加精细和准确的设计和实现。其次,这种分布式的设计方式也使得后续的修改和维护工作变得更加容易。因为每个模块都是独立的,所以在需要进行修改或维护时,我们只需要针对具体的模块进行操作,而无需影响到整个系统。这大大提高了我们的工作效率,并且也减少了出错的可能性。
  • 功能测试的执行方式是分布式的,这种方式与传统的集中式不同。在分布式的测试方式中,不是由一个团队负责所有的功能测试,而是由各个独立的团队分别负责各自的功能测试。这样,每个团队可以更专注于他们负责的功能区域,从而提高测试的效率和质量。

动态化

尽管页面配置和功能实现是分布式的,设计的架构不仅仅局限于分布式的基本概念,而是应该进一步实现动态化和自动化。在这样的架构中,一旦系统的任何一处配置发生改变,中心应用应该能够自动检测到这些变化,并且能够快速响应。这意味着中心APP需要具备智能的感知和处理能力,以便在配置更新后,能够自动重新生成或更新相应的页面。这种自动化的过程可以极大地减少手动干预的需求,提高开发和维护的效率。

single-app-vs-multiple-apps.drawio.png

设计

在这个系统设计中,我们有两个重要的概念:Host 和 Client 。Host 是中心应用,它的主要职责是发现所有注册的动态页面配置文件,并解析和管理这些配置。然后根据这些配置生成页面。而 Client 是其他需要将功能集成到 Host 应用的应用程序。它们在自己的程序内部编写配置文件,以实现配置文件所定义的功能。

Host 作为中心应用,扮演着整个系统的核心角色。它负责管理和维护所有注册的动态页面配置文件。当 Host 启动时,它会自动发现所有已注册的配置文件,并进行解析。解析过程包括读取配置文件中定义的页面结构、数据源、样式等信息,并将其存储在内存中。然后,Host 根据这些配置生成相应的页面。

Client 应用程序是需要将功能集成到 Host 应用的其他应用。它们在自己的程序内部编写配置文件,定义需要实现的功能。这些配置文件包括页面结构、数据源、样式等信息。这些配置文件会随 Client应用程序安装到系统中,以便 Host 应用能够发现并解析这些配置。

通过这种设计,Host 和 Client 之间实现了解耦和灵活性。Host 应用作为中心应用,负责管理和生成页面,而 Client 应用则可以根据自己的需求编写配置文件,实现定制化的功能。这种模块化的设计使得系统更加可扩展和可维护,同时也提供了更好的灵活性和定制化能力。

配置文件

配置文件需要具备以下特点:

  1. 可以和应用一起打包,并可以通过API读取到配置文件内容
  2. 清晰的定义

格式

针对不同的UI组件可以定义不同的属性和元素。不同UI组件可能会有区别于其他的自定义属性,但是它们也有一些通用的属性。

key:标记一个UI控件的唯一ID

label: UI 控件显示的名称

  • 文本
<Text key="test_text" label="Test Text" />
  • 按钮
<Button key="test_button" label="Test Button" click="action_test_button" />
  • 开关
<Toggle key="test_toggle" label="Test Toggle" click="action_test_toggle" />
  • 下拉菜单
<Dropdown key="test_dropdown" label="Test Dropdown"  click="action_test_dropdown">
	<item>option 1</item>
	<item>option 2</item>
	<item>option 3</item>
</Dropdown>

除了定义独立的UI组件,还需要定义组织一系列UI控件的组件。

  • 分组

将多个组件分为一组,在业务上可以将一个模块的功能组织在一起。

<Group lable="Test Group">
    <Text key="test_text" label="Test Text" />
    <Button key="test_button" label="Test Button" click="action_test_button"/>
    <Toggle key="test_toggle" label="Test Toggle" click="action_test_toggle">
    <Dropdown key="test_dropdown" label="Test Dropdown"  click="action_test_dropdown">
        <item>option 1</item>
        <item>option 2</item>
        <item>option 3</item>
    </Dropdown>
</Group>
  • 页面

不可能将所有UI控件都布局到一个页面上,可以添加页面组件,将多个控件组织到一个页面中,页面还可以包含子页面,这样可以实现多个页面之间的跳转。

<Page label="Page 1" category="General">
    <Text key="test_text" label="Test Text" />
    <Button key="test_button" label="Test Button" click="action_test_button"/>
    <Group>
            ...
    </Group>
    <Page lable="Child Page">
            ...
    </Page>
</Page>

component_level.drawio.png

ℹ️ 可以定义更多的UI组件,此处不再一一列举。

校验

如何保证XML文件的格式按照约定的规则编写?

根据上述 UI 控件的定义,编写 XML DTD (Document Type Definition) 文件

<!ELEMENT Chameleon (Page+)>

<!ELEMENT Page (#PCDATA | Page | Text | Button | Toggle | Dropdown | Group)*>
<!ATTLIST Page
    title CDATA #REQUIRED
    category CDATA #IMPLIED
    key CDATA #IMPLIED
    key CDATA #IMPLIED>

<!ELEMENT Text (#PCDATA)>
<!ATTLIST Text
    key CDATA #REQUIRED
    label CDATA #REQUIRED>

<!ELEMENT Button (#PCDATA)>
<!ATTLIST Button
    key CDATA #REQUIRED
    label CDATA #REQUIRED
    click CDATA #REQUIRED>

<!ELEMENT Toggle (#PCDATA)>
<!ATTLIST Toggle
    key CDATA #REQUIRED
    label CDATA #REQUIRED
    click CDATA #REQUIRED>

<!ELEMENT Dropdown (#PCDATA | item)>
<!ATTLIST Dropdown
    key CDATA #REQUIRED
    label CDATA #REQUIRED
    click CDATA #IMPLIED>

<!ELEMENT item (#PCDATA)>

<!ELEMENT Group (#PCDATA | Page | Text | Button | Toggle | Dropdown)*>
<!ATTLIST Group
    title CDATA #REQUIRED>

在 XML 文件中引入 DTD 文件

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Chameleon SYSTEM "chameleon.dtd">
<Chameleon>
    <Page
        title="Test Page"
        category="Test"
        key="test_page"
        label="Test Page">
        <Text
            key="test_text"
            label="Test Text" />
        <Button
            click="click_button"
            key="test_button"
            label="Test Button" />
        <Toggle
            click="click_toggle"
            key="test_toggle"
            label="Test Toggle" />
        <Dropdown
            click="click_dropdown"
            key="test_dropdown"
            label="Test Dropdown">
            <item>option 1</item>
            <item>option 2</item>
            <item>option 3</item>
        </Dropdown>
        <Group label="Group title">
            <Text
                key="test_text"
                label="Test Text" />
        </Group>
        <Page
            title="Test Child Page"
            category="Test"
            key="key_test_child_page"
            label="Test Child Page">
            <Text
                key="test_text"
                label="Test Text" />
        </Page>
    </Page>
</Chameleon>

注册与发现

注册

将xml文件放到 res/xml 文件夹下,可以一起打包apk文件中。

src
|__main
    |__res
        |__xml
            |__chameleon_page.xml

在 AndroidManifest.xml 文件中使用 meta-data 注册

<receiver
    android:name=".ChameleonReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.wx.action.CHAMELEON" />
    </intent-filter>
    <meta-data
        android:name="chameleon_page"
        android:resource="@xml/chameleon_page" />
</receiver>

发现

使用标准的API可以查询到系统中其他应用的组件信息

private fun queryReceivers(): List<ResolveInfo> {
    val intent = Intent("com.wx.action.CHAMELEON")
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        context.packageManager.queryBroadcastReceivers(
            intent,
            ResolveInfoFlags.of(GET_RECEIVERS.toLong()),
        )
    } else {
        context.packageManager.queryBroadcastReceivers(intent, GET_RECEIVERS)
    }
}

应用之间通信

AIDL

AIDL方式相对稳定,但要求Client必须在Host启动之前启动自己,或者在Host启动时发送广播给所有的Client,Client收到广播后再连接Host。这种方式的不足之处在于,一旦Host启动后,所有的Client进程都需要启动,这可能会占用大量的系统资源。

广播

广播方式虽然没有AIDL方式稳定,但它具有灵活性。广播只有在需要时才会启动特定的Client进程,这样可以避免一次性启动所有的Client进程,从而减少了对系统资源的占用。

然而,需要注意的是,广播方式也存在一些问题。首先,由于广播是一种全局通知机制,可能会导致安全性问题。因为任何应用都可以接收到广播,并且可能会对广播进行恶意利用。其次,广播的传递是基于事件的,可能会导致一些延迟和不确定性。特别是在高并发的情况下,广播的处理可能会变得缓慢,从而影响系统的响应性能。

综上所述,AIDL方式虽然稳定,但要求Client在Host启动之前启动自己,或者通过广播连接Host,可能会占用系统资源。而广播方式虽然灵活,但存在安全性和响应性能的问题。因此在方案设计时可以根据项目实际情况选择其中一种。

参考

developer.android.com/reference/a…

www.w3schools.com/xml/xml_dtd…