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

2026-06-14阅读 0热度 0
android

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

先聊几个关键点:插件化方案在 Android 生态里始终是一把“双刃剑”——它让业务解耦、独立迭代成为可能,却也始终要与系统安全机制周旋。几年前写过一篇关于 Speed Tools 的文章,彼时框架的核心目标很纯粹:让插件 APK 无需安装就能运行。

如今 Android 系统已演进至 14,格局大变。私有目录访问被严格限制,可写 dex 文件彻底被封禁,业务端对动态换肤和全局字体调节的需求也日益迫切。索性将整个项目从底层到上层全面重写。

  • 构建基线从 Support Library + compileSdk 28 升级至 AndroidX + compileSdk 35 + AGP 8.8.2;
  • Android 8.0 及以上全面切换至 InMemoryDexClassLoader,从内存加载 dex,彻底绕开 Android 14 的文件权限限制;
  • 新增运行时换肤与运行时字体切换两套完整能力。

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

本文带你从零运行 Demo,同时剖析背后的核心原理。

一、Speed Tools 能做什么?

一句话概括:一套面向 Android 的本地插件化框架,同时集成了换肤与字体调节能力。

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

为什么不用 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

3.1 环境要求

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

3.2 编译插件和皮肤包

# 编译插件 APK./gradlew :module_client_one:assembleDebug./gradlew :module_client_two:assembleDebug# 编译皮肤包./gradlew :black_theme:assembleDebug

3.3 放置 APK 到 assets

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

module_host_main/src/main/assets/├── module_client_one-debug.apk└── module_client_two-debug.apktheme_demo/src/main/assets/└── black_theme-debug.apk

3.4 运行

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

至此,你已亲眼见证框架的三项核心能力在运作。

四、插件化核心原理

4.1 整体架构

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

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

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

ja va.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.ja va):

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);}

4.3 资源桥接

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

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) 就能正确解析到插件自身的布局。

4.4 生命周期转发

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

五、插件化接入实战

5.1 宿主侧:加载插件

// 优先从外部目录查找,fallback 到 assets 拷贝File apkFile = SpeedUtils.resolvePluginApk(context, "/sdcard/Download", "module_client_one-debug.apk");SpeedApkManager.getInstance().loadApk("first_apk", // 插件 keyapkFile.getAbsolutePath(), "dex_output2", // dex 优化目录(每个插件独立)context);

5.2 宿主侧:跳转插件

SpeedUtils.goActivity(this, "first_apk", null);

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

5.3 插件侧:声明入口

插件业务类需要实现 SpeedBaseInterface,袋里层会通过反射实例化该类。

六、换肤与字体切换

6.1 核心设计

换肤与字体调节的本质,归根结底是资源替换:

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

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

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

6.2 三步接入

Step 1:Application 初始化

@Overridepublic void onCreate() { super.onCreate();SPFontManager.getInstance().init(this);SPThemeManager.getInstance().init(this);}

Step 2:Activity 注册监听

public class BaseActivity extends AppCompatActivity implements SPUpdateUIListener { @Overrideprotected void onCreate(Bundle sa vedInstanceState) { SPThemeManager.getInstance().registerUpdateUI(this);super.onCreate(sa vedInstanceState);}@Overrideprotected void onDestroy() { SPThemeManager.getInstance().unRegisterUpdateUI(this);super.onDestroy();}@Overridepublic void updateUI(boolean isFistLoading) { // 自定义控件手动刷新}}

Step 3:触发切换

// 换肤SPThemeManager.getInstance().changeTheme("black_theme-debug.apk").sendUpdateUIAction();// 字体放大SPFontManager.getInstance().changeConfig(40).updateUI();

6.3 皮肤包怎么制作?

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

  • 新建一个 Android App 模块;
  • res/ 中放置与主工程同名的 cxt_* / cxf_* 资源;
  • 打包生成 APK。

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

七、踩坑记录

7.1 Android 14 上插件加载失败

现象SecurityException: Writable dex file is not allowed
根因:Android 14 禁止加载可写 dex 文件
解决:框架已在 Android 8.0 以上自动切换 InMemoryDexClassLoader,业务侧无需改动。如果还在维护旧版本框架,可参考本文 5.2 节的实现思路自行迁移。

7.2 换肤后页面没有变化

按以下清单逐条排查:

  • Application 是否初始化了 SPThemeManagerSPFontManager
  • Activity 是否执行了 registerUpdateUI / unRegisterUpdateUI
  • 资源名是否使用了 cxt_ / cxf_ 前缀;
  • 是否调用了 sendUpdateUIAction() 触发刷新。

7.3 插件页面白屏

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

八、从旧版本迁移

如果仍在用早期的 com.liyihangjson:speed_tools:1.1.1 Ma ven 依赖,建议迁移到源码依赖:

// settings.gradleinclude ':lib_speed_tools'// app/build.gradledependencies {implementation project(':lib_speed_tools')}

迁移的收益明确:直接获得 Android 14 兼容性修复,顺手拿到换肤和字体切换能力,构建基线也同步升级至 AndroidX + AGP 8.x。

九、总结

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

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

GitHub 仓库:https://github.com/jasonliyihang/speed_tools

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策