TrueAsync Mobile
(演示项目,实验性,仓库 native-bridge,Android)
异步 PHP 非常适合 UI 应用:当程序在等待网络、读取磁盘,或等待下一个用户操作时, 界面不应该被卡住。TrueAsync 为此提供了专门的 C API:Trigger Event (zend_async_API.h 中的 ZEND_ASYNC_NEW_TRIGGER_EVENT())。它是一个只有单一方法 trigger() 的对象,任何 C 或 C++ 代码都可以从另一个线程调用它,以线程安全的方式 唤醒 PHP 反应器并把控制权交给它去处理事件。
native-bridge 正是为 Android 实现了这样的集成:PHP 作为持久化进程嵌入应用, 在后台线程上启动一次,运行一个事件循环(与整个生态系统中使用的同一个 TrueAsync 反应器),并双向与 Kotlin 通信。
为什么用持久化进程而不是请求/响应模式
常见的 PHP 场景是一次 Web 请求:进程启动,处理一个请求,然后退出。这不适合移动应用: PHP 需要在应用打开期间一直存活,并像处理程序响应 HTTP 请求那样响应用户事件 (点按、传感器、位置)。这正是 native-bridge 提供的能力:PHP 在应用启动时启动一次, 在自己的线程中一直运行,直到被显式停止,而该线程内的 TrueAsync 协程会并发地处理事件 和后台工作。
桥接架构
这个桥接工作在两个方向上:
- 从 Android 到 PHP。 Kotlin 把事件(一次点按、一条传感器读数、位置信息、 任意自定义事件)推入一个队列,PHP 从自己的循环中把它们取出。
- 从 PHP 到 Kotlin。 PHP 调用在 Kotlin 端实现的方法(显示一个 Toast、震动、 把文本复制到剪贴板,等等)。
两个方向都经过 JNI(Java Native Interface),这是让 C 代码调用 Kotlin/Java 代码、 反之亦然的标准 Android 机制。两个方向都不通过 JSON 或其他任何文本格式传递数据: 数值跨越边界时已经是带类型的,无需额外的转换。
PHP 运行在自己的操作系统线程上,永远不会阻塞 Android 的 UI 线程。如果 PHP 正在等待数据, UI 线程依然保持响应,反过来也是一样。
方向一:从 Android 到 PHP 的事件
Kotlin 通过 JNI 把事件发送到一个队列,PHP 用 NativeBridge::poll() 读取。当队列为空时, poll() 会立刻返回 null,PHP 应用自行决定是等待下一个事件,还是在此期间做点别的事情 (在演示应用中是一次短暂的 usleep() 暂停,期间 TrueAsync 得以运行后台协程和定时器)。
事件类型共有四种:屏幕触摸、位置数据、传感器数据(加速度计等),以及带名称和文本负载的 任意事件。演示应用正是用最后这种事件来标记按钮点击:
use TrueAsync\NativeBridge;
while (!NativeBridge::shouldStop()) {
$event = NativeBridge::poll();
if ($event === null) {
usleep(1000);
continue;
}
if ($event['type'] === NativeBridge::EVENT_GENERIC) {
match ($event['event_name']) {
'count_a' => $counterA->toggle(),
'fetch' => spawn(fn() => fetchDemo()),
default => null,
};
}
}前三种事件类型(触摸、位置、传感器)不需要字符串分配,因此即使在高调用频率下 (例如加速度计的数据流)依然保持轻量。
方向二:从 PHP 到 Kotlin 的调用
当 PHP 调用一个模块方法,例如 Toast::show('Hello', true),这个调用可以通过两种方式 到达 Kotlin:
通用路径
默认情况下,PHP 把参数打包进一个紧凑的带类型缓冲区(不使用 JSON 之类的字符串格式, 所以 Kotlin 无需解析文本,也不产生额外的内存分配),并通过一次调用 NativeBridge::invoke() 把它发出去。在这条路径上新增一个模块或方法完全不涉及 C 代码: 只需修改 Kotlin 和生成的 PHP 包装器,因此只需要对 Kotlin 端做一次 Gradle 重新构建, 不需要重新构建原生库。
快速路径:#[FastPath]
对于调用非常频繁的"热"方法(例如每一帧都要传入的传感器数据),PHP 规范会用 #[FastPath] 属性标记该方法。对于这样的方法,生成器会生成一个专门的带类型 C 函数, 直接通过 JNI 调用 Kotlin,不经过中间缓冲区。这类方法要求每次改动都重新构建原生库 (.so 文件),但运行更快,也没有额外的内存分配。方法本身的行为不会变化, 只是调用跨越 PHP/Kotlin 边界的方式不同。
描述一个模块:#[BridgeModule]
模块的契约在 PHP 端被描述为一个带 #[BridgeModule] 属性的接口:
namespace TrueAsync\Android\Spec;
#[BridgeModule]
interface ToastInterface
{
#[Ui]
public function show(string $text, bool $long): void;
public function batteryLevel(): int;
}- 模块名称由接口名称派生(
ToastInterface变成模块Toast),或者显式设置:#[BridgeModule('Clipboard')]。 - 方法上的
#[Ui]表示 Kotlin 端的实现必须运行在 Android 的 UI 线程上 (生成器会为你添加线程切换逻辑)。 - 方法上的
#[FastPath]启用上面描述的快速调用路径。
tools/bridge/gen.php 会生成什么
从一份 PHP 规范(一个带 #[BridgeModule] 属性的接口)出发,生成器每次运行时都会重建:
- 一个带抽象方法的 Kotlin 类(
ToastSpec); - 调用路由代码(Kotlin);
- 一个 PHP 包装器(
Toast::show(...)),供 PHP 应用其余代码调用; - 对于标记了
#[FastPath]的方法,直接调用 Kotlin 的带类型 C 代码。
PHP 应用生命周期
- Kotlin 在后台线程上启动 PHP,并传给它入口 PHP 脚本的路径。
- PHP 脚本调用
NativeBridge::init();从那一刻起,桥接就准备好接受事件和调用了。 - 从此应用在一个循环中运行:通过
poll()拉取事件、处理它们,并在需要时 (例如网络请求)派生后台 TrueAsync 协程。 - 关闭是优雅的:Kotlin 调用
NativeBridge.stop(),PHP 循环通过NativeBridge::shouldStop()感知到这一点,收尾并干净地释放资源。
示例:按钮上的计数器
一个基于演示应用简化后的例子:一个按钮用来启动和停止一个不断递增的计数器, 它的值直接更新在 UI 上。启动和停止都是用一个普通的 TrueAsync 协程的 spawn()/cancel() 实现的,不会阻塞 UI 线程:
use TrueAsync\NativeBridge;
use TrueAsync\Android\Ui;
use function Async\spawn;
use function Async\delay;
NativeBridge::init();
$root = Ui::newLinearLayout();
$button = Ui::newButton();
Ui::setText($button, '▶ start counter');
Ui::setOnClickListener($button, 'toggle');
Ui::addView($root, $button);
$label = Ui::newTextView();
Ui::setText($label, 'stopped');
Ui::addView($root, $label);
Ui::setContentView($root);
$counter = null;
function tick(int $label): void
{
$n = 0;
while (true) {
Ui::setText($label, 'tick ' . ++$n);
delay(400);
}
}
while (!NativeBridge::shouldStop()) {
$event = NativeBridge::poll();
if ($event === null) {
usleep(1000);
continue;
}
if ($event['type'] === NativeBridge::EVENT_GENERIC && $event['event_name'] === 'toggle') {
if ($counter === null) {
$counter = spawn(fn() => tick($label));
Ui::setText($button, '■ stop counter');
} else {
$counter->cancel();
$counter = null;
Ui::setText($button, '▶ start counter');
}
}
}第二次点击会通过 cancel() 取消 $counter 协程,计数器就停在它达到的那个值上。 包含多个独立计数器的完整示例在仓库的 android/app.php 中。
状态与限制
- 目前只支持 Android;iOS 支持已在规划中,但尚未实现。
- 桥接目前只能传递简单类型:字符串、整数、浮点数、布尔值。按字段传递复合对象 (同样不使用字符串格式)正在规划中。
- PHP 到 Kotlin 方向是同步的:方法会立即返回结果;这一侧尚不支持延迟(异步)结果。
- PHP 的 opcache 在 Android 上被强制关闭:应用沙箱不允许它使用所需的锁文件和 可执行内存。
- 需要线程安全(ZTS)的 PHP 构建,因为 PHP 运行在自己的操作系统线程上, 而不是应用的主线程。