Binder 线程池耗尽案例:持锁同步调用 VHAL 导致 SystemUI 卡顿或 ANR

适用场景:SystemUI / Cluster / CarSettings 等上游通过同步 Binder 调用 carserver 或 system_server;服务端 Binder 线程在处理请求时,持锁同步调用 VHAL 或其他下游 Binder 服务;下游长时间不回包,最终耗尽服务端 Binder 线程池。

源码依据:

  • AOSP:~/code/android16_r2
  • Kernel:~/code/android16_r2_kernel/common

1. 结论

这类问题的本质不是某个 Binder 调用单独慢,而是:

text 复制代码
上游同步 Binder 调用
  + 服务端 Binder 线程持锁
  + 服务端 Binder 线程再次发起同步下游 Binder 调用
  + 下游 VHAL 或服务长期不回包
  = Binder 线程无法返回线程池,锁也无法释放
  = 其他 Binder 线程继续等待同一把锁或等待下游
  = 服务端 Binder Thread Pool 耗尽
  = 上游没有 BR_REPLY,最终卡顿或 ANR

最危险的代码形态:

java 复制代码
synchronized (mLock) {
    return mVhalProxy.someSyncMethod();
}

风险来自三个条件叠加:

text 复制代码
持锁 + 跨进程 + 同步等待

2. 参与角色

角色 典型进程 本案例中的职责
上游 Client SystemUI / Cluster / CarSettings 同步调用 carserver 的某个 Binder 接口,等待 reply
中间 Server carserver 或 system_server 接收上游请求;处理过程中同步调用 VHAL
下游 Server VHAL 进程或其他服务进程 接收 carserver 的下游同步 Binder 请求并返回 reply
Kernel Binder Driver Kernel 投递事务、调度 Binder 线程、维护事务队列

一个进程可以同时具有 Client 和 Server 两种角色:

text 复制代码
对 SystemUI 请求:
  carserver 是 Server

对 VHAL 请求:
  carserver 是 Client

因此同一个 carserver Binder Thread 可以先接收请求,随后在处理该请求时发起新的同步 Binder 调用。


3. 完整时序图

sequenceDiagram participant UI as SystemUI / Cluster participant K as Kernel Binder Driver participant Car as carserver Binder Thread T1 participant VHAL as VHAL / 下游服务 UI->>K: BC_TRANSACTION(同步调用 carserver) K->>Car: BR_TRANSACTION Car->>Car: IPCThreadState::executeCommand(BR_TRANSACTION) Car->>Car: ICarXXXService.Stub.onTransact() Car->>Car: CarService.xxx() Car->>Car: 获得 mLock Car->>K: BC_TRANSACTION(同步调用 VHAL) K->>VHAL: BR_TRANSACTION Car->>Car: IPCThreadState::waitForResponse() Note over Car: T1 持有 mLock 并等待 VHAL reply UI->>K: 新请求 BC_TRANSACTION K->>Car: BR_TRANSACTION(分配给 T2) Car->>Car: T2 尝试获得 mLock Note over Car: T2 阻塞等待 T1 释放 mLock UI->>K: 更多请求 BC_TRANSACTION K->>Car: Binder todo 队列持续堆积 Note over Car: 所有 Binder Thread 均等待锁或下游 reply Note over UI,Car: 无可用 Binder Thread,新的请求无法执行 UI->>UI: 同步等待 BR_REPLY Note over UI: UI 卡顿 / Input timeout / ANR

4. 上游:SystemUI 同步调用 carserver

text 复制代码
SystemUI / Cluster / CarSettings 进程

  ICarXXXService.Stub.Proxy.xxx()
    → mRemote.transact(..., flags = 0)
    → BinderProxy.transact()
    → JNI android_os_BinderProxy_transact()
    → BpBinder::transact()
    → IPCThreadState::transact()        ← 上游调用线程执行
    → writeTransactionData(BC_TRANSACTION)
    → waitForResponse()
      → talkWithDriver()
      → ioctl(BINDER_WRITE_READ)
    → Kernel

本地源码中,同步调用等待 reply 的位置:

cpp 复制代码
// frameworks/native/libs/binder/IPCThreadState.cpp:868-942
status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    flags |= TF_ACCEPT_FDS;

    err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);
    if (err != NO_ERROR) {
        if (reply) reply->setError(err);
        return (mLastError = err);
    }

    if ((flags & TF_ONE_WAY) == 0) {
        if (reply) {
            err = waitForResponse(reply); // 同步调用阻塞等待 BR_REPLY
        } else {
            Parcel fakeReply;
            err = waitForResponse(&fakeReply);
        }
    } else {
        err = waitForResponse(nullptr, nullptr);
    }

    return err;
}

这里的 IPCThreadState::transact() 运行在 SystemUI 发起调用的线程中。这个线程可能是主线程、HandlerThread 或其他业务线程;如果是主线程,carserver 长时间不返回会直接造成界面卡顿风险。

4.1 上游同步调用:等待发生在哪里

上游看到的典型调用链是:

text 复制代码
SystemUI 调用线程

  ICarXXXService.Stub.Proxy.xxx()
  → mRemote.transact(..., flags = 0)
  → BinderProxy.transact()
  → BpBinder::transact()
  → IPCThreadState::transact()
  → writeTransactionData(BC_TRANSACTION)
  → waitForResponse()                    ← 逻辑上的同步等待入口
    → talkWithDriver()
      → ioctl(BINDER_WRITE_READ)         ← 常见的实际内核睡眠位置
  → 等待 BR_REPLY

同步调用链中 waitForResponse() 表示等待已经开始,但应区分两个层次:

层次 位置 含义
逻辑等待点 IPCThreadState::waitForResponse() 当前同步 Binder 调用尚未收到远端 reply
实际可能阻塞点 talkWithDriver()ioctl(BINDER_WRITE_READ) 线程进入 Kernel,等待 Binder Driver 返回 BR_REPLY 或其他 BR_* 命令
text 复制代码
SystemUI 卡在 waitForResponse / ioctl
  不等于 SystemUI 自己有问题

它只证明:
  SystemUI 还没有收到 carserver 返回的 BR_REPLY

carserver 能否返回 BR_REPLY,取决于服务端请求处理是否开始、是否完成,以及是否最终走到 sendReply()


5. Kernel:投递到 carserver Binder 线程池

text 复制代码
Kernel Binder Driver

  binder_ioctl_write_read()
    → binder_thread_write()
    → 读取 BC_TRANSACTION
    → binder_transaction()
    → 根据 target.handle 找到 carserver 的目标 Binder node
    → target_proc = carserver / system_server
    → binder_proc_transaction()
    → transaction 入队到 Binder Thread todo 或进程 todo
    → binder_wakeup_thread_ilocked()
    → 唤醒空闲 Binder Thread

核心投递逻辑:

c 复制代码
// kernel/common/drivers/android/binder.c:3025-3099
static int binder_proc_transaction(struct binder_transaction *t,
                                   struct binder_proc *proc,
                                   struct binder_thread *thread)
{
    // ...
    if (!thread && !pending_async)
        thread = binder_select_thread_ilocked(proc);

    if (thread) {
        binder_transaction_priority(thread, t, node);
        binder_enqueue_thread_work_ilocked(thread, &t->work);
    } else if (!pending_async) {
        binder_enqueue_work_ilocked(&t->work, &proc->todo);
    }

    if (!pending_async)
        binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */);

    proc->outstanding_txns++;
}

Binder 线程池是进程级资源,不是某个独立 AIDL 服务专用资源。若 CarService 与其他服务同处 system_server,CarService 的线程阻塞可能影响 system_server 中其他 Binder 服务的响应能力。

32 / 16 也不是 Android 固定值;实际可用线程数受 BINDER_SET_MAX_THREADS、主动加入线程池的线程数、Driver 请求创建线程的时机和线程阻塞状态共同影响。


6. carserver:Binder Thread 作为 Server 接收请求

text 复制代码
carserver / system_server 进程

  Binder Thread T1

    IPCThreadState::joinThreadPool()
      → getAndExecuteCommand()
      → talkWithDriver()
      → ioctl(BINDER_WRITE_READ)
      ← Kernel 返回 BR_TRANSACTION
      → executeCommand(BR_TRANSACTION)
      → BBinder::transact()
      → JavaBBinder::onTransact()
      → Binder.execTransact()
      → ICarXXXService.Stub.onTransact()
      → CarService.xxx()

服务端线程池循环:

cpp 复制代码
// frameworks/native/libs/binder/IPCThreadState.cpp:788-804
void IPCThreadState::joinThreadPool(bool isMain)
{
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
    mIsLooper = true;

    do {
        processPendingDerefs();
        result = getAndExecuteCommand();
    } while (result != -ECONNREFUSED && result != -EBADF);
}
cpp 复制代码
// frameworks/native/libs/binder/IPCThreadState.cpp:687-710
status_t IPCThreadState::getAndExecuteCommand()
{
    status_t result;
    int32_t cmd;

    result = talkWithDriver();
    if (result >= NO_ERROR) {
        cmd = mIn.readInt32();
        size_t newThreadsCount = mProcess->mExecutingThreadsCount.fetch_add(1) + 1;
        result = executeCommand(cmd);
        mProcess->mExecutingThreadsCount.fetch_sub(1);
    }
    return result;
}

收到 BR_TRANSACTION 后,Native Binder 调用 Java 服务端:

cpp 复制代码
// frameworks/native/libs/binder/IPCThreadState.cpp:1421-1491
case BR_TRANSACTION:
{
    binder_transaction_data tr;
    // ... 从 mIn 读取 tr ...

    Parcel buffer;
    buffer.ipcSetDataReference(
            reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
            tr.data_size,
            reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
            tr.offsets_size / sizeof(binder_size_t), freeBuffer);

    if (tr.target.ptr) {
        if (reinterpret_cast<RefBase::weakref_type*>(tr.target.ptr)->attemptIncStrong(this)) {
            error = reinterpret_cast<BBinder*>(tr.cookie)->transact(
                    tr.code, buffer, &reply, tr.flags);
            reinterpret_cast<BBinder*>(tr.cookie)->decStrong(this);
        }
    }
}

此时 T1 的角色是:

text 复制代码
Server
  → 正在处理 SystemUI / Cluster → carserver 的入站 Binder 请求

7. 风险点:同一个 Binder Thread 持锁同步调用 VHAL

假设业务代码形态如下:

java 复制代码
void xxx() {
    synchronized (mLock) {
        return mVhalProxy.someSyncMethod();
    }
}

此时 T1 的完整栈会变成:

text 复制代码
carserver Binder Thread T1

  IPCThreadState::executeCommand(BR_TRANSACTION)
  → ICarXXXService.Stub.onTransact()
  → CarService.xxx()
  → synchronized (mLock)              ← T1 获得 mLock
  → mVhalProxy.someSyncMethod()
  → BinderProxy.transact()
  → JNI android_os_BinderProxy_transact()
  → BpBinder::transact()
  → IPCThreadState::transact()        ← 同一个 T1 再次执行
  → writeTransactionData(BC_TRANSACTION)
  → waitForResponse()                 ← 等 VHAL 回复

此刻同一个线程发生角色切换:

text 复制代码
前半段:T1 是 carserver 的 Server Binder Thread
  接收并处理 SystemUI 发来的请求

后半段:T1 是 VHAL 的 Client Thread
  向 VHAL 发起同步 Binder 调用,等待 BR_REPLY

这和 IPCThreadState.cpp 同时存在 Client / Server 路径不矛盾:它是每个使用 Native Binder 的进程都会加载的用户态传输层,同一个线程可以先处理 BR_TRANSACTION,再发送 BC_TRANSACTION,随后等待 BR_REPLY

7.1 AIDL 自动生成的 onTransact:为什么仍然会阻塞

第二张图中的代码通常来自 AIDL 自动生成的 Stub.onTransact()。它负责:

text 复制代码
读取 Parcel 参数
  → 调用真正的服务实现
  → 服务实现正常返回后写 reply
  → Native 层 sendReply
  → Kernel 将 BR_REPLY 返回上游

以当前工作区生成的 IContentService 为例:

java 复制代码
// out/soong/.../gen/aidl/java/IContentService.java:485-500
case TRANSACTION_registerContentObserver:
{
    Uri _arg0 = data.readTypedObject(Uri.CREATOR);
    boolean _arg1 = data.readBoolean();

    IContentObserver _arg2 =
            IContentObserver.Stub.asInterface(data.readStrongBinder());

    int _arg3 = data.readInt();
    int _arg4 = data.readInt();

    this.registerContentObserver(_arg0, _arg1, _arg2, _arg3, _arg4);
    // ↑ 自动生成分发 → 真实服务实现的边界

    reply.writeNoException();
    break;
}

这里的 this.registerContentObserver(...) 虽然写在自动生成文件中,却是一次虚方法分发

text 复制代码
this 的静态类型:IContentService.Stub
this 的运行时类型:ContentService

实际执行的方法:
  ContentService.registerContentObserver(...)

当前源码中的真实实现:

java 复制代码
// frameworks/base/services/core/java/com/android/server/content/ContentService.java:365-400
@Override
public void registerContentObserver(Uri uri, boolean notifyForDescendants,
        IContentObserver observer, int userHandle, int targetSdkVersion) {
    // 参数、用户和权限校验 ...

    synchronized (mRootNode) {
        mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
                uid, pid, userHandle);
    }
}

所以"自动生成"不等于"不会阻塞"。自动生成部分只是一个很薄的分发壳;只要真实实现没有返回,下面的:

java 复制代码
reply.writeNoException();

就不会执行,上游 waitForResponse() 也不会返回。

这段 AIDL 分发链可能对应三种不同情况:

分类 服务端实际状态 上游结果
真实实现等待锁 线程在 synchronized(mRootNode) 或业务锁上 BLOCKED 尚未写 reply,上游继续等
真实实现嵌套同步调用下游 线程在下游 waitForResponse() 尚未写 reply,上游继续等
Binder 线程池耗尽 请求尚未被任何 Binder Thread 执行,AIDL 分发逻辑甚至尚未开始 尚未写 reply,上游继续等

还有一个本案例相关的特殊失败点:data.readStrongBinder() 也可能在真实服务实现之前失败。

text 复制代码
data.readStrongBinder()
  → unflattenBinder()
  → getStrongProxyForHandle()
  → javaObjectForIBinder()
  → BinderProxy.getInstance()
  → sProxyMap.set()
  → BinderProxyMapSizeException

这种情况下,ContentService.registerContentObserver() 根本没有开始执行;它是参数反扁平化阶段的 ProxyMap 泄漏崩溃,不是业务实现等待锁或等待 VHAL。

7.2 最小 Demo:自动分发、持锁同步下游与线程池耗尽

下面的代码是用于说明调用关系的最小 Demo,不是 AOSP 现有 CarService 的具体实现。

AIDL 接口

aidl 复制代码
// ICarDataService.aidl
interface ICarDataService {
    int getVehicleValue();
}

// IVehicleGateway.aidl
interface IVehicleGateway {
    int getValueSync();
}

AIDL 自动生成的服务端分发逻辑(等价结构)

java 复制代码
// ICarDataService.Stub.onTransact() 的等价结构
case TRANSACTION_getVehicleValue: {
    data.enforceInterface(DESCRIPTOR);

    int result = this.getVehicleValue();
    //                  ↑ 虚方法分发到 CarDataService.getVehicleValue()
    //                    未返回前,下面两行不会执行

    reply.writeNoException();
    reply.writeInt(result);
    break;
}

错误示例:持锁同步调用 VHAL

java 复制代码
final class CarDataService extends ICarDataService.Stub {
    private final Object mLock = new Object();
    private final IVehicleGateway mVehicleGateway;

    @Override
    public int getVehicleValue() throws RemoteException {
        synchronized (mLock) {
            // 错误点:Binder Thread 持有 mLock,并同步等待下游 VHAL。
            return mVehicleGateway.getValueSync();
        }
    }

    public void updateCacheFromOtherRequest(int value) {
        synchronized (mLock) {
            // 这个路径与 getVehicleValue() 竞争同一把锁。
        }
    }
}

线程状态演化

text 复制代码
T1:处理 SystemUI 的 getVehicleValue() 请求
  ICarDataService.Stub.onTransact()
  → CarDataService.getVehicleValue()
  → 获得 mLock
  → mVehicleGateway.getValueSync()
  → BinderProxy.transact()
  → IPCThreadState.waitForResponse()
  → VHAL 未回复
  → T1 持锁阻塞

T2:处理另一条 updateCacheFromOtherRequest() 请求
  ICarDataService.Stub.onTransact()
  → CarDataService.updateCacheFromOtherRequest()
  → 尝试获得 mLock
  → BLOCKED,等待 T1

T3、T4 ...:
  → 等同一把 mLock,或者各自同步等待 VHAL
  → 无法返回 Binder Thread Pool

新的 SystemUI 请求:
  → Kernel todo 队列
  → 没有可用 Binder Thread
  → AIDL onTransact 分发逻辑尚未开始
  → 上游继续 waitForResponse()

改进示例:锁外同步调用

java 复制代码
final class CarDataService extends ICarDataService.Stub {
    private final Object mLock = new Object();
    private final IVehicleGateway mVehicleGateway;
    private Request mPendingRequest;

    @Override
    public int getVehicleValue() throws RemoteException {
        final Request request;
        synchronized (mLock) {
            request = mPendingRequest;
        }

        // 锁外等待下游,VHAL 慢不会长期霸占 mLock。
        final int value = mVehicleGateway.getValueSync();

        synchronized (mLock) {
            mPendingRequest = request;
            return value;
        }
    }
}

这个改动不能让 VHAL 自动恢复,也不会让 T1 自动回到线程池;它解决的是锁级联问题:其他 Binder Thread 不再因为同一个 mLock 被 T1 连带阻塞。


8. 下游 VHAL 不回复时的阻塞状态

text 复制代码
Binder Thread T1

  获得 mLock
  → 同步调用 VHAL
  → IPCThreadState::waitForResponse()
  → 等待 VHAL BR_REPLY
  → VHAL 未及时回复
  → T1 持续阻塞

此时:

text 复制代码
T1 持有 mLock
T1 不能处理下一个 carserver Binder 请求
T1 不能返回 Binder 线程池

需要区分两种下游失败:

下游状态 常见结果
VHAL 进程已死亡 Driver 可返回 BR_DEAD_REPLYwaitForResponse() 结束并返回错误
VHAL 进程仍存活,但服务线程死锁、阻塞或硬件长期不返回 没有 BR_REPLY,调用线程持续停在 waitForResponse()

Binder 没有适用于所有业务调用的统一 Java 层自动超时取消机制;具体超时通常由上层业务、HAL 协议、Watchdog 或调用策略处理。


9. 后续请求如何耗尽 Binder 线程池

9.1 情况 A:其他请求也需要同一把 mLock

text 复制代码
Binder Thread T1
  获得 mLock
  → waitForResponse(VHAL)
  → 卡住

Binder Thread T2
  收到新的 SystemUI 请求
  → CarService.yyy()
  → 尝试 synchronized (mLock)
  → BLOCKED,等待 T1 释放 mLock

Binder Thread T3
  收到新的 Cluster 请求
  → CarService.zzz()
  → 尝试 synchronized (mLock)
  → BLOCKED,等待 T1 释放 mLock

T2T3 没有停在 waitForResponse(),但它们也无法完成入站 Binder 调用,因而同样占住 Binder 线程池资源。

9.2 情况 B:多个 Binder Thread 各自同步等待 VHAL

text 复制代码
Binder Thread T1 → waitForResponse(VHAL request #1)
Binder Thread T2 → waitForResponse(VHAL request #2)
Binder Thread T3 → waitForResponse(VHAL request #3)
...
Binder Thread TN → waitForResponse(VHAL request #N)

这种场景不依赖同一把锁,所有 Binder Thread 直接因为同步下游调用而被占住。


10. 耗尽后的最终状态

text 复制代码
carserver Binder Thread Pool

  T1  → waitForResponse(VHAL),持有 mLock
  T2  → BLOCKED,等待 mLock
  T3  → BLOCKED,等待 mLock
  T4  → waitForResponse(VHAL)
  T5  → waitForResponse(VHAL)
  ...
  TN  → BLOCKED 或 waitForResponse(VHAL)

  可用 Binder Thread = 0

新的上游 Binder 请求仍可进入 Kernel:

text 复制代码
SystemUI
  → BC_TRANSACTION
  → Kernel
  → carserver binder_proc.todo / binder_thread.todo

但 Kernel 找不到可用的 carserver Binder Thread 去处理:

text 复制代码
新的 transaction
  → 留在 carserver todo 队列排队
  → 没有可用 Binder Thread 取出并执行
  → 没有业务方法运行
  → 没有 BR_REPLY 返回上游

上游同步调用持续等待:

text 复制代码
SystemUI 主线程
  → waitForResponse()
  → 等 carserver BR_REPLY
  → carserver 无可用 Binder Thread
  → 长时间阻塞
  → Input timeout / ANR

11. 完整因果链

text 复制代码
VHAL 卡住 / 回复慢
  ↓
carserver Binder Thread T1 持 mLock 发起同步 Binder 调用
  ↓
T1 卡在 IPCThreadState::waitForResponse()
  ↓
mLock 长时间不释放
  ↓
其他 carserver Binder Thread:
  - 要么等待 mLock
  - 要么也同步等待 VHAL
  ↓
carserver Binder Thread Pool 耗尽
  ↓
新的 SystemUI / Cluster / CarSettings Binder 请求在 Kernel todo 队列排队
  ↓
上游同步调用没有 BR_REPLY
  ↓
SystemUI 卡顿 / ANR

12. 排查证据

出现这类问题时,优先抓 carserver / system_server 的 Java 线程栈和 native Binder 事务状态。

12.1 Binder Thread 的典型阻塞栈

text 复制代码
Binder:* 线程
  → BinderProxy.transact
  → 某个 VHAL AIDL / 下游服务方法
  → IPCThreadState::waitForResponse

12.2 同一把锁导致的连锁阻塞

text 复制代码
Binder:* 线程 T1
  → 持有 mLock
  → BinderProxy.transact
  → waitForResponse

Binder:* 线程 T2 / T3 / ...
  → BLOCKED
  → waiting to lock <mLock>
  → owner = T1

12.3 Kernel Binder 状态

bash 复制代码
adb shell cat /dev/binderfs/binder_logs/state > binder_state.txt

重点观察:

text 复制代码
- carserver / system_server 的 Binder Thread 是否都处于执行中或等待中
- carserver 的 todo 队列是否出现持续堆积的 pending transaction
- 下游 VHAL 进程是否存在未完成 transaction
- Binder transaction 的 sender / target PID 是否形成等待链

13. 修复原则

避免让锁覆盖未知耗时的同步跨进程调用。

不建议:

java 复制代码
synchronized (mLock) {
    return mVhalProxy.someSyncMethod();
}

建议:

java 复制代码
final Request request;
synchronized (mLock) {
    request = buildRequestLocked();
}

// 锁外执行未知耗时的同步跨进程调用
final Result result = mVhalProxy.someSyncMethod(request);

synchronized (mLock) {
    applyResultLocked(result);
}

这样不能让 VHAL 自动恢复,但可以避免 VHAL 慢调用长期霸占 mLock,防止其他 Binder Thread 因等待同一把锁而被连带耗尽。

若业务允许,还应评估:

text 复制代码
- 是否可将下游调用改为异步或 oneway + 明确回调
- 是否可增加业务级超时与取消策略
- 是否应将慢路径移出 Binder 线程,交由受控工作线程执行
- 是否会发生下游回调 carserver 并再次请求同一把 mLock 的死锁环

14. VHAL 接口类型前提

本文以 AIDL Binder VHAL 为例:

text 复制代码
BinderProxy → BpBinder → IPCThreadState → /dev/binder

若下游仍是旧 HIDL VHAL,用户态对象和设备节点可能变为:

text 复制代码
IHwBinder / HwBinder → /dev/hwbinder

但"持锁同步等待下游 → Binder Thread 不返回线程池 → 线程池耗尽 → 上游同步调用卡住"的因果关系仍然成立。

相关推荐
Zeroplucky37 分钟前
从 registerContentObserver 剖析 Android Binder 流程
前端
Yinlin12438 分钟前
深入悬浮组件实现
前端
yangzheui39 分钟前
nvue页面事件穿透到下层元素解决办法
前端
杉氧40 分钟前
从 Modifier 到 Flexbox:React Native 布局与样式设计哲学
android·前端·react native
比老马还六42 分钟前
Bipes-Blockly项目二次开发/硬件功能-LED灯条(十一)
前端·嵌入式硬件·硬件工程
circuitsosk1 小时前
智能体任务拆解与执行:基于ReAct+Plan-and-Execute框架的行业Skill构建实录
前端·javascript·python·react.js·react·llm agent·智能体编排
daols881 小时前
vue 表格vxe-table 实现紧凑型表格的方式
前端·javascript·vue.js
恋猫de小郭1 小时前
Flutter 多窗口支持类型和 API 介绍
android·前端·flutter