从0到1,用`ComboLite`构建一个“万物皆可插拔”的动态化App

引言:告别"巨石",拥抱真正的模块化

在前面的系列文章中,我们从理论、历史、架构和源码等多个维度,深入探讨了Android插件化的必要性,以及ComboLite作为一个现代化、"0 Hook"框架的设计哲学与技术实现。理论的深度最终需要通过实践来检验。今天,我们将从零开始,手把手地带领大家完成一次完整的插件化开发闭环,用ComboLite亲手构建一个"万物皆可插拔"的动态化应用。

我们将构建一个极其简单的应用场景:一个"空壳"宿主App,动态加载一个"问候"插件,并显示由该插件提供的UI界面。这个过程虽然简单,但它将完整地覆盖从环境搭建、宿主配置、插件开发、插件打包动态加载的全过程,为您后续构建更复杂的动态化系统打下坚实的基础。

第一步:万丈高楼平地起 ------ 项目初始化与依赖配置

首先,请使用Android Studio创建一个新的、包含"Empty Activity"模板的Android项目。在这里,我们将其命名为ComboLiteDemo

ComboLite的集成遵循现代Android项目的最佳实践,我们强烈推荐使用**Version Catalog (libs.versions.toml)**来管理依赖,这会让您的依赖版本管理更加清晰和可维护。

  1. gradle/libs.versions.toml 中定义依赖项

    打开位于项目根目录下 gradle 文件夹内的 libs.versions.toml 文件,添加ComboLite核心库和aar2apk打包插件的版本及定义:

    Ini, TOML

    ini 复制代码
    [versions]
    # ... 其他版本定义
    combolite = "1.0.0"  # 强烈建议在此处使用最新的稳定版
    aar2apk = "1.0.0"    # 同样,使用最新版本
    
    [libraries]
    # ... 其他库定义
    # ComboLite 核心运行时库
    combolite-core = { group = "io.github.lnzz123", name = "combolite-core", version.ref = "combolite" }
    
    [plugins]
    # ... 其他插件定义
    # ComboLite 专用的AAR转APK打包插件
    combolite-aar2apk = { id = "io.github.lnzz123.combolite-aar2apk", version.ref = "aar2apk" }
  2. 在项目根 build.gradle.kts 中应用打包插件

    aar2apk插件只需在项目根目录的build.gradle.kts中应用一次。它将负责后续所有被声明的插件模块的打包任务。

    Kotlin

    scss 复制代码
    // in your project's root /build.gradle.kts
    plugins {
        // ... 其他插件
        alias(libs.plugins.combolite.aar2apk)
    }
  3. 在宿主 :app 模块的 build.gradle.kts 中添加核心库

    宿主模块需要运行时依赖 combolite-core

    Kotlin

    scss 复制代码
    // in your :app/build.gradle.kts
    dependencies {
        // ... 其他依赖
        implementation(libs.combolite.core)
    }

至此,我们的项目已经具备了ComboLite框架的基础能力。

第二步:构筑坚实基座 ------ 配置"空壳"宿主

"空壳"宿主是所有插件的容器和管理器,我们需要为其进行最简化的初始化配置。

  1. 实现自动初始化的 Application

    ComboLite提供了极致简单的初始化方式。只需让你的Application类继承自BaseHostApplication,框架将自动为你完成所有繁琐的初始化工作,包括PluginManager的配置、资源管理器的创建以及PluginCrashHandler的注册。

    app模块中创建HostApp.kt

    Kotlin

    kotlin 复制代码
    // in :app/src/main/java/.../HostApp.kt
    import com.combo.core.base.BaseHostApplication
    
    // 只需继承即可,一键完成所有配置,无需任何额外代码
    class HostApp : BaseHostApplication()

    然后,在app/src/main/AndroidManifest.xml中注册这个Application类:

    XML

    ini 复制代码
    <application
        android:name=".HostApp"
        ...>
    </application>
  2. 配置代理 Activity

    为了让插件能够正确地访问合并后的资源,并为未来可能的插件Activity提供代理占坑,宿主的Activity需要继承自BaseHostActivity

    修改我们项目模板中自动创建的MainActivity.kt

    Kotlin

    kotlin 复制代码
    // in :app/src/main/java/.../MainActivity.kt
    import com.combo.core.base.BaseHostActivity
    
    // 让MainActivity继承BaseHostActivity
    class MainActivity : BaseHostActivity() {
        // ... onCreate 等方法
    }

    BaseHostActivity内部重写了getResources()getAssets()方法,这是实现插件资源对Jetpack Compose透明访问的关键。

第三步:创造第一个"零件" ------ 开发一个简单的"问候"插件

