Magisk 模块开发完全指南:从 Systemless 原理到实战发布

本文基于 Magisk 官方开发者文档整理并结合作者的实际模块维护经验写成,所有关键机制均以官方文档为准,文末附参考链接。文中代码可直接复制使用。

写在前面

每一个折腾过 Android 的人,迟早会与 Magisk 相遇。刷机论坛里那些琳琅满目的模块——全局去广告、替换系统字体、注入调试工具、修改动效与音量步进——它们是怎么被写出来的?为什么装上就能生效、删掉就能还原,甚至不用重刷一次系统?

这篇文章把 Magisk 模块开发从原理到实战完整地讲一遍:分区与 systemless 机制、模块的目录结构、module.prop 的每一条字段、安装器与 customize.sh、五个脚本的执行时机、文件替换与删除的三种语义、在线更新、WebUI 与 Zygisk 的边界,最后用两个完整实战收尾——一个从零开始写的教学模块,和一个我本人长期维护的小米 HyperOS 字体模块的真实经验。读完这篇,你应该可以独立完成一个模块的开发、调试与发布。

阅读前提:会用 adb / fastboot,懂基本 Linux shell,手上有一台已解锁 Bootloader 并刷入 Magisk 的设备。没有也没关系,前半部分原理照读无碍,只是跟着做实战时需要一台"实验机"——不要拿主力机做第一个模块的试验场

一、为什么是 Magisk:Systemless 的核心思想

1.1 传统改机的困境

在 Magisk 出现之前,想改系统就得真的去改系统:把 /system 分区重新挂载为可写、替换文件后重刷分区镜像。这条路有三个致命问题:

  • OTA 升级基本报废。系统分区被改动后,官方增量包的完整性校验必然失败,往往只能线刷整包,且通常伴随数据清除。
  • 只读分区寸步难行。现代 Android 普遍启用 AVB/dm-verity 完整性校验(启动时逐块校验分区内容与哈希树是否一致,被改动的分区直接验证失败),/system/vendor/product/system_ext 以只读方式挂载;即便拿到 root 强行 remount 写入,也会破坏校验链,轻则改动被回滚,重则设备拒绝启动。
  • 不可回滚。改坏了某个文件,轻则个别功能异常,重则卡开机,恢复成本极高。

此外,如今越来越多的完整性检测机制(如 Google Play Integrity)会读取系统分区状态,物理改动几乎等于自报家门。

1.2 Magisk 的解法:一个字节都不动 system

Magisk 的思路是把所有修改从"改分区"变成"改挂载":

  1. 安装 Magisk 时,只修补引导镜像——多数设备是 boot.img(内核 + ramdisk),Android 13 起采用 GKI 的新设备则多为独立的 init_boot.img,注入自己的守护进程与执行环境;
  2. 所有用户修改集中存放在数据分区的 /data/adb/modules/ 下;
  3. 每次开机早期,Magisk 把每个模块的文件以挂载的方式"叠加"到真实文件系统对应路径上。

对系统和应用来说,看到的 /system/fonts/xxx.ttf 就是模块里那个文件;而 system 分区本身从头到尾没有被写入过任何一个字节。这套机制就是所谓的 systemless(无系统修改)。它的收益是结构性的:

  • 想撤销?删掉模块,下次开机就是纯净系统;
  • OTA 前担心冲突?卸载 Magisk 时选择"恢复原厂镜像(Restore Images)"即可还原被修补的分区;
  • 修改以模块为单位分发、安装、卸载,形成了整个刷机社区的插件生态。

1.3 必要的分区知识:模块路径如何映射多分区

现代 ROM 的系统文件散落在四个分区:

分区典型内容
/system框架、系统 App、字体、build.prop
/vendor厂商 HAL、传感器配置
/product预装应用、运营商定制
/system_ext从 system 拆分出的扩展组件

关键在于:不同机型上,这些分区可能是独立的物理分区,也可能被合并挂载在 /system 之下(例如 /vendor 实际挂在 /system/vendor)。Magisk 对模块开发者屏蔽了这种差异——你只需要把文件放进模块 system/ 目录下的对应子路径:

模块内路径                                真实目标
system/fonts/Foo.ttf                 →   /system/fonts/Foo.ttf
system/vendor/etc/bar.conf           →   /vendor/etc/bar.conf
system/product/app/Baz/Baz.apk       →   /product/app/Baz/Baz.apk
system/system_ext/framework/x.jar    →   /system_ext/framework/x.jar

官方文档的说法是:无论这些目录是否为独立分区,Magisk 都会透明地处理挂载。另外要记住一条规矩:模块目录里可能出现的 vendorproductsystem_ext 三个入口是 Magisk 自动生成的符号链接,开发者不要自己创建,统一从 system/ 子路径出发。

1.4 开机时序:模块在哪个环节登场

把一次开机中与模块有关的节点串起来,是理解后面所有脚本行为的基础:

内核启动 init

挂载 /data 并解密

post-fs-data 阶段
执行各模块 post-fs-data.sh
(阻塞, 上限约 40 秒)

Magic Mount
把各模块 system/ 子树
挂载到真实路径

启动 Zygote / system_server

late_start 服务阶段
执行各模块 service.sh
(非阻塞, 后台运行)

开机动画结束 sys.boot_completed=1

用户交互阶段
Magisk App / action.sh / WebUI

记住两个分界线:post-fs-data.sh 在模块挂载之前运行(所以它可以动态调整模块内容),service.sh 在系统大部队启动之后运行(所以它适合做"开机后"的事情)。这条时间轴会在第六节反复出现。

二、模块的本质:/data/adb/modules 下的一棵目录树

一个"已安装"的 Magisk 模块,就是 /data/adb/modules/<模块id>/ 下的一棵普通目录树。没有注册表、没有数据库,一切皆文件。完整布局如下:

/data/adb/modules/<id>/
├── module.prop          # 模块信息(唯一必需的文件)
├── system/              # 要叠加到系统分区的文件树
├── post-fs-data.sh      # 早期脚本(阻塞阶段执行)
├── service.sh           # 晚期脚本(late_start 阶段执行)
├── action.sh            # Magisk App 中按钮触发的脚本
├── uninstall.sh         # 模块被卸载时执行
├── system.prop          # 以 resetprop 方式追加的系统属性
├── sepolicy.rule        # SELinux 规则补充
├── zygisk/              # Zygisk 模块的原生库(如开发 Zygisk 模块)
├── webroot/             # WebUI 静态页面(v27+ 支持的约定)
├── skip_mount           # 标记文件:存在则不挂载本模块的 system/
├── disable              # 标记文件:存在则模块整体禁用
└── remove               # 标记文件:存在则下次重启时移除模块

除了 module.prop,其余全部可选。一个只包含 module.propsystem/ 的模块,已经是一个能用的模块了。

三个标记文件(skip_mountdisableremove)是 Magisk 的"开关与刹车",它们不是给安装器用的,而是给运行时和你自救用的——比如某个模块导致无法开机,你可以通过 ADB 或 Recovery 执行:

# 禁用而不是删除,便于排查(/data/adb 归 root 所有,需要 root shell)
adb shell su -c 'touch /data/adb/modules/<id>/disable'
# 或直接标记为下次重启移除
adb shell su -c 'touch /data/adb/modules/<id>/remove'

关于挂载机制多说一句:官方把这套按路径逐文件叠加的机制称为 Magic Mount,其内部实现随 Magisk 版本持续演进(绑定挂载、overlay 等),但模块开发者完全不需要关心内部实现——你唯一需要遵守的就是"把文件放到 system/ 下对应的路径",剩下的交给 Magisk。

三、module.prop:模块的身份证

module.prop 是模块唯一必需的文件,格式是极简的 key=value

id=hello_bootcount
name=Hello Boot Counter
version=v1.0.0
versionCode=10000
author=your_name
description=一个用于演示的开机计数模块
updateJson=https://example.com/hello_bootcount/update.json

各字段规则与注意事项:

字段必填规则与建议
id必须匹配正则 ^[a-zA-Z][a-zA-Z0-9._-]+$:以字母开头,之后是字母/数字/./_/-。合法如 a_modulemodule-101;非法如 1_module-a-modulea module(含空格)。这是模块的唯一标识,一旦发布就不要再改——改了 id 等于一个新模块,老用户升级会变成"并存双模块"
name展示名,随便起,支持中文,但建议附英文(部分管理器对非 ASCII 兼容不佳)
version展示用版本串,建议语义化版本(v1.2.0),只给人看
versionCode必须是整数,Magisk 用它判断新旧,每次发版必须递增。常用习惯是从 10000 起步,按 1010010200 递进,为小修留空间
author作者名
description一句话描述,会显示在模块列表里,别写营销文案
updateJson指向一个 JSON 的 URL,让 Magisk App 具备在线检查更新能力,见第九节

此外,部分社区管理器(如 MMRL 等)还识别 donatesupport 之类的扩展字段,属于生态约定而非官方规范,需要时再了解即可。

三个实战教训,全部来自真实翻车:

  1. 换行符必须是 LF。在 Windows 上用记事本编辑 module.prop,存出来是 CRLF,解析出来的 id 末尾会带不可见字符,模块直接安装失败或行为诡异。这是 Windows 上开发模块的第一大坑。
  2. 文件编码用 UTF-8(无 BOM)description 里写中文时尤其注意。
  3. 别动 update-binary。安装器脚本会被 Magisk App 强制替换为官方版本,任何自定义修改都是无用功(下一节展开)。

四、安装器与 customize.sh:安装期你能做什么

4.1 模块 zip 的结构

把模块打包成 zip,通过 Magisk App 的"从本地安装"刷入。结构上有一处非常容易踩坑的地方——zip 的根目录就是模块文件本身

# ✅ 正确:zip 根目录直接是 module.prop、system/ ...
hello.zip
├── module.prop
├── customize.sh
├── system/
│   └── ...

# ❌ 错误:多套了一层目录,安装器找不到 module.prop
hello.zip
└── hello/
    ├── module.prop
    └── ...

在 Windows 资源管理器里右键压缩文件夹时,几乎必然得到第二种结构——因为压缩的是"文件夹"而不是"文件夹的内容"。用命令行打包可以杜绝这个问题。

另外两个文件仅在需要支持第三方 Recovery(如 TWRP)刷入时才需要:

META-INF/com/google/android/update-binary    # 官方 module_installer.sh 原样放入
META-INF/com/google/android/updater-script   # 内容只有一行字符串: #MAGISK

update-binary 是官方模板提供的通用安装器,所有模块共用同一个文件,不要在里面添加任何自定义逻辑——Magisk App 安装模块时会用自己内置的最新版强行替换它。用 Magisk App 安装模块则完全不需要这两个文件,只要有 module.prop 就能识别。

4.2 customize.sh:被"source"而不是被执行

customize.sh 是模块安装期的钩子。它不是被独立执行的,而是被安装器 source 进当前 shell——所以你可以直接使用安装器注入的变量和函数,不需要自己 export 什么。

安装器提供的变量:

变量含义
MAGISK_VER / MAGISK_VER_CODE当前 Magisk 版本,可用于最低版本检查
BOOTMODE是否在系统内通过 Magisk App 安装(true/false),Recovery 刷入时为 false
MODPATH本模块的安装目标路径(安装期指向 /data/adb/modules_update/<id> 暂存目录,重启后才转正为 /data/adb/modules/<id>
TMPDIR安装临时目录
ZIPFILE本次安装的 zip 文件路径
ARCH设备架构:arm / arm64 / x86 / x64 / riscv64
IS64BIT是否 64 位
APIAndroid API 级别(如 34 对应 Android 14),做版本分支判断用

安装器提供的函数:

函数用途
ui_print <msg>把信息打印到安装界面。在 Recovery 里 echo 是看不见的,一律用 ui_print
abort <msg>报错并终止安装。不要用 exit——会跳过安装器的清理逻辑
set_perm <目标> <属主> <属组> <权限> [context]设置单个文件/目录的权限,默认 SELinux context 为 u:object_r:system_file:s0
set_perm_recursive <目录> <属主> <属组> <目录权限> <文件权限> [context]递归设置权限

默认情况下,安装器已经把文件权限统一处理为:属主/属组 0:0,目录 0755、文件 0644。绝大多数模块不需要额外动权限;需要可执行位或特殊属主时再用 set_perm 微调。

4.3 一个标准的 customize.sh

ui_print "======================================"
ui_print "  Hello Boot Counter v1.0.0"
ui_print "======================================"
ui_print "- 架构: $ARCH (Android API $API)"
ui_print "- Magisk: $MAGISK_VER (code $MAGISK_VER_CODE)"
ui_print "- 系统内安装: $BOOTMODE"

# 最低版本门槛:低于 Magisk 26 的环境直接拒绝安装
if [ "$MAGISK_VER_CODE" -lt 26000 ]; then
  abort "本模块需要 Magisk v26.0 及以上版本"
fi

# 按架构做分支是模块的常见需求
if [ "$ARCH" != "arm" ] && [ "$ARCH" != "arm64" ]; then
  abort "本模块仅支持 arm / arm64 设备"
fi

# 默认权限不满足需求时手动调整
set_perm_recursive "$MODPATH" 0 0 0755 0644
set_perm "$MODPATH/service.sh" 0 0 0755
set_perm "$MODPATH/action.sh"  0 0 0755

ui_print "- 安装完成,重启后生效"

三条铁律(全部来自官方文档的明确警告):

  1. 不要修改 update-binary(会被强制替换,理由见 4.1);
  2. 不要在 zip 里放名为 install.sh 的文件(会与安装流程约定冲突);
  3. customize.sh 结尾不要写 exit——用 abort 处理错误,正常结束让它自然返回,否则清理逻辑被跳过,安装状态可能残留。

4.4 SKIPUNZIP、REPLACE 与 REMOVE

三个进阶工具,能显著减少样板代码:

SKIPUNZIP=1——在 customize.sh 开头声明,安装器就不再自动解压 zip,整个解压流程由你接管。适合需要按架构挑选文件、动态生成配置的模块。接管后你需要自己完成解压,安装器提供的 unzip 函数可用:

SKIPUNZIP=1
unzip -o "$ZIPFILE" "system/*" -d "$MODPATH" >/dev/null

REPLACE 列表——声明需要"整体替换"的目录(自动生成 .replace 标记,语义见下一节):

REPLACE="
/system/app/YouTube
/system/priv-app/MiuiSystemUI
"

REMOVE 列表——声明需要"删除"的文件或目录(自动生成空字符设备标记):

REMOVE="
/system/app/YouTube/YouTube.apk
/system/etc/permissions/android.hardware.telephony.xml
"

这两个列表本质是第五节三种修改语义的语法糖——手工摆文件和写列表,效果完全等价,选可读性更好的那种。

五、system/ 目录:改文件的三种语义

模块对系统的文件级修改全部通过 system/ 子树表达,共三种语义:替换/新增(默认合并)、整体覆盖目录(.replace)、删除(空字符设备)

5.1 合并(默认)

把文件按路径放进 system/,开机挂载后,同路径的系统文件被模块文件顶替,系统里不存在的路径则成为"新增文件"。逐文件生效、逐目录合并,不会影响同目录下的其他文件:

system/fonts/CustomFont.ttf   → /system/fonts/CustomFont.ttf
system/etc/hosts              → /system/etc/hosts
system/app/MyTool/MyTool.apk  → /system/app/MyTool/MyTool.apk  (新增的"系统应用")

5.2 整体替换目录:.replace

system/ 下的某个目录里放一个名为 .replace 的空文件,该目录不再与系统目录合并,而是整个被模块目录替换——原目录里所有内容一刀切消失,只剩下模块目录里的东西。想"彻底干掉某个系统 App"时最常用:

system/app/YouTube/.replace   → /system/app/YouTube 整个目录被替换为空,App 消失

与上一节 REPLACE 列表等价。注意区别:.replace目录级别的操作,杀伤半径是整个目录,想精细删除单个文件用下一种。

5.3 删除单个文件:mknod 字符设备

Magisk 约定:system/ 下如果某个路径是一个主次设备号均为 0 的字符设备文件,挂载后对应的真实文件将被"抹除"(表现为不存在):

# 在 customize.sh 中,等价于 REMOVE 列表的效果(mknod 需要父目录已存在)
mkdir -p "$MODPATH/system/app/YouTube"
mknod "$MODPATH/system/app/YouTube/YouTube.apk" c 0 0

手工打包时也可以用 shell 事先造好这种文件。不过实践中更推荐 REMOVE 列表写法——不必在仓库里维护特殊文件。

5.4 skip_mount

如果模块目录下存在 skip_mount 标记文件,Magisk 将完全跳过该模块 system/ 子树的挂载。纯脚本型模块(只有 service.sh 做事,不改任何系统文件)可以带上它,明确表达意图、避免无谓的挂载开销。

六、五个脚本:执行时机决定一切

Magisk 模块共有五个钩子脚本,各自在完全不同的时机、以不同的约束运行。选错时机是模块 bug 的第一大来源,这一节值得反复对照。

脚本执行时机是否阻塞典型用途
post-fs-data.sh/data 挂载解密后、模块挂载之前、Zygote 之前阻塞(开机暂停等它,上限约 40 秒后强制放行)极少数必须"赶在一切之前"的准备动作
service.shlate_start 服务阶段,系统启动大部队之后非阻塞,后台运行绝大多数模块的默认选择
action.sh用户在 Magisk App 模块卡片点按钮时由用户触发手动执行动作、刷新配置
uninstall.sh模块被 Magisk 移除时清理模块在模块目录之外产生的数据
customize.sh安装时(上一节已详述)安装期逻辑

6.1 post-fs-data.sh:快,但要小心

官方文档对这个阶段的定性原文是:阻塞(BLOCKING),开机流程会停下来等脚本跑完,或者等满约 40 秒。同时它在模块 system/ 挂载之前执行——所以你可以在这个脚本里动态生成、调整模块即将挂载的文件。

两个硬性禁忌:

  • 绝对不要在 post-fs-data.sh 里用 setprop,会直接死锁开机流程。官方明确要求改用 resetprop -n <名称> <值>——-n 表示绕过 property_service 直接设置属性,不触发可能卡死开机的属性变更事件。
  • 不要做任何耗时操作(网络请求、大文件处理)。这个阶段网络一定不可用,且你的每一秒都是用户盯着开机动画的一秒。

什么才值得放进 post-fs-data?官方建议是"非必要不使用"。真实案例:根据某个属性值决定今天要不要挂载某个文件——这种"必须赶在挂载前做的决策"才是它的主场。

6.2 service.sh:默认选项

late_start 服务阶段意味着:Zygote 已启动、系统服务正在并行拉起、你的脚本与其他模块脚本并行在后台跑。对绝大多数模块,service.sh 就够了——开机自启动的服务、定时任务、检测逻辑全放这里。

两个细节:

  • 此阶段 UI 与网络大概率已可用,但"完全就绪"不保证。需要等系统完全启动,用属性轮询:
until [ "$(getprop sys.boot_completed)" = "1" ]; do
  sleep 1
done
  • 官方文档同时提到可以用 resetprop -w 等待属性(resetprop -w <属性名> <值>),具体语义以设备上 resetprop --help 为准;轮询写法虽然朴素,但行为最透明,社区模块里也最常见。

6.3 通用脚本环境:BusyBox 与 MODDIR

所有模块脚本由 Magisk 内置的 BusyBox ash 解释,且运行在 standalone 模式:脚本里调用的 lsgrepsed 等都解析为 BusyBox applet,不受系统 PATH 污染——这保证了脚本在不同 ROM 上行为一致,你可以放心使用 BusyBox 提供的丰富命令,而不必依赖各 ROM 自带 toybox/toolbox 的实现差异。

一条官方强调的编码惯例:

MODDIR=${0%/*}

用脚本自身路径推导模块目录,永远不要硬编码 /data/adb/modules/<id>。顺带一提,老教程里常见的 /sbin/magisk 路径在 Android 11+ 上已不可靠(Magisk 的 tmpfs 挂载点随版本变化,可能位于 /debug_ramdisk 下),凡引用 Magisk 运行时路径的脚本都需要以此为准重新审视。

环境变量方面:若设备启用了 Zygisk,模块所有脚本里 ZYGISK_ENABLED 会被置为 1,可以据此做兼容分支。

6.4 action.sh 与 uninstall.sh

action.sh 在用户点击 Magisk App 里模块卡片上的执行按钮时运行,脚本的输出通常会在 App 界面里展示(不同管理器版本的行为可能略有差异,拿不准就同时写入日志)。注意这里是 echo 的世界,ui_print 是安装器的函数,别带过来。适合"立即刷新一次配置""手动跑一次清理"这类动作。

uninstall.sh 在 Magisk 移除模块时执行,用来清理模块在自身目录之外产生的文件——比如你的 service.sh/data/local/tmp 写过的缓存。注意:模块目录本身由 Magisk 负责删除,不用你管。

6.5 system.prop 与 sepolicy.rule

两个"声明式"配置文件,放好了就生效,不需要脚本:

  • system.propkey=value 格式,逐行声明要修改的系统属性(等价于 resetprop 的批量声明)。改 DPI、关摄像头声音这类属性级修改用它最干净。
  • sepolicy.rule:一行一条 magiskpolicy 语句,为你的脚本/程序补充 SELinux 放行规则。遇到 avc: denied 相关问题时按需添加,格式示例:
allow <源context> <目标context> <类别> <权限>

排查流程:adb logcat | grep avc 找到 denial 行,按其内容写规则。sepolicy 规则写得过宽会引入安全风险,只放行确实需要的最小权限。

最后一条官方约定:Magisk 还有面向"非模块"的通用脚本目录 /data/adb/post-fs-data.d//data/adb/service.d/模块不要往这两个目录安装脚本——那是给用户手动放临时脚本用的,模块的一切都该待在自己的目录里。

七、实战一:从零写一个完整模块(全流程)

目标:写一个"开机计数器"模块。麻雀虽小,覆盖安装器、service.sh、action.sh、打包、安装、验证、卸载全流程,且零风险——它只在自己的目录里写日志,不碰任何系统文件。

7.1 目录结构与全部代码

hello_bootcount/
├── module.prop
├── customize.sh
├── service.sh
└── action.sh

module.prop(注意:LF 换行、UTF-8 无 BOM):

id=hello_bootcount
name=Hello Boot Counter
version=v1.0.0
versionCode=10000
author=your_name
description=教学示例:记录每次开机时间与累计次数,验证模块全链路

customize.sh(4.3 节模板的精简版:省去了版本与架构检查。安装器默认权限本就是属主 0:0、目录 0755、文件 0644,这里显式声明一遍,是为了给两个脚本补上可执行位):

ui_print "======================================"
ui_print "  Hello Boot Counter v1.0.0"
ui_print "======================================"
ui_print "- 架构: $ARCH (Android API $API)"
ui_print "- Magisk: $MAGISK_VER"

set_perm_recursive "$MODPATH" 0 0 0755 0644
set_perm "$MODPATH/service.sh" 0 0 0755
set_perm "$MODPATH/action.sh"  0 0 0755

ui_print "- 安装完成,重启后生效"

service.sh

MODDIR=${0%/*}

# 等待系统完全启动
until [ "$(getprop sys.boot_completed)" = "1" ]; do
  sleep 1
done

NOW=$(date '+%Y-%m-%d %H:%M:%S')
COUNT=$(( $(cat "$MODDIR/count" 2>/dev/null || echo 0) + 1 ))
echo "$COUNT" > "$MODDIR/count"
echo "[$NOW] 第 $COUNT 次开机,模块运行于 $MODDIR" >> "$MODDIR/boot.log"

action.sh

MODDIR=${0%/*}
NOW=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$NOW] 用户在 Magisk App 中按下了执行按钮" >> "$MODDIR/boot.log"
echo "已追加一条记录,当前累计开机 $(cat "$MODDIR/count" 2>/dev/null || echo 0) 次"

7.2 打包

关键点只有一个:在模块目录内部打包,让 zip 根目录直接是模块文件

Git Bash / Linux / macOS:

cd hello_bootcount
zip -r9 ../hello_bootcount-v1.0.0.zip .

Windows 用户特别注意:不要用资源管理器右键"压缩成 zip 文件"(必然多套一层目录),也不建议用 PowerShell 的 Compress-Archive(它生成的 zip 条目路径使用反斜杠分隔符,属于非标准实现,BusyBox 的 unzip 等解压端可能把文件解到错误位置)。稳妥选择:7-Zip(进目录全选再压缩)或 Git Bash 自带的 zip

打包后自查结构(两种环境任选):

unzip -l hello_bootcount-v1.0.0.zip
# 第一层就应该看到 module.prop、customize.sh、service.sh、action.sh

7.3 安装与验证

# 推到手机并安装(或直接在 Magisk App 里选择本地文件)
adb push hello_bootcount-v1.0.0.zip /data/local/tmp/

在 Magisk App → 模块 → 从本地安装,选中 zip。安装界面应当输出 customize.sh 里每一条 ui_print。注意此时模块处于"已安装、待重启"状态(安装到 /data/adb/modules_update/,重启后才转正到 /data/adb/modules/)。重启后:

# 确认模块就位且处于激活状态(无 disable/remove 文件)
adb shell su -c 'ls /data/adb/modules/hello_bootcount'

# 查看脚本产出
adb shell su -c 'cat /data/adb/modules/hello_bootcount/boot.log'
# 期望输出: [2026-08-15 10:23:45] 第 1 次开机,模块运行于 /data/adb/modules/hello_bootcount

再到 Magisk App 的模块卡片上点一次执行按钮,然后重新 cat 日志,应当多出 action.sh 写入的一行。至此,安装器、开机脚本、手动脚本三条链路全部验证通过。

7.4 卸载与"假死"实验

建议完整走一遍:

  1. Magisk App 里卸载模块,重启,确认 /data/adb/modules/hello_bootcount 不存在;若模块实现了 uninstall.sh,卸载时就是它被执行的时机,负责清理模块目录之外的产物;
  2. 重新安装后,adb shell su -c 'touch /data/adb/modules/hello_bootcount/disable',重启,确认日志不再新增——这就是排查问题时的"软停用";
  3. 同样方式试一次 remove 标记,理解它与 disable 的区别(下次重启彻底移除)。

八、实战二:维护一个 HyperOS 字体模块的真实经验

第二个实战来自我长期使用的真实场景:给小米 HyperOS 设备更换全局字体。这类模块在 MIUI/HyperOS 玩家群里非常流行,它的维护经验比教学示例更能体现"工程问题"。

8.1 原理与路径的现实

字体替换的原理很直白:系统渲染文字时按固定顺序在字体目录里查找字体文件,用模块把目标 ttf 顶替成同名文件即可全局换字体。/system/fonts/ 下的通用字体路径是公开且稳定的。

但 MIUI/HyperOS 有自己的主题字体机制(主题商店的"字体"就是走私有通道),不同 ROM 版本、不同机型上私有路径存在差异。成熟的做法是参考社区里同机型的现成模块,确认它在你的 ROM 版本上替换的具体文件,再动手做自己的版本——不要凭网上的零散教程盲猜路径。这也是字体模块"机型/ROM 版本适配"问题的根源:换一台设备、升一次大版本,路径就可能变化。

8.2 瘦身:模块体积也是体验

很多字体模块是从别处转打包的:原包为了覆盖所有语言区域,带着十几个按语言细分的 ttf 变体,而绝大多数中文用户实际用到的只有主字体一个。做瘦身版时把用不到的语言变体全部剔除,最终 21.8 MB 的模块包相比原包小了一大圈——对每次更新都要重新下载模块的用户来说,下载体积直接决定更新体验。

裁剪的原则:先在实机上验证默认语言渲染正常,再逐个剔除变体并验证设置里切换语言的场景(如果你确实需要多语言)。字体文件的版权也要留意:个人使用与公开分发是两回事,把商用授权字体打包进公开模块分发存在侵权风险。

8.3 升级策略:id 不变,原地升级

这是维护期最重要的设计。我的做法:

  • id 永不变。它既是模块目录名,也是升级判定的锚点。id 一变,老用户升级时会装出一个并行的新模块,旧模块还赖在系统里;
  • 每次更新只递增 versionCodeversion 展示串同步改)。Magisk 靠整数比较判断新旧,忘了递增就是"更新了个寂寞"——App 会提示已是最新;
  • 日常换字体的最快路径:直接把新 ttf 换进模块目录覆盖同名文件,重启生效,连重装都不用(/data/adb 归 root 所有,adb push 直写会报权限错误,先推到临时目录再用 su 拷入):
adb push NewFont.ttf /data/local/tmp/
adb shell su -c 'cp /data/local/tmp/NewFont.ttf /data/adb/modules/<你的字体模块id>/system/fonts/目标.ttf'
adb reboot

同名覆盖 + 重启,挂载的就是新文件。这个技巧对所有"纯文件替换型"模块都成立,非常实用;

  • 正式发版则走完整流程:改 module.prop 版本 → 打包 → 上传 → 更新 updateJson(第九节),用户在 App 内一键升级。

8.4 Windows 环境的两个高频翻车点

再强调一次它们,因为字体模块的维护脚本最容易踩:

  1. module.prop 被编辑器悄悄存成 CRLF 或 UTF-8 BOM,导致 id 解析异常;
  2. 打包时多套一层目录。字体模块目录里文件又多又大,出问题时排查非常痛苦,打包命令务必固定成脚本而不是每次手敲。

九、进阶能力:在线更新、WebUI 与 Zygisk 的边界

9.1 updateJson:一行配置换在线更新

module.prop 里配置 updateJson 指向一个可公网访问的 JSON,Magisk App 就会自动检查并提示升级:

{
    "version": "v1.1.0",
    "versionCode": 10100,
    "zipUrl": "https://example.com/hello_bootcount/v1.1.0.zip",
    "changelog": "https://example.com/hello_bootcount/changelog_v1.1.0.md"
}

四个字段:version 展示串、versionCode 整数(App 靠它判断要不要提示更新,必须与 zip 内 module.prop 一致且大于已装版本)、zipUrl 新版 zip 直链、changelog 更新日志链接。把 JSON 和 changelog 托管在稳定直链上(GitHub Releases、Gitee、对象存储均可),就得到了一个零后端的自动更新体系。

9.2 WebUI:给模块一个操作界面

从 Magisk v27.0 起,App 支持 KernelSU 风格的模块 WebUI:在模块目录放一个 webroot/ 子目录,入口为 webroot/index.html,用户点击模块即可打开这个内嵌页面。页面里可以配合套接字与模块后台脚本通信,实现配置开关、状态展示这类图形界面。

需要说明的是,这套约定源自 KernelSU 生态,Magisk 的管理器支持相对较晚且早期功能有限,社区里 WebUI-X、MMRL 等第三方管理器对 WebUI 的兼容往往更好。如果你的模块重度依赖 WebUI 交互,README 里建议同时推荐这些打开方式。

9.3 Zygisk 模块:另一条技术路线

本文讲的都属于"脚本 + 文件挂载"型模块。另一类模块基于 Zygisk——通过在 Zygote 进程注入原生库,实现在每个应用进程里运行代码(做 Hook、按应用隐藏 root、注入框架等)。它有几个显著特征:

  • 代码是放在模块 zygisk/ 目录下的各 ABI 原生库(arm64-v8a.soarmeabi-v7a.so 等),用 C/C++ 开发,遵循 Zygisk API;
  • 与脚本模块完全不是一个开发范式,开发门槛高得多,官方提供了专门的示例仓库(zygisk-module-sample);
  • 模块脚本里可通过 ZYGISK_ENABLED 环境变量感知 Zygisk 是否开启,据此做优雅降级;
  • 若原生库加载失败(例如与设备不兼容),Magisk 会在模块目录留下 unloaded 标记文件。

一句话划界:改文件、跑脚本用本文的体系;要在应用进程里做进程内 Hook,才需要 Zygisk。此外还有面向 boot ramdisk 定制的 overlay.d 机制(替换或追加 .rc、向 Magisk tmpfs 塞文件),属于更底层的少数派需求,官方文档有专门章节,此处不展开。

十、调试、自救与发布

10.1 调试手段

  • 脚本自带日志:所有后台动作落到自己模块目录下的日志文件(如本文示例的 boot.log),这是成本最低、收益最大的习惯;
  • Magisk App 日志页:能看到安装与脚本执行的全局日志;
  • logcatadb logcat 是系统级问题的第一现场,SELinux 问题用 adb logcat | grep avc 定位;
  • 分阶段验证:改完一个脚本先手动执行验证语法(adb shell su -c 'sh /data/adb/modules/<id>/service.sh'——注意手动跑不会复现开机时序,但能排除低级错误),再重启验证真实链路。

10.2 模块把手机搞挂了怎么办

每个模块开发者都会经历一次"卡开机"。救援阶梯从轻到重:

  1. :post-fs-data 脚本卡住时,约 40 秒后会被强制放行,先等够这个时间;
  2. 进安全模式:多数设备在开机动画期间长按音量下键可进入安全模式,随后用 Magisk App 卸载问题模块;
  3. ADB 标记:设备能被 ADB 识别时,执行 adb shell su -c 'touch /data/adb/modules/<id>/remove'(或 disable)后重启;确认是模块问题但拿不准是哪个时,官方还提供了一键移除全部模块的命令 adb shell su -c 'magisk --remove-modules'
  4. Recovery 兜底:第三方 Recovery 挂载 data 后同样可以创建标记文件;
  5. 最后的手段:卸载 Magisk 并通过它"恢复原厂镜像",或用 fastboot 刷回原厂引导分区、线刷整包,此处不展开。

把这条阶梯写进模块的 README,是对用户负责的一部分。

10.3 发布检查清单

打包发布前逐条核对:

检查项说明
zip 根目录无嵌套unzip -l 第一层就是 module.prop
LF 换行 / UTF-8 无 BOMmodule.prop 与所有 .sh,Windows 开发者重点自查
versionCode 已递增updateJson 内一致
id 未变更升级发布的前提
权限最小化可执行位只给需要的脚本,sepolicy.rule 只放行必要规则
Recovery 兼容(可选)需要时补 META-INF 两件套
README 完整用途、适用机型与 ROM 版本、安装/卸载方法、升级说明、救援方法、免责声明

分发渠道:国际社区以 GitHub Releases + XDA 为主;国内访问 GitHub 不稳定,可同步 Gitee 或网盘,并在 README 注明。开源模块的仓库里放源文件与打包脚本、Release 里放 zip,是社区的标准做法。

10.4 一点工程哲学

模块开发的特殊性在于:你的代码运行在别人每天要用的主力机上。系统级的写权限意味着一个 rm 的路径写错就可能是灾难。因此模块工程的默认姿势是保守的:

  • 只改必要的文件,能声明式(system/system.prop)就不命令式(脚本);
  • 所有副作用可追溯(日志)、可撤销(模块卸载即还原);
  • 发布前在自己设备上跑完整升级路径(旧版 → 新版),而不只测全新安装。

十一、结语

Magisk 模块开发的全部核心知识,其实就三句话:文件放对地方(system/ 三种语义)、脚本挑对时机(post-fs-data 阻塞早期 / service 非阻塞后期)、元数据守规矩(module.prop 的 id 与 versionCode)。剩下的——安装器的变量与函数、updateJson、WebUI、Zygisk——都是围绕这三件事的增强。

从原理走到发布,一个下午足够完成第一个模块。之后你会发现,限制你的不再是"会不会写模块",而是"想解决什么问题"。去读几个社区里口碑好的模块的源码(很多都开源),对照本文的框架看它们如何组织文件与脚本,是进阶最快的方式。

玩得开心,也善待每一位安装你模块的用户。

参考资料

  • Magisk 官方开发者指南(模块与脚本的全部机制权威出处):https://topjohnwu.github.io/Magisk/guides.html
  • Magisk 官方模块模板仓库:https://github.com/topjohnwu/magisk-module-template
  • Zygisk 官方示例模块仓库:https://github.com/topjohnwu/zygisk-module-sample
  • Magisk 官方更新日志(WebUI 等特性的引入版本):https://topjohnwu.github.io/Magisk/changes.html
  • resetprop 等内置工具文档(Magisk 仓库 docs/tools.md):https://github.com/topjohnwu/Magisk/blob/master/docs/tools.md

声明:本文为原创内容,转载请注明出处。Root 与模块修改可能影响设备保修,部分金融类应用会检测设备完整性,请自行评估风险;字体等资源文件务必注意版权,仅分发拥有授权的内容。

内容概要:本文围绕微电网群的双层优化与分布式优化问题,提出基于交替方向乘子法(ADMM)的分布式协同优化控制策略,并通过Matlab代码实现仿真验证。研究构建了上层为微电网间能量协调与优化调度、下层为各微电网内部源--储精细化运行管理的双层优化模型。采用ADMM算法将集中式优化问题分解为多个可并行求解的子问题,实现了计算的分布式化与信息隐私保护,显著提升了系统的可扩展性与鲁棒性。文中系统阐述了模型构建原理、ADMM算法设计流程及其收敛性分析,并通过仿真实验验证了该方法在降低系统综合运行成本、提高可再生能源消纳能力以及维持系统稳定运行方面的有效性。; 适合人群:具备电力系统分析、优化理论基础,熟悉Matlab编程,从事微电网、分布式能源系统、智能电网等领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:① 学习和掌握基于ADMM的分布式优化方法在微电网群协同能量管理中的具体应用;② 实现微电网群多主体参与下的经济调度与仿真分析;③ 深入理解双层优化架构的设计理念与分布式求解算法的实现机制。; 阅读建议:建议结合所提供的Matlab代码进行动手实践,重点剖析模型建立与算法实现的关键细节,可通过调整系统参数、改变运行场景等方式,深入探究ADMM算法的收敛特性及其对优化效果的影响。
内容概要:本文聚焦于风电出力不确定性的精确建模问题,提出采用拉丁超立方抽样(LHS)方法生成具有统计代表性的风电场景,并结合先进的场景缩减技术以降低计算复杂度。通过Matlab编程实现了LHS在高维随机变量空间中的均匀采样,有效克服了传统蒙特卡洛方法样本收敛慢、效率低的问题。在此基础上,引入基于聚类分析或概率距离度量的场景缩减算法,对初始大规模场景集进行优化合并,保留关键概率特征与出力趋势,构建出精简且具代表性的典型场景集。该方法显著提升了电力系统随机优化模型(如机组组合、储能调度、微电网能量管理)的求解效率与数值稳定性,同时确保输入场景的合理性与真实性,具备良好的可复现性与工程应用价值。; 适合人群:适用于具备一定电力系统分析基础和Matlab编程能力,从事新能源并网、随机规划、场景生成、电力市场及综合能源系统优化等方向研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握拉丁超立方抽样在风电不确定性建模中的理论原理与Matlab实现技巧;②学习并应用场景生成与缩减技术,提升随机优化问题的建模与求解效率;③为含高比例风电的电力系统调度、风险评估与决策分析提供高质量的输入场景支撑。; 阅读建议:建议读者结合所提供的Matlab代码逐模块调试运行,深入理解抽样策略、距离计算、聚类缩减等核心算法的实现细节,并尝试将其集成到具体的优化模型中进行验证与拓展,以强化科研实践能力与创新思维。
内容概要:本文围绕基于交替方向乘子法(ADMM)的多主体综合能源系统分布式协同优化展开研究,提出了一种利用ADMM算法实现多个能源主体间高效协同优化的解决方案。该方法将集中式优化问题分解为多个可并行求解的子问题,各主体在保护自身数据隐私的前提下,仅通过交换少量边界信息即可完成全局协同优化,有效解决了传统集中式方法存在的通信负担重、隐私泄露风险高等问题。研究涵盖了模型构建、算法设计、收敛性分析及仿真验证全过程,并以Matlab代码实现了算法原型,展示了其在提升系统运行效率、促进可再生能源消纳方面的潜力。该研究不仅提供了完整的算法实现框架,还深入探讨了ADMM在多区域电网、多微网及产消者等复杂场景下的适用性与扩展能力,为现代综合能源系统的分布式管理提供了理论依据和技术支持。; 适合人群:具备一定电力系统、优化理论及Matlab编程基础的研究生、科研人员或从事综合能源系统相关工作的工程技术人员。; 使用场景及目标:①应用于多区域电网、多微网、产消者(Prosumer)等多主体参与的综合能源系统协同调度;②实现数据隐私保护下的分布式优化,避免中心节点收集全局敏感信息;③学习ADMM算法在电力系统中的具体建模与实现方法,掌握其收敛特性与参数整定技巧。; 阅读建议:读者应结合提供的Matlab代码进行实践,重点关注ADMM算法的迭代流程、惩罚因子设定及其对收敛速度的影响,同时可通过修改系统规模与参数设置,进一步探究算法在不同场景下的适应性与性能表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

newcih

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值