Speed Tools:一套低侵入的 Android 插件化 + 动态换肤 + 字体切换框架

Speed Tools:一套低侵入的 Android 插件化 + 动态换肤 + 字体切换框架

作者:一航

GitHub:jasonliyihang/speed_tools

博客首发于 CSDN,本文基于 2026 年最新代码重构整理。


一、前言

几年前我在 CSDN 写过一篇 《android 插件化框架 speed-tools》,那时候框架只解决了"不安装 APK 就能加载页面"这一个核心诉求。几年过去,Android 系统经历了多次大版本迭代,插件化方案也面临新的挑战:

  • Android 10 收紧了私有目录访问;
  • Android 14 彻底禁止了加载可写 dex 文件
  • 业务侧对"动态换肤"和"全局字体调节"的需求越来越强烈。

于是我把整个项目从里到外升级了一遍:

  • 构建基线从 Support Library + compileSdk 28 升级到 AndroidX + compileSdk 35 + AGP 8.8.2
  • 类加载策略在 Android 8.0+ 切换为 InMemoryDexClassLoader(内存加载),天然规避 Android 14+ 的文件权限限制;
  • 新增 运行时换肤运行时字体切换两大能力。

本文会带你从 0 到 1 跑通整个 Demo,并理解背后的核心原理。


二、Speed Tools 能做什么?

一句话概括:一套面向 Android 的本地插件化框架,同时附带换肤和字体调节能力

能力 一句话说明 典型场景
插件化 宿主加载未安装的 APK,代理启动插件页面 多业务独立演进、按插件解耦
动态换肤 运行时加载皮肤包 APK,替换颜色/图片/背景 夜间模式、节日主题、品牌定制
字体调节 运行时全局调整字体大小,支持用户偏好持久化 无障碍适配、老年模式

2.1 为什么不用 Google Play Dynamic Delivery?

  • 国内应用商店生态复杂,很多渠道不支持 PAD;
  • 需要完全本地可控,不依赖外部服务;
  • 希望低侵入接入现有工程,而非改造成 Dynamic Feature Module 结构。

Speed Tools 的定位就是:本地可控、低侵入、开箱即用


三、工程结构一览

复制代码
speed_tools/
├── lib_speed_tools/          # 核心库(插件加载、代理、换肤、字体)
├── module_host_main/         # 宿主示例 App
├── module_client_one/        # 插件示例 1
├── module_client_two/        # 插件示例 2
├── theme_demo/               # 换肤与字体切换演示 App
├── black_theme/              # 皮肤包示例(纯资源,无业务代码)
└── lib_img_utils/            # 第三方图片库测试模块
  • lib_speed_tools 是唯一的依赖入口,宿主和插件都只依赖它;
  • module_host_main 演示了如何加载插件并跳转;
  • theme_demo 演示了换肤和字体切换的完整链路。

四、10 分钟跑通 Demo

4.1 环境要求

  • Android Studio(推荐最新稳定版)
  • JDK 17
  • compileSdk 35 / minSdk 21 / targetSdk 35

4.2 编译插件和皮肤包

bash 复制代码
# 编译插件 APK
./gradlew :module_client_one:assembleDebug
./gradlew :module_client_two:assembleDebug

# 编译皮肤包
./gradlew :black_theme:assembleDebug

4.3 放置 APK 到 assets

把编译产物复制到对应目录:

复制代码
module_host_main/src/main/assets/
    ├── module_client_one-debug.apk
    └── module_client_two-debug.apk

theme_demo/src/main/assets/
    └── black_theme-debug.apk

4.4 运行

  1. 选择运行配置 module_host_main → 启动后自动加载插件,点击按钮进入插件页面;
  2. 选择运行配置 theme_demo → 依次体验:切换黑色主题 → 恢复默认 → 放大字体 → 恢复字体。

到这里,你已经亲眼见证了框架的三项核心能力。