现在,让我们来创建第一个真正的插件。

  1. 新建插件模块

    在项目中,选择 File > New > New Module...,然后选择 Android Library,我们将模块命名为 hello-plugin

  2. 为插件添加依赖

    打开新建的:hello-plugin/build.gradle.kts文件。插件模块对核心库的依赖应该是编译时依赖 (compileOnly) ,因为在实际运行时,这些类将由宿主提供。

    Kotlin

    scss 复制代码
    // in :hello-plugin/build.gradle.kts
    dependencies {
        // ...
        // 插件只在编译时需要核心库,运行时由宿主提供
        compileOnly(libs.combolite.core)
    }
  3. 实现插件入口类 IPluginEntryClass

    这是插件的核心。它实现了IPluginEntryClass接口,是插件与框架交互的唯一桥梁。

    hello-plugin模块中创建HelloPluginEntry.kt

    Kotlin

    kotlin 复制代码
    // in :hello-plugin/src/main/java/.../HelloPluginEntry.kt
    package com.example.helloplugin
    
    import androidx.compose.foundation.layout.Box
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import com.combo.core.interfaces.IPluginEntryClass
    
    class HelloPluginEntry : IPluginEntryClass {
    
        /**
         *   (可选) 声明此插件提供的 Koin 依赖注入模块
         *   插件化框架会自动处理依赖注入的加载和卸载
         */
        override val pluginModule: List<Module>
            get() = emptyList()
            
        /**
         * onLoad 生命周期回调
         * 当插件被框架加载后,此方法会被调用。
         * 这是执行所有初始化逻辑的最佳位置。
         *
         * @param context 插件运行的上下文环境
         */
        override fun onLoad(context: PluginContext) {
        }
    
        /**
         * onUnload 生命周期回调
         * 当插件被框架卸载前,此方法会被调用。
         * 这是执行所有资源清理工作的最佳位置。
         */
        override fun onUnload() {
        }
    
        /**
         * 插件的UI入口,必须是一个 @Composable 函数。
         * 框架在启动插件后,会调用此方法来获取并展示插件的UI。
         */
        @Composable
        override fun Content() {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Hello from a dynamically loaded Plugin!")
            }
        }
    }
  4. 在 Manifest 中配置插件元数据

    框架如何知道这个插件的ID、版本和入口类呢?答案是通过插件AndroidManifest.xml中的<meta-data>标签。

    打开:hello-plugin/src/main/AndroidManifest.xml,添加以下元数据:

    XML

    ini 复制代码
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <meta-data android:name="plugin.id" android:value="com.example.hello-plugin" />
            <meta-data android:name="plugin.version" android:value="1.0.0" />
            <meta-data android:name="plugin.entryClass" android:value="com.example.helloplugin.HelloPluginEntry" />
            <meta-data android:name="plugin.description" android:value="A simple plugin that says hello." />
        </application>
    </manifest>

    恭喜!你的第一个插件已经开发完成了!

第四步:打包!aar2apk的一键魔法

现在,我们需要将这个library模块打包成一个可供宿主加载的APK文件。

  1. 在根项目 build.gradle.kts 中配置 aar2apk

    回到项目根目录的build.gradle.kts,添加aar2apk的配置块,并声明我们的hello-plugin模块。

    Kotlin

    scss 复制代码
    // in your project's root /build.gradle.kts
    plugins {
        // ...
        alias(libs.plugins.combolite.aar2apk) // 在这里应用插件
    }
    
    aar2apk {
        // 声明需要打包成APK的插件模块列表
        modules {
            module(":hello-plugin")
        }
    }
  2. 执行打包任务

    现在,打开Android Studio的Gradle面板,或者在终端中,你可以找到或执行新的打包任务。任务名遵循convert_<module_path>_<buildType>的格式。对于我们的hello-plugin,任务名就是:

    • convert_hello-plugin_debug
    • convert_hello-plugin_release

    执行convert_hello-plugin_debug任务。

    Bash

    bash 复制代码
    ./gradlew convert_hello-plugin_debug
  3. 找到你的插件APK

    构建成功后,你可以在app/build/outputs/plugin-apks/debug/目录下,找到我们新鲜出炉的插件包:hello-plugin-debug.apk

第五步:见证奇迹 ------ 动态加载并运行插件

最后一步,让我们在宿主中加载并运行这个插件。为了快速验证,我们将插件APK预置在宿主的assets目录中。

  1. 预置插件APK

    1. 在宿主:app模块的src/main目录下,创建一个assets/plugins文件夹。
    2. 将上一步生成的hello-plugin-debug.apk复制到这个目录中。
  2. 编写宿主加载逻辑

    现在,我们来填充MainActivity.kt的逻辑。我们将使用一个简单的ViewModel来处理从assets安装和加载插件的异步逻辑。

    LoadingViewModel.kt

    Kotlin

    kotlin 复制代码
    // in :app/src/main/java/.../LoadingViewModel.kt
    import android.app.Application
    import androidx.lifecycle.AndroidViewModel
    import androidx.lifecycle.viewModelScope
    import com.combo.core.interfaces.IPluginEntryClass
    import com.combo.core.manager.PluginManager
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.asStateFlow
    import kotlinx.coroutines.launch
    import java.io.File
    import java.io.FileOutputStream
    
    class LoadingViewModel(application: Application) : AndroidViewModel(application) {
        private val _pluginEntry = MutableStateFlow<IPluginEntryClass?>(null)
        val pluginEntry = _pluginEntry.asStateFlow()
    
        companion object {
            const val PLUGIN_ID = "com.example.hello-plugin"
            const val PLUGIN_APK_NAME = "hello-plugin-debug.apk"
        }
    
        fun loadPluginFromAssets() {
            viewModelScope.launch {
                val context = getApplication<Application>()
                val pluginFile = File(context.filesDir, PLUGIN_APK_NAME)
    
                // 1. 从assets复制APK到应用私有目录
                context.assets.open("plugins/$PLUGIN_APK_NAME").use { inputStream ->
                    FileOutputStream(pluginFile).use { outputStream ->
                        inputStream.copyTo(outputStream)
                    }
                }
    
                // 2. 安装插件
                val installResult = PluginManager.installerManager.installPlugin(pluginFile)
                if (installResult) {
                    // 3. 启动插件
                    val launchResult = PluginManager.launchPlugin(PLUGIN_ID)
                    if (launchResult) {
                        // 4. 获取插件实例
                        _pluginEntry.value = PluginManager.getPluginInstance(PLUGIN_ID)
                    }
                }
            }
        }
    }

    MainActivity.kt

    Kotlin

    kotlin 复制代码
    // in :app/src/main/java/.../MainActivity.kt
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.viewModels
    import androidx.compose.foundation.layout.*
    import androidx.compose.material3.Button
    import androidx.compose.material3.CircularProgressIndicator
    import androidx.compose.material3.Text
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import com.combo.core.base.BaseHostActivity
    
    class MainActivity : BaseHostActivity() {
        private val viewModel: LoadingViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                val pluginEntry by viewModel.pluginEntry.collectAsState()
    
                if (pluginEntry != null) {
                    // 插件已加载,显示插件UI
                    pluginEntry?.Content()
                } else {
                    // 插件未加载,显示加载按钮
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        Button(onClick = { viewModel.loadPluginFromAssets() }) {
                            Text("Load Hello Plugin")
                        }
                    }
                }
            }
        }
    }
  3. 运行应用

    现在,运行你的app模块。应用启动后,会显示一个"Load Hello Plugin"的按钮。点击它,ViewModel中的逻辑会执行,插件被安装、加载,然后------ "Hello from a dynamically loaded Plugin!" 将会神奇地出现在你的屏幕中央!

结语:这仅仅是开始

您已经成功地跨出了最重要的一步,亲手完成了ComboLite插件化开发的完整闭环。这不仅仅是一个简单的"Hello World",它证明了您已经掌握了将应用功能拆分为独立、动态模块的核心能力,这是开启现代化、大型App构建之路的钥匙。

今天我们只触及了冰山一角。ComboLite还提供了对四大组件的完整支持、强大的热更新与链式重启能力、以及灵活的去中心化插件管理。我们鼓励您深入探索官方文档和示例项目,将这些强大的能力应用到您的真实项目中。

我们为你感到骄傲,欢迎来到"万物皆可插拔"的新世界!

  • 项目源码 : github.com/lnzz123/Com...

    • 如果ComboLite的设计理念与工程实践获得了你的认可,请不吝给我们一个 Star!你的支持是我们持续迭代的最大动力。
  • 示例App下载 : 点击这里直接下载APK

    • 安装示例App,亲手体验一个"万物皆可插拔"的应用是怎样的。
  • 交流与贡献:

    • 有任何问题、建议或发现了Bug?我们期待在 GitHub Issues 中与您展开深入的技术探讨!

📚 ComboLite 深度探索系列文章

相关推荐
jctech7 小时前
解构ComboLite:0 Hook的背后,是哪些精妙的架构设计?
android
jctech7 小时前
你的App越来越“胖”了吗?给Android应用“减肥”的终极秘诀——插件化
android
jctech7 小时前
告别Hook!ComboLite:为Jetpack Compose而生的下一代Android插件化框架
android
LiuYaoheng8 小时前
【Android】Notification 的基本使用
android·java·笔记·学习
上等猿9 小时前
JUC多线程个人笔记
android·java·笔记
fatiaozhang952711 小时前
创维LB2004_安装软件教程
android·网络·电视盒子·刷机固件·机顶盒刷机
来来走走17 小时前
Flutter MVVM+provider的基本示例
android·flutter
CYRUS_STUDIO20 小时前
一步步带你移植 FART 到 Android 10,实现自动化脱壳
android·java·逆向
CYRUS_STUDIO20 小时前
FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器
android·java·逆向