Skip to content

系统模块 (APM) 开发指南

APatch 提供了一个模块机制(AndroidPatch Module),它可以在保持系统分区完整性的同时达到修改系统分区的效果;这种机制通常被称之为 systemless

APatch 的模块实现是从 KernelSU 模块复制并修改而来,感谢 KernelSU。

具体修改的代码对应位置:

KernelSU: https://github.com/tiann/KernelSU/tree/main/userspace/ksud
APatch: https://github.com/bmax121/APatch/tree/main/apd

以下的文档内容基本来自于 KernelSU 的文档,其中大部分内容与 KernelSU 方面保持一致。需要注意的主要有以下几个地方:

  1. 文件位置
  2. 环境变量
  3. SELinux 支持,APatch 直接使用了 magiskpolicy

APatch 的模块运作机制与 Magisk 几乎是一样的,如果你熟悉 Magisk 模块的开发,那么开发 APatch 的模块大同小异,你可以跳过下面有关模块的介绍,只需要了解 APatch 模块与 Magisk 模块的异同(编写中)。

BusyBox

APatch 提供了一个功能完备的 BusyBox 二进制文件(包括完整的SELinux支持)。可执行文件位于 /data/adb/ap/bin/busybox。 APatch 的 BusyBox 支持运行时可切换的 "ASH Standalone Shell Mode"。 这种独立模式意味着在运行 BusyBox 的 ash shell 时,每个命令都会直接使用 BusyBox 中内置的应用程序,而不管 PATH 设置为什么。 例如,lsrmchmod 等命令将不会使用 PATH 中设置的命令(在Android的情况下,默认情况下分别为 /system/bin/ls/system/bin/rm/system/bin/chmod),而是直接调用 BusyBox 内置的应用程序。 这确保了脚本始终在可预测的环境中运行,并始终具有完整的命令套件,无论它运行在哪个Android版本上。 要强制一个命令不使用BusyBox,你必须使用完整路径调用可执行文件。

在 APatch 上下文中运行的每个 shell 脚本都将在 BusyBox 的 ash shell 中以独立模式运行。对于第三方开发者相关的内容,包括所有启动脚本和模块安装脚本。

对于想要在 APatch 之外使用这个“独立模式”功能的用户,有两种启用方法:

  1. 设置环境变量 ASH_STANDALONE1
    例如:ASH_STANDALONE=1 /data/adb/ap/bin/busybox sh <script>
  2. 使用命令行选项切换:/data/adb/ap/bin/busybox sh -o standalone <script>

为了确保所有后续的 sh shell 都在独立模式下执行,第一种是首选方法(这也是 APatch 和 APatch 管理器内部使用的方法),因为环境变量会被继承到子进程中。

与 KernelSU 的差异

BusyBox 的位置由 /data/adb/ksu/bin/busybox 改到了 /data/adb/ap/bin/busybox

与 Magisk 的差异

APatch 的 BusyBox 现在是直接使用 Magisk 项目编译的二进制文件,感谢 Magisk! 因此,你完全不用担心 BusyBox 脚本与在 Magisk 和 APatch 之间的兼容问题,因为他们是完全一样的!

APatch 模块

APatch 模块就是一个放置在 /data/adb/modules 内且满足如下结构的文件夹:

txt
/data/adb/modules
├── .
├── .
|
├── $MODID                  <--- 模块的文件夹名称与模块 ID 相同
│   │
│   │      *** 模块配置文件 ***
│   │
│   ├── module.prop         <--- 此文件保存模块相关的一些配置,如模块 ID、版本等
│   │
│   │      *** 模块内容 ***
│   │
│   ├── system              <--- 这个文件夹通常会被挂载到系统
│   │   ├── ...
│   │   ├── ...
│   │   └── ...
│   │
│   │      *** 标记文件 ***
│   │
│   ├── skip_mount          <--- 如果这个文件存在,那么模块的 `/system` 将不会被挂载
│   ├── disable             <--- 如果这个文件存在,那么模块会被禁用
│   ├── remove              <--- 如果这个文件存在,下次重启的时候模块会被移除
│   │
│   │      *** 可选文件 ***
│   │
│   ├── post-fs-data.sh     <--- 这个脚本将会在 post-fs-data 模式下运行
│   ├── post-mount.sh       <--- 这个脚本将会在 post-mount 模式下运行
│   ├── service.sh          <--- 这个脚本将会在 late_start 服务模式下运行
│   ├── boot-completed.sh   <--- 这个脚本将会在 Android 系统启动完毕后以服务模式运行
|   ├── uninstall.sh        <--- 这个脚本将会在模块被卸载时运行
|   ├── action.sh           <--- 这个脚本将会在管理器模块中点击 Action 时运行
│   ├── system.prop         <--- 这个文件中指定的属性将会在系统启动时通过 resetprop 更改
│   ├── sepolicy.rule       <--- 这个文件中的 SELinux 策略将会在系统启动时加载
│   │
│   │      *** 自动生成的目录,不要手动创建或者修改! ***
│   │
│   ├── vendor              <--- A symlink to $MODID/system/vendor
│   ├── product             <--- A symlink to $MODID/system/product
│   ├── system_ext          <--- A symlink to $MODID/system/system_ext
│   │
│   │      *** Any additional files / folders are allowed ***
│   │
│   ├── ...
│   └── ...
|
├── another_module
│   ├── .
│   └── .
├── .
├── .

与 Magisk 的差异

APatch 没有内置的针对 Zygisk 的支持,因此模块中没有 Zygisk 相关的内容。

但你可以参照 Zygisk 支持? 中列出的方式来支持 Zygisk 模块,此时 Zygisk 模块的内容与 Magisk 所支持的 Zygisk 是完全相同的。

module.prop

module.prop 是一个模块的配置文件,在 APatch 中如果模块中不包含此文件,那么它将不被认为是一个模块;此文件的格式如下:

txt
id=<string>
name=<string>
version=<string>
versionCode=<int>
author=<string>
description=<string>
  • id 必须与这个正则表达式匹配:^[a-zA-Z][a-zA-Z0-9._-]+$ 例如:✓ a_module,✓ a.module,✓ module-101,✗ a module,✗ 1_module,✗ -a-module。这是您的模块的唯一标识符,发布后不应更改。
  • versionCode 必须是一个整数,用于比较版本。
  • 其他未在上面提到的内容可以是任何单行字符串。
  • 请确保使用 UNIX(LF)换行类型,而不是Windows(CR + LF)Macintosh(CR)

Shell 脚本

请阅读 启动脚本 一节,以了解 post-fs-data.sh, post-mount.sh, service.shboot-completed.sh 之间的区别。对于大多数模块开发者来说,如果您只需要运行一个启动脚本,service.sh 应该已经足够了。

在您的模块的所有脚本中,请使用 MODDIR=${0%/*}来获取您的模块的基本目录路径;请勿在脚本中硬编码您的模块路径。

与 Magisk, KernelSU 的差异

你可以通过环境变量 APATCH 来判断脚本是否运行在 APatch 中,如果运行在 APatch,这个值会被设置为 true

system 目录

这个目录的内容会在系统启动后,以 OverlayFS 的方式叠加在系统的 /system 分区之上,这意味着:

  1. 系统中对应目录的同名文件会被此目录的文件覆盖。
  2. 系统中对应目录的同名文件夹会与此目录的文件夹合并。

如果你想删掉系统原来目录某个文件或者文件夹,你需要在模块目录通过 mknod filename c 0 0 来创建一个 filename 的同名文件;这样 OverlayFS 系统会自动 whiteout 等效删除此文件(/system 分区并没有被更改)。

你也可以在 customize.sh 中声明一个名为 REMOVE 并且包含一系列目录的变量来执行删除操作,APatch 会自动为你在模块对应目录执行 mknod <TARGET> c 0 0。例如:

sh
REMOVE="
/system/app/YouTube
/system/app/Bloatware
"

上面的这个列表将会执行: mknod $MODPATH/system/app/YouTube c 0 0mknod $MODPATH/system/app/Bloatware c 0 0;并且 /system/app/YouTube/system/app/Bloatware 将会在模块生效后被删除。

如果你想替换掉系统的某个目录,你需要在模块目录创建一个相同路径的目录,然后为此目录设置此属性:setfattr -n trusted.overlay.opaque -v y <TARGET>;这样 OverlayFS 系统会自动将系统内相应目录替换(/system 分区并没有被更改)。

你可以在 customize.sh 中声明一个名为 REPLACE 并且包含一系列目录的变量来执行替换操作,APatch 会自动为你在模块对应目录执行相关操作。例如:

sh
REPLACE="
/system/app/YouTube
/system/app/Bloatware
"

上面这个列表将会:自动创建目录 $MODPATH/system/app/YouTube$MODPATH//system/app/Bloatware,然后执行 setfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/YouTubesetfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/Bloatware;并且 /system/app/YouTube/system/app/Bloatware 将会在模块生效后替换为空目录。

与 Magisk 的差异

APatch 的 systemless 机制是通过内核的 OverlayFS 实现的,而 Magisk 当前则是通过 magic mount (bind mount),二者实现方式有着巨大的差异,但最终的目标实际上是一致的:不修改物理的 /system 分区但实现修改 /system 文件。

如果你对 OverlayFS 感兴趣,建议阅读 Linux Kernel 关于 OverlayFS 的文档

system.prop

这个文件的格式与 build.prop 完全相同:每一行都是 [key]=[value] 的形式。

sepolicy.rule

如果您的模块需要一些额外的 SELinux 策略补丁,请将这些规则添加到此文件中。这个文件中的每一行都将被视为一个策略语句。

模块安装包

APatch 的模块安装包就是一个可以通过 APatch 管理器 APP 刷入的 zip 文件,此 zip 文件的格式如下:

txt
module.zip

├── customize.sh                       <--- (Optional, more details later)
│                                           This script will be sourced by update-binary
├── ...
├── ...  /* 其他模块文件 */

WARNING

APatch 模块不支持在 Recovery 中安装!

定制安装过程

如果你想控制模块的安装过程,可以在模块的目录下创建一个名为 customize.sh 的文件,这个脚本将会在模块被解压后导入到当前 shell 中,如果你的模块需要根据设备的 API 版本或者设备构架做一些额外的操作,那这个脚本将非常有用。

如果你想完全控制脚本的安装过程,你可以在 customize.sh 中声明 SKIPUNZIP=1 来跳过所有的默认安装步骤;此时,你需要自行处理所有安装过程(如解压模块,设置权限等)。

customize.sh 脚本以“独立模式”运行在 APatch 的 BusyBox ash shell 中。你可以使用如下变量和函数:

变量

  • KERNELPATCH (bool): 标记此脚本运行在 APatch 环境下,此变量的值将永远为 true

  • KERNEL_VERSION (hex): 从 KernelPatch 继承,内核版本号 (如: 50a01 是指 5.10.1)

  • KERNELPATCH_VERSION (hex): 从 KernelPatch 继承,KernelPatch 版本号 (如: a05 是指 0.10.5)

  • SUPERKEY (string): 从 KernelPatch 继承,用于调用 kpatch 或者 supercall

  • APATCH (bool): 标记此脚本运行在 APatch 环境下,此变量的值将永远为 true

  • APATCH_VER_CODE (int): APatch 当前的版本号 (如. 10672)

  • APATCH_VER (string): APatch 当前的版本名 (如. 10672)

  • BOOTMODE (bool): 此变量在 APatch 中永远为 true

  • MODPATH (path): 当前模块的安装目录

  • TMPDIR (path): 可以存放临时文件的目录

  • ZIPFILE (path): 当前模块的安装包文件

  • ARCH (string): 设备的 CPU 构架,只有 arm64

  • IS64BIT (bool): 是否是 64 位设备

  • API (int): 当前设备的 Android API 版本 (如:Android 6.0 上为 23)

WARNING

MAGISK_VER_CODE 在 APatch 为 27000MAGISK_VER 则为 27.0

函数

txt
ui_print <msg>
    print <msg> to console
    Avoid using 'echo' as it will not display in custom recovery's console

abort <msg>
    print error message <msg> to console and terminate the installation
    Avoid using 'exit' as it will skip the termination cleanup steps

set_perm <target> <owner> <group> <permission> [context]
    if [context] is not set, the default is "u:object_r:system_file:s0"
    this function is a shorthand for the following commands:
       chown owner.group target
       chmod permission target
       chcon context target

set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]
    if [context] is not set, the default is "u:object_r:system_file:s0"
    for all files in <directory>, it will call:
       set_perm file owner group filepermission context
    for all directories in <directory> (including itself), it will call:
       set_perm dir owner group dirpermission context

启动脚本

在 APatch 中,根据脚本运行模式的不同分为两种:post-fs-data 模式和 late_start 服务模式。

  • post-fs-data 模式

    • 这个阶段是阻塞的。在执行完成之前或者 10 秒钟之后,启动过程会暂停。
    • 脚本在任何模块被挂载之前运行。这使得模块开发者可以在模块被挂载之前动态地调整它们的模块。
    • 这个阶段发生在 Zygote 启动之前。
    • 使用 setprop 会导致启动过程死锁!请使用 resetprop -n <prop_name> <prop_value> 代替。
    • 只有在必要时才在此模式下运行脚本
  • late_start 服务模式

    • 这个阶段是非阻塞的。你的脚本会与其余的启动过程并行运行。
    • 大多数脚本都建议在这种模式下运行

在 APatch 中,启动脚本根据存放位置的不同还分为两种:通用脚本和模块脚本。

  • 通用脚本

    • 放置在 /data/adb/post-fs-data.d, /data/adb/post-mount.d, /data/adb/service.d/data/adb/boot-completed.d 中。
    • 只有在脚本被设置为可执行(chmod +x script.sh)时才会被执行。
    • post-fs-data.d 中的脚本以 post-fs-data 模式运行,在 service.d 中的脚本以 late_start 服务模式运行。
    • 模块不应在安装过程中添加通用脚本。
  • 模块脚本

    • 放置在模块自己的文件夹中。
    • 只有当模块被启用时才会执行。
    • post-fs-data.sh 以 post-fs-data 模式运行,post-mount.sh 以 post-mount 模式运行,而 service.sh 则以 late_start 服务模式运行,boot-completed 在 Android 系统启动完毕后以服务模式运行。

所有启动脚本都将在 APatch 的 BusyBox ash shell 中运行,并启用“独立模式”。