五、插件化核心原理

5.1 整体架构

复制代码
┌─────────────────────────────────────────┐
│              宿主 APK                    │
│  ┌─────────┐   ┌─────────────────────┐  │
│  │ Assets  │──▶│ SpeedApkManager     │  │
│  │ (插件)  │   │ (类加载 + 资源桥接)  │  │
│  └─────────┘   └─────────────────────┘  │
│                     │                    │
│                     ▼                    │
│  ┌───────────────────────────────────┐  │
│  │       代理 Activity                │  │
│  │  (转发 onCreate/onResume/onDestroy)│  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│            插件 APK(未安装)              │
│  ┌──────────────┐   ┌──────────────┐   │
│  │ 业务实现类    │   │    res/      │   │
│  │(继承接口)    │◀──│  (资源文件)  │   │
│  └──────────────┘   └──────────────┘   │
└─────────────────────────────────────────┘

5.2 类加载:InMemoryDexClassLoader 如何规避 Android 14 限制

Android 14 引入了一条硬性限制:禁止加载可写的 dex 文件 。旧方案使用 DexClassLoader 直接加载 APK 路径,如果文件权限是可写的,就会抛出:

复制代码
java.lang.SecurityException: Writable dex file ... is not allowed.

Speed Tools 的解决思路很直接:

  • Android 8.0+(API 26) :从 APK 中解压出 classes.dex,读取到内存 ByteBuffer,通过 InMemoryDexClassLoader 加载。dex 数据完全在内存中,不走文件系统,自然不受"可写"限制。
  • Android 5.0~7.1(API 21~25) :回退到传统的 DexClassLoader,这些旧版本没有此限制。

核心代码片段(SpeedUtils.java):

java 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    ByteBuffer[] dexBuffers = extractDexBuffersFromApk(apkPath);
    return new InMemoryDexClassLoader(dexBuffers, appContext.getClassLoader());
} else {
    return new DexClassLoader(apkPath, optimizedDir, nativeLibDir, parent);
}

5.3 资源桥接

插件的 res/ 资源如何被宿主识别?答案是反射创建 AssetManager

java 复制代码
AssetManager assetManager = AssetManager.class.getDeclaredConstructor().newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, apkPath);

Resources pluginRes = new Resources(assetManager, hostMetrics, hostConfig);

宿主代理 Activity 持有这份 pluginRes,插件页面里的 setContentView(R.layout.xxx) 就能正确找到插件自己的布局了。

5.4 生命周期转发

插件业务类不直接继承 Activity,而是继承 SpeedBaseInterfaceImp,实现一套与 Activity 对应的生命周期接口。宿主端的 SpeedHostBaseActivity 作为"壳",在 onCreateonResumeonDestroy 等节点调用插件实现类的对应方法,完成生命周期转发。


六、插件化接入实战

6.1 宿主侧:加载插件

java 复制代码
// 优先从外部目录查找,fallback 到 assets 拷贝
File apkFile = SpeedUtils.resolvePluginApk(
        context, "/sdcard/Download", 
        "module_client_one-debug.apk"
);

SpeedApkManager.getInstance().loadApk(
        "first_apk",           // 插件 key
        apkFile.getAbsolutePath(), 
        "dex_output2",         // dex 优化目录(每个插件独立)
        context
);

6.2 宿主侧:跳转插件

java 复制代码
SpeedUtils.goActivity(this, "first_apk", null);

第三个参数 classTag 对应插件 AndroidManifest.xmlmeta-dataname,为空时走默认入口。

6.3 插件侧:声明入口

xml 复制代码
<application>
    <meta-data
        android:name="root_class"
        android:value="com.example.clientdome.ClientMainActivity" />
</application>

插件业务类需要实现 SpeedBaseInterface,代理层会通过反射实例化这个类。


七、换肤与字体切换

7.1 核心设计

换肤和字体调节的本质是资源替换

  • 换肤 :拦截 LayoutInflater 创建 View 的过程,把颜色/图片资源替换为皮肤包中的同名资源;
  • 字体 :拦截 textSize 属性的读取,在基础值上叠加用户设置的偏移量。

为了框架能识别哪些资源需要被替换,规定了强制前缀:

类型 前缀 示例
颜色/背景/图片 cxt_ @color/cxt_primary
字体维度 cxf_ @dimen/cxf_normal

7.2 三步接入

Step 1:Application 初始化

java 复制代码
@Override
public void onCreate() {
    super.onCreate();
    SPFontManager.getInstance().init(this);
    SPThemeManager.getInstance().init(this);
}

Step 2:Activity 注册监听

java 复制代码
public class BaseActivity extends AppCompatActivity implements SPUpdateUIListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SPThemeManager.getInstance().registerUpdateUI(this);
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onDestroy() {
        SPThemeManager.getInstance().unRegisterUpdateUI(this);
        super.onDestroy();
    }

    @Override
    public void updateUI(boolean isFistLoading) {
        // 自定义控件手动刷新
    }
}

Step 3:触发切换

java 复制代码
// 换肤
SPThemeManager.getInstance()
        .changeTheme("black_theme-debug.apk")
        .sendUpdateUIAction();

// 字体放大
SPFontManager.getInstance().changeConfig(40).updateUI();

7.3 皮肤包怎么制作?

皮肤包就是一个只包含资源、不含业务代码的普通 Android App 模块:

  1. 新建 Android App 模块;
  2. res/ 中放置与主工程同名cxt_* / cxf_* 资源;
  3. 打包生成 APK。

主工程和皮肤包的资源名必须完全一致,值可以不同。运行时 SPThemeManager 会读取皮肤包 APK 的资源,通过 AssetManager.addAssetPath 建立资源上下文,完成替换。


八、踩坑记录

8.1 Android 14 上插件加载失败

现象SecurityException: Writable dex file is not allowed

根因 :Android 14 禁止加载可写 dex 文件

解决 :框架已在 Android 8.0+ 自动切换 InMemoryDexClassLoader,无需业务侧改动。如果仍在维护旧版本框架,可参考本文 5.2 节的实现思路自行迁移。

8.2 换肤后页面没有变化

按这个清单逐条排查:

  1. Application 是否初始化了 SPThemeManagerSPFontManager
  2. Activity 是否 registerUpdateUI / unRegisterUpdateUI
  3. 资源名是否用了 cxt_ / cxf_ 前缀;
  4. 是否调用了 sendUpdateUIAction() 触发刷新。

8.3 插件页面白屏

  • 检查插件 AndroidManifest.xmlmeta-data 入口声明是否正确;
  • 检查插件资源是否齐全;
  • 检查宿主和插件依赖的 lib_speed_tools 版本是否一致。

九、从旧版本迁移

如果你还在使用早期的 com.liyihangjson:speed_tools:1.1.1 Maven 依赖,建议迁移到源码依赖

gradle 复制代码
// settings.gradle
include ':lib_speed_tools'

// app/build.gradle
dependencies {
    implementation project(':lib_speed_tools')
}

迁移收益:

  • 直接获得 Android 14 兼容性修复;
  • 获得换肤和字体切换能力;
  • 构建基线同步升级到 AndroidX + AGP 8.x。

十、总结

Speed Tools 从最早的"纯插件化"框架,逐步演变成了插件化 + 换肤 + 字体调节的三位一体工具集。本次升级的核心是适配新时代 Android 系统的安全限制(Android 14 的 dex 文件权限),同时响应业务侧对动态 UI 的诉求。

项目完全开源,欢迎 Star 和 PR。


如有问题,欢迎在 GitHub Issues 交流。

相关推荐
千里马学框架15 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台15 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone15 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc16 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo17 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770018 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼18 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone19 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen19 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone19 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui