【车载audio】【AudioPolicyManager 01】【AudioPolicyClient 类介绍】

1. 背景

在 Android 音频架构中,mpClientInterface 是一个至关重要的"桥梁"指针。如果说 AudioPolicyManager (APM) 是音频系统的"大脑"(负责制定路由、音量和策略决策),那么 mpClientInterface 就是大脑的手臂,负责执行具体的硬件操作。

以下是结合源码的详细解析:


  1. 它是什么?(定义)

mpClientInterface 是 AudioPolicyClientInterface 类型的指针。

  • 接口定义:位于 frameworks/av/services/audiopolicy/common/include/AudioPolicyInterface.h(或相似路径)。
  • 具体实现:AudioPolicyService (APS) 实现了这个接口。
  • 持有者:AudioPolicyManager。

设计模式:这是一个典型的委派模式(Delegate)。APM 不直接操作硬件,而是通过这个接口要求外部环境(APS)去操作。


  1. 它是如何初始化的?(源码追踪)

mpClientInterface 的初始化发生在 AudioPolicyManager 创建的那一刻。

第一步:AudioPolicyService 启动

在 AudioPolicyService.cpp 的构造函数或 onFirstRef 中,会创建 APM:

c 复制代码
 // 源码位置:AudioPolicyService.cpp
 void AudioPolicyService::onFirstRef() {
     // ...
     // 这里创建了 APM,并传入了 this
     // 因为 AudioPolicyService 继承了 AudioPolicyClientInterface
     mAudioPolicyManager = createAudioPolicyManager(this);
 }

第二步:AudioPolicyManager 构造

在 AudioPolicyManager.cpp 中,构造函数接收这个指针并赋值:

c 复制代码
// -------- clientInterface 赋值流程

// services/audiopolicy/service/AudioPolicyService.cpp
AudioPolicyService::AudioPolicyService()
    : BnAudioPolicyService(),
      mAudioPolicyManager(NULL),
      mAudioPolicyClient(NULL),
      mPhoneState(AUDIO_MODE_INVALID),
      mCaptureStateNotifier(false),
      mCreateAudioPolicyManager(createAudioPolicyManager), // 这里将函数指针赋值给了 mCreateAudioPolicyManager 变量, 可以通过 mCreateAudioPolicyManager 调用到 createAudioPolicyManager 函数
      mDestroyAudioPolicyManager(destroyAudioPolicyManager) {
      setMinSchedulerPolicy(SCHED_NORMAL, ANDROID_PRIORITY_AUDIO);
}

// AudioPolicyService 第一次被引用时将调用 
void AudioPolicyService::onFirstRef()
{
	mAudioPolicyClient = new AudioPolicyClient(this);
	mAudioPolicyManager = mCreateAudioPolicyManager(mAudioPolicyClient); // 这里将触发 createAudioPolicyManager 的调用

}

// ------- 也就是说 mAudioPolicyClient 就是 clientInterface  == AudioPolicyClient 类



static AudioPolicyInterface* createAudioPolicyManager(AudioPolicyClientInterface *clientInterface /*这里*/)
{
    AudioPolicyManager *apm = new AudioPolicyManager(clientInterface/*这里*/);
    status_t status = apm->initialize();
    if (status != NO_ERROR) {
        delete apm;
        apm = nullptr;
    }
    return apm;
}




// 源码位置:AudioPolicyManager.cpp
AudioPolicyManager::AudioPolicyManager(AudioPolicyClientInterface *clientInterface/*这里*/)
    : mUidCached(AID_AUDIOSERVER),
      mpClientInterface(clientInterface), // <--- 在这里完成了初始化赋值
      mConfig(mHwModulesAll, mOutputDevicesAll, mInputDevicesAll, mDefaultOutputDevice)
{
    // ...
}

  1. 它能做什么?(核心职责)

APM 只负责"逻辑计算",所有的"物理动作"都通过 mpClientInterface 呼叫 AudioPolicyService,最终由 APS 调用 AudioFlinger 来实现。

它的核心方法包括但不限于:

A. 加载硬件模块 (loadHwModule)

正如上面看到的 onNewAudioModulesAvailableInt:

c 复制代码
    // APM 决定加载某个模块
    hwModule->setHandle(mpClientInterface->loadHwModule(hwModule->getName()));

背后逻辑:mpClientInterface (即 APS) 会通知 AudioFlinger 去 dlopen 对应的 HAL 库。

B. 打开/关闭音频流 (openOutput / openInput)

c 复制代码
    // APM 探测硬件时
    status = mpClientInterface->openOutput(moduleHandle, &output, ...);

背后逻辑:APS 转发请求给 AudioFlinger,AF 创建 PlaybackThread 并开启硬件输出流。

C. 设置参数 (setParameters)

c 复制代码
    // APM 需要向下层传递路由或音效参数
    mpClientInterface->setParameters(output, "routing=2");

D. 通知上层状态变化 (onAudioPortListUpdate)

c 复制代码
    // 当硬件插拔,APM 更新完设备列表后
    mpClientInterface->onAudioPortListUpdate();

背后逻辑:APS 会通过 Binder 回调 Java 层的 AudioService,最终触发 App 侧的 OnAudioDeviceConnectionStateChanged。


  1. 为什么要这样设计?(设计动机)

  2. 解耦决策与执行:

    • AudioPolicyManager 是纯逻辑类,它不需要知道 Binder 怎么通信,也不需要知道 AudioFlinger 是如何管理线程的。
    • 它只需要调用 mpClientInterface 的方法,就像调用本地函数一样。
  3. 易于测试:

    • 在进行单元测试时,可以方便地 mock 一个 AudioPolicyClientInterface 传给 APM,从而在不启动整个 Android 音频系统的情况下测试策略逻辑。
  4. 单向依赖:

    • APS 持有 APM 的强引用,APM 持有 APS 的抽象接口指针。这避免了循环引用,且符合依赖倒置原则。

  1. 总结

在 AudioPolicyManager 的源码中,每当你看到 mpClientInterface->xxxx() 时,你可以将其翻译为:

"大脑(APM)呼叫手臂(APS):我已经计算好了,现在请你去让底层硬件执行这个动作。"

它是音频系统从策略层(Policy)跨越到服务层(Service/Flinger)的关键节点。没有它,APM 计算出的任何路由决策都无法真正变成物理声音。

2. AudioPolicyClient 介绍

这个类在车载 Audio AOSP 里其实地位挺关键的,但名字又特别容易让人误会

1. 一句话定位(先有整体感)

AudioPolicyClient 是 AudioPolicyManager 的"执行手",负责把「策略层的决定」真正落地到 AudioFlinger / Audio HAL。

在车机里:
谁该出声、走哪个设备、音量多大、要不要切换、要不要建 patch

👉 决策在 AudioPolicyManager

👉 执行靠 AudioPolicyClient

2. 角色关系(车载场景)

角色 类比 作用
AudioPolicyManager 大脑 / 调度中心 决定路由、音区、策略
AudioPolicyClient 手 + 翻译官 把策略变成真正的 HAL / Flinger 操作
AudioFlinger 混音工厂 管理线程、Track、Mixer
Audio HAL 硬件司机 真正和声卡 / DSP 打交道

3. AudioPolicyClient 在做什么(核心职责表)

1. 硬件模块管理(Audio HAL 级)

方法 干什么 车载实际意义
loadHwModule() 加载 Audio HAL 模块 加载 primary / usb / a2dp / remote submix 等
setDeviceConnectedState() 通知设备上下线 插 U 盘 / 蓝牙断连 / HDMI 接入
getAudioPort() 查询 audio port 能力 构建音区 / patch 路由的基础

2. 音频输出控制(播放链路)

方法 作用 车载场景举例
openOutput() 打开一个输出流 媒体 → 扬声器 / 导航 → 仪表
closeOutput() 关闭输出 切换音区 / 模式退出
openDuplicateOutput() 创建"复制输出" 媒体同时给前排+后排
suspendOutput() 暂停但不销毁 电话打断音乐
restoreOutput() 恢复暂停输出 电话挂断后继续音乐

车载多音区、多路并发,基本都绕不开这些接口

3. 音频输入控制(采集链路)

方法 作用 车载场景
openInput() 打开输入流 语音助手 / 蓝牙通话 MIC
closeInput() 关闭输入 语音结束
onRecordingConfigurationUpdate() 录音配置变化通知 麦克风切换、效果变更

4. 音量与策略执行

方法 作用 说明
setStreamVolume() 设置某流在某 output 上的音量 同一音乐在不同音区音量不同
setVoiceVolume() 通话音量 通话是特殊链路
invalidateStream() 强制重路由 设备变化 / 策略变化
onAudioVolumeGroupChanged() 音量组变化回调 CarAudioService 常用

5. 参数与厂商扩展

方法 作用 为什么重要
setParameters() 向 HAL 发送 KV 参数 DSP 场景切换、EQ、ANC
getParameters() 从 HAL 读取参数 查询硬件状态
很多车厂的"黑科技"都藏在这里

6. Effect / Patch / 动态路由(车载核心)

方法 作用 车载意义
moveEffects() effect 在输出间迁移 切音区不重建效果
setEffectSuspended() 暂停效果 通话 / 紧急广播
createAudioPatch() 创建 audio patch 音区路由、直通 DSP
releaseAudioPatch() 释放 patch 路由恢复
setAudioPortConfig() 修改端口配置 动态设备配置
onRoutingUpdated() 路由变化通知 上层同步状态

7. ID / 状态管理(支撑系统)

方法 作用
newAudioUniqueId() 生成唯一 ID(session / effect 等)
setSoundTriggerCaptureState() 声学唤醒与录音协调
updateSecondaryOutputs() Track → 多输出绑定

4. 在车载 Audio 架构中的一句总结

AudioPolicyClient = AudioPolicyService 的"对外执行接口"

  • 不做决策

  • 不算策略

  • 只负责:
    把"策略层想好的事情"准确、完整、按时地执行到 AudioFlinger / HAL

3. 案例分析

1. mpClientInterface->loadHwModule 深度探索

这里将调用到 AudioPolicyClient 中的 loadHwModule

// services/audiopolicy/service/AudioPolicyClientImpl.cpp

c 复制代码
audio_module_handle_t AudioPolicyService::AudioPolicyClient::loadHwModule(const char *name)
{
    sp<IAudioFlinger> af = AudioSystem::get_audio_flinger(); // 获取 AudioFlinger 服务
    if (af == 0) {
        ALOGW("%s: could not get AudioFlinger", __func__);
        return AUDIO_MODULE_HANDLE_NONE;
    }

    return af->loadHwModule(name); // <-------- 重点
}
c 复制代码
audio_module_handle_t AudioFlinger::loadHwModule(const char *name)
{
    if (name == NULL) {
        return AUDIO_MODULE_HANDLE_NONE;
    }
    if (!settingsAllowed()) {
        return AUDIO_MODULE_HANDLE_NONE;
    }
    Mutex::Autolock _l(mLock);
    AutoMutex lock(mHardwareLock);
    return loadHwModule_l(name);
}
c 复制代码
// 函数名带 _l,表示调用者必须已经持有锁
// 要求:
//  - AudioFlinger::mLock:保护 AudioFlinger 的整体状态(输出、线程、设备)
//  - AudioFlinger::mHardwareLock:保护 HAL 访问,防止并发初始化硬件
//
// 背景(车载非常重要):
// 车载 Audio 设备多、启动慢、HAL 状态复杂,
// 必须保证 HAL 加载、初始化、参数交互是"串行且可控"的
audio_module_handle_t AudioFlinger::loadHwModule_l(const char *name)
{
    // ===== 1. 是否已经加载过该 HAL 模块 =====
    // mAudioHwDevs:已加载的 AudioHwDevice 列表(key = module handle)
    // 防止重复加载同一个 HAL(例如 primary、usb、a2dp)
    for (size_t i = 0; i < mAudioHwDevs.size(); i++) {
        // moduleName() 是 HAL 注册时的名字
        // 使用 strncmp 而不是 strcmp,允许名字带后缀
        if (strncmp(mAudioHwDevs.valueAt(i)->moduleName(), name, strlen(name)) == 0) {
            // 已加载则直接返回已有 handle
            ALOGW("loadHwModule() module %s already loaded", name);
            return mAudioHwDevs.keyAt(i);
        }
    }

    // ===== 2. 通过 DevicesFactory 打开 HAL 设备 =====
    // DeviceHalInterface:AudioFlinger 与 HAL 之间的抽象接口
    // 不直接依赖具体 HIDL / AIDL 实现
    sp<DeviceHalInterface> dev;

    // 真正加载 HAL 的地方:
    // - 查找 audio.<name>.so
    // - 打开 hw_device_t
    // - 包装成 DeviceHalInterface
    int rc = mDevicesFactoryHal->openDevice(name, &dev);
    if (rc) {
        // 打开失败:HAL 不存在 / 初始化失败 / 权限问题
        ALOGE("loadHwModule() error %d loading module %s", rc, name);
        return AUDIO_MODULE_HANDLE_NONE;
    }

    // ===== 3. HAL 初始化自检(initCheck) =====
    // mHardwareStatus 主要用于:
    // - debug
    // - dumpsys
    // - 在异常时知道"卡在哪一步"
    mHardwareStatus = AUDIO_HW_INIT;
    rc = dev->initCheck();
    mHardwareStatus = AUDIO_HW_IDLE;

    if (rc) {
        // HAL 打开了,但内部初始化失败
        // 例如:DSP 没起来、声卡节点异常
        ALOGE("loadHwModule() init check error %d for module %s", rc, name);
        return AUDIO_MODULE_HANDLE_NONE;
    }

    // ===== 4. 查询 HAL 对"主音量 / 主静音"的支持能力 =====
    //
    // 设计背景:
    // - Android 支持"系统级 master volume / master mute"
    // - 但并不是所有 HAL 都支持
    // - AudioFlinger 需要在启动时缓存 HAL 的能力
    //
    // 车载场景:
    // - 有的车厂把 master volume 放在 DSP
    // - 有的完全不支持,由上层自己算
    AudioHwDevice::Flags flags = static_cast<AudioHwDevice::Flags>(0);

    // 只有"第一个加载的 HAL"才负责初始化系统主音量状态
    // 通常是 primary HAL
    if (0 == mAudioHwDevs.size()) {
        // 尝试从 HAL 读取"当前主音量"
        mHardwareStatus = AUDIO_HW_GET_MASTER_VOLUME;
        float mv;
        if (OK == dev->getMasterVolume(&mv)) {
            // 如果 HAL 支持,则用 HAL 的值作为系统初始值
            mMasterVolume = mv;
        }

        // 尝试从 HAL 读取"当前主静音状态"
        mHardwareStatus = AUDIO_HW_GET_MASTER_MUTE;
        bool mm;
        if (OK == dev->getMasterMute(&mm)) {
            mMasterMute = mm;
        }
    }

    // ===== 5. 探测 HAL 是否支持"设置 master volume" =====
    // 通过"尝试 set,看返回值"来判断能力(非常 AOSP 风格)
    mHardwareStatus = AUDIO_HW_SET_MASTER_VOLUME;
    if (OK == dev->setMasterVolume(mMasterVolume)) {
        flags = static_cast<AudioHwDevice::Flags>(
                flags | AudioHwDevice::AHWD_CAN_SET_MASTER_VOLUME);
    }

    // ===== 6. 探测 HAL 是否支持"设置 master mute" =====
    mHardwareStatus = AUDIO_HW_SET_MASTER_MUTE;
    if (OK == dev->setMasterMute(mMasterMute)) {
        flags = static_cast<AudioHwDevice::Flags>(
                flags | AudioHwDevice::AHWD_CAN_SET_MASTER_MUTE);
    }

    mHardwareStatus = AUDIO_HW_IDLE;

    // ===== 7. MSD(Mix Stream Decoder)模块的特殊处理 =====
    // MSD 是一个"插入式 HAL"
    // 用来在 AudioFlinger 内部混合 encoded stream(例如 offload / 厂商扩展)
    if (strcmp(name, AUDIO_HARDWARE_MODULE_ID_MSD) == 0) {
        // 标记为 insert 型 HAL
        flags = static_cast<AudioHwDevice::Flags>(
                flags | AudioHwDevice::AHWD_IS_INSERT);
    }

    // ===== 8. 为该 HAL 分配唯一 module handle =====
    // AudioFlinger 内部所有模块 / output / input 都用 unique id 管理
    audio_module_handle_t handle =
            (audio_module_handle_t) nextUniqueId(AUDIO_UNIQUE_ID_USE_MODULE);

    // 创建 AudioHwDevice 对象:
    // - 封装 HAL
    // - 记录能力 flags
    // - 作为 AudioFlinger 管理硬件的核心对象
    AudioHwDevice *audioDevice = new AudioHwDevice(handle, name, dev, flags);

    // ===== 9. primary HAL 的特殊处理 =====
    if (strcmp(name, AUDIO_HARDWARE_MODULE_ID_PRIMARY) == 0) {
        // primary 是"系统主输出设备"
        mPrimaryHardwareDev = audioDevice;

        // 将当前系统 audio mode(NORMAL / IN_CALL / IN_COMMUNICATION)
        // 同步给 HAL
        mHardwareStatus = AUDIO_HW_SET_MODE;
        mPrimaryHardwareDev->hwDevice()->setMode(mMode);
        mHardwareStatus = AUDIO_HW_IDLE;
    }

    // ===== 10. AAudio 相关能力查询(新 HAL 才支持) =====
    //
    // 背景:
    // - AAudio 需要知道硬件 buffer / burst 能力
    // - 用于低延迟音频
    //
    // 车载意义:
    // - 语音助手
    // - 提示音
    // - 实时反馈音效
    if (mDevicesFactoryHal->getHalVersion() > MAX_AAUDIO_PROPERTY_DEVICE_HAL_VERSION) {
        // 查询 HAL 推荐的 mixer burst 数
        if (int32_t mixerBursts = dev->getAAudioMixerBurstCount();
            mixerBursts > 0 && mixerBursts > mAAudioBurstsPerBuffer) {
            mAAudioBurstsPerBuffer = mixerBursts;
        }

        // 查询硬件最小 burst 时间(微秒)
        if (int32_t hwBurstMinMicros = dev->getAAudioHardwareBurstMinUsec();
            hwBurstMinMicros > 0
            && (hwBurstMinMicros < mAAudioHwBurstMinMicros || mAAudioHwBurstMinMicros == 0)) {
            mAAudioHwBurstMinMicros = hwBurstMinMicros;
        }
    }

    // ===== 11. 将 HAL 设备加入 AudioFlinger 管理列表 =====
    mAudioHwDevs.add(handle, audioDevice);

    ALOGI("loadHwModule() Loaded %s audio interface, handle %d", name, handle);

    // 返回 module handle,供 AudioPolicy / AudioFlinger 后续使用
    return handle;
}

