【车载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;
}
相关推荐
敲上瘾2 小时前
磁盘到 inode:深入理解 Linux ext 文件系统底层原理
android·linux·运维·文件系统
hewence12 小时前
Kotlin CoroutineScope解密
android·开发语言·kotlin
遇雪长安3 小时前
高通安卓设备DIAG端口启用指南
android·adb·usb·dm·qpst·diag·qxdm
华章酱3 小时前
MySQL EXPLAIN 完全解读:从执行计划到索引优化
android·数据库·mysql
2501_915921433 小时前
Fastlane 结合 AppUploader 来实现 CI 集成自动化上架
android·运维·ci/cd·小程序·uni-app·自动化·iphone
贤泽3 小时前
Android 15 AOSP Notification分析
android
特立独行的猫a3 小时前
腾讯Kuikly多端框架(KMP)实战:轮播图的完整实现
android·harmonyos·轮播图·jetpack compose·kuikly
2501_915921433 小时前
iOS 抓包怎么绕过 SSL Pinning 证书限制,抓取app上的包
android·网络协议·ios·小程序·uni-app·iphone·ssl
陈健平4 小时前
用 Kimi 2.5 Agent 从 0 搭建「宇宙吞噬,ps:和球球大作战这种差不多」对抗小游戏(Canvas 粒子特效 + AI Bot + 排行榜)
android·人工智能·agent·kimi2.5