这里我们在重点看一下 mDevicesFactoryHal->openDevice(name, &dev);

1. mDevicesFactoryHal->openDevice(name, &dev)

c 复制代码
status_t DevicesFactoryHalHidl::openDevice(
        const char *name,
        sp<DeviceHalInterface> *device) {

    // ===== 1️ 获取当前系统中已注册的 audio HAL 工厂列表 =====
    //
    // 背景:
    // - 在 HIDL 架构下,audio HAL 是通过 IDevicesFactory 暴露的
    // - 一个系统中可能存在多个 factory(vendor / default / passthrough)
    // - factory 可能是 lazy service,需要在第一次使用时才真正拉起
    //
    // copyDeviceFactories():
    // - 返回当前可用 factory 的"快照"
    // - 避免遍历过程中 factory 列表发生变化
    auto factories = copyDeviceFactories();

    // 如果一个 factory 都没有:
    // - 说明 audio HAL 服务还没启动
    // - 或者 vendor audio HAL 根本没注册
    if (factories.empty()) return NO_INIT;

    // ===== 2️ 将 HAL 模块名转换为 HIDL 可识别的 device id =====
    //
    // name 示例:
    // - "primary"
    // - "usb"
    // - "a2dp"
    //
    // idFromHal() 的作用:
    // - 校验 name 是否是合法的 audio HAL 模块名
    // - 将字符串映射为 HIDL enum / 整型 ID
    // - 如果 name 非法,直接返回错误
    status_t status;
    auto hidlId = idFromHal(name, &status);

    // 如果 name 本身非法(例如拼错 / 未定义)
    if (status != OK) return status;

    // ===== 3️ 初始化 HAL 返回结果 =====
    //
    // Result 是 audio HAL HIDL 定义的返回码:
    // - OK                :设备存在且初始化成功
    // - NOT_INITIALIZED   :设备存在,但初始化失败
    // - INVALID_ARGUMENTS :参数错误
    // - NOT_SUPPORTED     :不支持该设备
    //
    // 这里初始化为 NOT_INITIALIZED,
    // 用于后续区分"找到但失败"和"根本没找到"
    Result retval = Result::NOT_INITIALIZED;

    // ===== 4️ 遍历所有 factory,尝试打开目标 audio device =====
    //
    // 设计原因:
    // - 不同 factory 可能支持不同类型的 device
    // - 不能假设第一个 factory 一定支持 primary / usb
    for (const auto& factory : factories) {

        // HIDL 调用的返回对象
        // ret.isOk() 代表 binder 事务是否成功
        Return<void> ret;

        // ===== 5️ primary HAL 的特殊处理 =====
        //
        // 背景(非常关键):
        // - primary HAL 实际上返回的是 IPrimaryDevice
        // - 但在 HIDL 7.1 中:
        //   ❌ IDevice 不能安全 cast 回 IPrimaryDevice
        // - 因此 primary 必须走"专用接口"
        if (strcmp(name, AUDIO_HARDWARE_MODULE_ID_PRIMARY) == 0) {

            // V7.1 的兼容分支
#if MAJOR_VERSION == 7 && MINOR_VERSION == 1
            ret = factory->openPrimaryDevice_7_1(
#else
            // 正常路径:直接打开 primary device
            ret = factory->openPrimaryDevice(
#endif
                // ===== HIDL 回调 =====
                //
                // r      :HAL 返回的 Result
                // result :HAL 返回的 IPrimaryDevice Binder 对象
                [&](Result r,
                    const sp<::android::hardware::audio::CPP_VERSION::IPrimaryDevice>& result) {

                    // 保存 HAL 的返回结果
                    retval = r;

                    // 只有 HAL 成功初始化,才包装成 DeviceHalHidl
                    if (retval == Result::OK) {
                        // DeviceHalHidl 是:
                        // - AudioFlinger 使用的统一 HAL 抽象
                        // - 屏蔽 HIDL / AIDL 差异
                        *device = new DeviceHalHidl(result);
                    }
                });
        } else {
            // ===== 6️ 非 primary HAL(usb / a2dp / remote / etc.)=====
#if MAJOR_VERSION == 7 && MINOR_VERSION == 1
            ret = factory->openDevice_7_1(
#else
            ret = factory->openDevice( // <---------------- 这里是重点, 这里会直接调到 audio hal 中
#endif
                // hidlId 是通过 name 映射得到的 device id
                hidlId,

                // ===== HIDL 回调 =====
                [&](Result r,
                    const sp<::android::hardware::audio::CPP_VERSION::IDevice>& result) {

                    // 保存 HAL 返回状态
                    retval = r;

                    // 成功时,包装为统一 DeviceHalInterface
                    if (retval == Result::OK) {
                        *device = new DeviceHalHidl(result);
                    }
                });
        }

        // ===== 7️ 检查 HIDL binder 调用是否成功 =====
        //
        // ret.isOk() == false:
        // - binder 通信失败
        // - HAL 服务 crash / 被杀
        // - 极端情况下 system_server ↔ HAL 通道异常
        if (!ret.isOk()) return FAILED_TRANSACTION;

        // ===== 8️ 根据 HAL 返回结果决定是否结束 =====
        switch (retval) {

            // ✅ 找到设备,且初始化成功
            // 这是"理想路径"
            case Result::OK:
                return OK;

            // ⚠️ 找到设备,但初始化失败
            // 例如:
            // - DSP 未就绪
            // - 声卡节点不存在
            // - vendor HAL 内部错误
            //
            // 注意:
            // - 这里直接返回 NO_INIT
            // - 不再尝试其他 factory
            case Result::NOT_INITIALIZED:
                return NO_INIT;

            // 其他情况:
            // - NOT_SUPPORTED
            // - INVALID_ARGUMENTS
            //
            // 表示:
            // - 当前 factory 不支持该 device
            // - 继续尝试下一个 factory
            default:
                ;
        }
    }

    // ===== 9️ 所有 factory 都尝试过,但没有一个识别该 name =====
    //
    // 说明:
    // - audio_policy 要求加载的 HAL
    // - vendor 并没有实现
    ALOGW("The specified device name is not recognized: \"%s\"", name);

    // name 本身合法,但没有 HAL 支持
    return BAD_VALUE;
}
相关推荐
三少爷的鞋10 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)14 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码16 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓21 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer