【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
1. 背景
我们在 gd_shim_module 介绍章节中,看到 我们将 StorageModule 模块加入到了 modules 中。
// system/main/shim/stack.cc
modules.add<storage::StorageModule>();
在 ModuleRegistry::Start 函数中我们对 加入的所有 module 挨个初始化。
而在该函数中启动一个 module 都要执行那下面几步:
-
创建module 实体
- Module* instance = module->ctor_();
-
将 当前 module 实体和 gd_stack_thread 线程绑定
- set_registry_and_handler(instance, thread);
-
启动当前模块所依赖的所有子模块。
- instance->ListDependencies(&instance->dependencies_);
- Start(&instance->dependencies_, thread);
-
最后调用自己的 Start() 函数
- instance->Start();
-
将module 实体加入到 started_modules_
- started_modules_[module] = instance;
本篇文章就拿 storage::StorageModule 模块来具体分析一下 他的启动。
2. modules.add
我们先来看一下 在调用 modules.add 时, 到底做了那些事情。
modules.add<storage::StorageModule>();
class ModuleList {friend Module;friend ModuleRegistry;public:template <class T>void add() {list_.push_back(&T::Factory); // add 时 添加的是 StorageModule::Factory}private:std::vector<const ModuleFactory*> list_;
};
- 从代码中不难发现, 我们是将 StorageModule::Factory 加入到 list_ 中的。
// system/gd/storage/storage_module.cc
const ModuleFactory StorageModule::Factory = ModuleFactory([]() {return new StorageModule(os::ParameterProvider::ConfigFilePath(), kDefaultConfigSaveDelay, kDefaultTempDeviceCapacity, false, false);
});// 这里在创建 ModuleFactory 对象时, 传入了一个 函数, 这个函数 去 new StorageModule 对象
- 这里在创建 ModuleFactory 对象时, 传入了一个 函数,但是并没有去调用这个函数。
- 这个函数的目的是 去 new StorageModule 对象
class ModuleFactory {friend ModuleRegistry;friend FuzzTestModuleRegistry;public:ModuleFactory(std::function<Module*()> ctor);private:std::function<Module*()> ctor_;
};// system/gd/module.cc
ModuleFactory::ModuleFactory(std::function<Module*()> ctor) : ctor_(ctor) {
}
- 在创建 ModuleFactory 对象时, 也仅仅是将 如下的函数赋值给了 ModuleFactory::ctor_ 函数指针。
[]() {return new StorageModule(os::ParameterProvider::ConfigFilePath(), kDefaultConfigSaveDelay, kDefaultTempDeviceCapacity, false, false);
}
3. 模块具体启动流程
1. 创建module 实体
- 创建module 实体
- Module* instance = module->ctor_();
[]() {return new StorageModule(os::ParameterProvider::ConfigFilePath(), kDefaultConfigSaveDelay, kDefaultTempDeviceCapacity, false, false);
}// 参数讲解// 传入 配置文件加载的路径
std::string ParameterProvider::ConfigFilePath() {{std::lock_guard<std::mutex> lock(parameter_mutex);if (!config_file_path.empty()) {return config_file_path;}}return is_default_bluetooth() ?"/data/misc/bluedroid/bt_config.conf" :"/data/misc/bluedroid/new/bt_config.conf";
}// Save config whenever there is a change, but delay it by this value so that burst config change won't overwhelm disk
/*1. 是配置保存的延迟时间,默认是 3000 毫秒(即 3 秒)2. 配置每次变化都要保存,但通过设置一个延迟时间,避免连续多次修改频繁写盘,造成磁盘负担。3. 例如用户在 1 秒内修改了 10 个配置,系统只会在最后一次修改后延迟 3 秒进行一次集中保存
*/
static const std::chrono::milliseconds kDefaultConfigSaveDelay = std::chrono::milliseconds(3000);/*代表:临时设备或临时数据的最大容纳数量,比如 10000 个设备或条目目的: 控制内存中“临时设备列表”或“缓存队列”的最大容量,防止内存溢出
*/
static const size_t kDefaultTempDeviceCapacity = 10000;
- 这里就会去实际触发 该函数,去创建 StorageModule 对象。
- 也就是说,modules.addstorage::StorageModule();模块对应的 实体其实是 StorageModule 对象。
class StorageModule : public bluetooth::Module {}
- StorageModule 又继承 Module
如下是 StorageModule 的构造函数
// system/gd/storage/storage_module.cc
StorageModule::StorageModule(std::string config_file_path,std::chrono::milliseconds config_save_delay,size_t temp_devices_capacity,bool is_restricted_mode,bool is_single_user_mode): config_file_path_(std::move(config_file_path)),config_save_delay_(config_save_delay),temp_devices_capacity_(temp_devices_capacity),is_restricted_mode_(is_restricted_mode),is_single_user_mode_(is_single_user_mode) {// e.g. "/data/misc/bluedroid/bt_config.conf" to "/data/misc/bluedroid/bt_config.bak"config_backup_path_ = config_file_path_.substr(0, config_file_path_.find_last_of('.')) + ".bak";ASSERT_LOG(config_save_delay > kMinConfigSaveDelay/*这个事 20ms*/,"Config save delay of %lld ms is not enough, must be at least %lld ms to avoid overwhelming the disk",config_save_delay_.count(),kMinConfigSaveDelay.count());
};// The config saving delay must be bigger than this value to avoid overwhelming the disk
// 表示最小允许的配置保存延迟时间:20 毫秒。
// 写入配置文件到磁盘至少需要 10~20ms(取决于是否创建备份),因此不应在更短间隔内频繁触发写盘操作
// 目的:给开发者设置一个“下限”参考,避免误把保存延迟设置得太小
static const std::chrono::milliseconds kMinConfigSaveDelay = std::chrono::milliseconds(20);
这三行常量控制了 配置文件的保存机制,防止因配置频繁变更导致磁盘 I/O 过载:
常量名 | 含义 | 默认值 |
---|---|---|
kDefaultTempDeviceCapacity | 临时设备的最大缓存数量 | 10000 |
kDefaultConfigSaveDelay | 配置变更后的写盘延迟,避免频繁写盘 | 3000 ms |
kMinConfigSaveDelay | 配置保存的最小延迟阈值,低于此值会造成磁盘压力 | 20 ms |
2. 将 当前 module 实体和 gd_stack_thread 线程绑定
- 将 当前 module 实体和 gd_stack_thread 线程绑定
- set_registry_and_handler(instance, thread);
void ModuleRegistry::set_registry_and_handler(Module* instance, Thread* thread) const {instance->registry_ = this;instance->handler_ = new Handler(thread);
}
- 将我们的 gd_stack_thread 对应的 handle 直接保存在 Module->handler_ 中。
Handler* Module::GetHandler() const {ASSERT_LOG(handler_ != nullptr, "Can't get handler when it's not started");return handler_;
}
- 通过 Module::GetHandler() 来获取当前 handler_
3.启动当前模块所依赖的所有子模块
- 启动当前模块所依赖的所有子模块。
- instance->ListDependencies(&instance->dependencies_);
- Start(&instance->dependencies_, thread);
// system/gd/storage/storage_module.ccvoid StorageModule::ListDependencies(ModuleList* list) const {list->add<metrics::CounterMetrics>();
}
-
这里可以看到 StorageModule 模块依赖 CounterMetrics 模块。
-
此时就会去加载 CounterMetrics 模块。
4. 最后调用自己的 Start() 函数
- 最后调用自己的 Start() 函数
- instance->Start();
1. StorageModule::Start
// system/gd/storage/storage_module.ccstatic const std::string kFactoryResetProperty = "persist.bluetooth.factoryreset";void StorageModule::Start() {// 确保整个 Start() 执行期间持有互斥锁,避免多线程同时修改内部状态std::lock_guard<std::recursive_mutex> lock(mutex_);std::string file_source;/*如果系统属性中标记了“恢复出厂设置”1. 删除主配置文件和备份配置文件2. 然后重置该系统属性为 "false"3. 这是在执行软重置或恢复设置时常见的模式*/if (os::GetSystemProperty(kFactoryResetProperty) == "true") {LOG_INFO("%s is true, delete config files", kFactoryResetProperty.c_str());LegacyConfigFile::FromPath(config_file_path_).Delete();LegacyConfigFile::FromPath(config_backup_path_).Delete();os::SetSystemProperty(kFactoryResetProperty, "false");}/*校验配置文件有效性(主 + 备份)1. 调用 is_config_checksum_pass() 检查主文件和备份文件的校验和是否正确2. 若无效,则删除对应配置文件,防止加载损坏数据*/if (!is_config_checksum_pass(kConfigFileComparePass)) {LegacyConfigFile::FromPath(config_file_path_).Delete();}if (!is_config_checksum_pass(kConfigBackupComparePass)) {LegacyConfigFile::FromPath(config_backup_path_).Delete();}/*加载配置文件(主优先,备份次之)1. 尝试从主路径读取配置。2. 如果读取失败,或缺少关键的 "Adapter" 配置段,则尝试读取备份文件3. 并记录文件来源 "Backup"*/auto config = LegacyConfigFile::FromPath(config_file_path_).Read(temp_devices_capacity_);if (!config || !config->HasSection(kAdapterSection)) {LOG_WARN("cannot load config at %s, using backup at %s.", config_file_path_.c_str(), config_backup_path_.c_str());config = LegacyConfigFile::FromPath(config_backup_path_).Read(temp_devices_capacity_);file_source = "Backup";}/*加载失败:创建空配置1. 如果备份也不可用,同样缺少核心段落,则说明配置彻底损坏2. 创建一个新的空配置对象,参数用于初始化默认设备列表*/if (!config || !config->HasSection(kAdapterSection)) {LOG_WARN("cannot load backup config at %s; creating new empty ones", config_backup_path_.c_str());config.emplace(temp_devices_capacity_, Device::kLinkKeyProperties);file_source = "Empty";}/*记录配置来源(Info 段)1. 在配置中标记是从哪个源(主/备份/空)加载的2. 有利于后续调试或问题分析。*/if (!file_source.empty()) {config->SetProperty(kInfoSection, kFileSourceProperty, std::move(file_source)); // 这里不是设置到android的系统属性里,而是保存在 配置文件中。}/*清除“访客模式”下遗留数据1. 如果当前不是“受限模式”(如访客模式),则删除 Restricted 属性相关的 section。2. 防止访客数据泄露*/// Cleanup temporary pairings if we have left guest modeif (!is_restricted_mode_) {config->RemoveSectionWithProperty("Restricted");}/*初始化配置创建时间(如不存在)1. 读取配置信息段中的 TimeCreated2. 如果不存在,就使用当前系统时间格式化一个时间字符串写入3. 用于记录配置的首次生成时间*/// Read or set config file creation timestampauto time_str = config->GetProperty(kInfoSection, kTimeCreatedProperty);if (!time_str) {std::stringstream ss;auto now = std::chrono::system_clock::now();auto now_time_t = std::chrono::system_clock::to_time_t(now);ss << std::put_time(std::localtime(&now_time_t), kTimeCreatedFormat.c_str());config->SetProperty(kInfoSection, kTimeCreatedProperty, ss.str());}/*修正设备类型配置中的不一致项1. 自动扫描并修复配置中设备类型的异常数据2. 可能针对历史版本残留配置进行兼容性修复*/config->FixDeviceTypeInconsistencies();/*注册配置变更回调1. 当配置发生变更时,自动调用 SaveDelayed()2. SaveDelayed() 是延迟保存机制(之前提到默认 3 秒后),避免频繁写盘*/config->SetPersistentConfigChangedCallback([this] { this->CallOn(this, &StorageModule::SaveDelayed); });/*初始化核心实现(Impl)1. 创建 impl 类实例(Pimpl Idiom:接口/实现分离)2. 把读取好的配置传入其中,以及临时设备容量限制3. GetHandler() 提供事件处理线程句柄。*/// TODO (b/158035889) Migrate metrics module to GDpimpl_ = std::make_unique<impl>(GetHandler(), std::move(config.value()), temp_devices_capacity_);SaveDelayed();// 再次调用延迟保存,确保初始化之后的状态持久化/*尝试加解密配置密钥1. 如果系统启用了 Bluetooth Keystore 接口(密钥加解密),则尝试转换加密格式2. 用于配置文件中的 LinkKey 或密钥材料的迁移*/if (bluetooth::os::ParameterProvider::GetBtKeystoreInterface() != nullptr) {bluetooth::os::ParameterProvider::GetBtKeystoreInterface()->ConvertEncryptOrDecryptKeyIfNeeded();}
}
2. bt_config.conf
记录分享一个真实的 bt_config.conf
// adb pull /data/misc/bluedroid/bt_config.conf[Info]
FileSource = Empty
TimeCreated = 2025-04-18 10:33:13[Adapter]
Address = 22:22:96:de:b1:39
LE_LOCAL_KEY_IRK = 7c39750923b27df313f988bbfee74bb8
LE_LOCAL_KEY_IR = ad109bf638e98981aa439c9f0570bfe9
LE_LOCAL_KEY_DHK = d48e846c3fb280a572fbec8e096a137d
LE_LOCAL_KEY_ER = 74a3b80a8145351e9b0943600e8a68e3
ScanMode = 0
DiscoveryTimeout = 120
Name = cheji_8295[Metrics]
Salt256Bit = 94bb8f142ccfe6265a4b3ab38d447fcaexxxxxxxf7c6aa4043d7f[00:1b:dc:f4:b1:83]
Timestamp = 1745558511
DevClass = 1049092
DevType = 3
AddrType = 0
Name = PTS-PBAP-B183
LinkKeyType = 8
PinLength = 0
LinkKey = a88655950fc0ad9xxxxxxx31e96bde
MetricsId = 21
Service = 0000110a-0000-1000-8000-00805f9b34fb
AvdtpVersion = 0001[70:8f:47:91:b0:62]
Name = cbx
DevClass = 5898764
DevType = 3
AddrType = 0
SdpDiManufacturer = 29
SdpDiModel = 4608
SdpDiHardwareVersion = 5174
SdpDiVendorIdSource = 1
Service = 00001105-0000-1000-8000-00805f9b34fb 0000110a-0000-1000-8000-00805f9b34fb 0000110c-0000-1000-8000-00805f9b34fb 0000110e-0000-1000-8000-00805f9b34fb 00001112-0000-1000-8000-00805f9b34fb 00001115-0000-1000-8000-00805f9b34fb 00001116-0000-1000-8000-00805f9b34fb 0000111f-0000-1000-8000-00805f9b34fb 0000112d-0000-1000-8000-00805f9b34fb 0000112f-0000-1000-8000-00805f9b34fb 00001132-0000-1000-8000-00805f9b34fb 00001200-0000-1000-8000-00805f9b34fb 2c042b0a-7f57-4c0a-afcf-1762af70257c 8fa9c715-bd1f-596c-a1b0-13162b15c892 9fed64fd-e91a-499e-88dd-73dfe023feed
Timestamp = 1745510776
LinkKeyType = 8
PinLength = 0
LinkKey = 6453dc8978aexxxxxxx0b4e7b90
MetricsId = 25
AvdtpVersion = 0301
3.StorageModule::impl
/*初始化核心实现(Impl)1. 创建 impl 类实例(Pimpl Idiom:接口/实现分离)2. 把读取好的配置传入其中,以及临时设备容量限制3. GetHandler() 提供事件处理线程句柄。*/pimpl_ = std::make_unique<impl>(GetHandler(), std::move(config.value()), temp_devices_capacity_);
这里我们看一下 StorageModule::impl 构造做了那些事情。
struct StorageModule::impl {explicit impl(Handler* handler, ConfigCache cache, size_t in_memory_cache_size_limit): config_save_alarm_(handler), cache_(std::move(cache)), memory_only_cache_(in_memory_cache_size_limit, {}) {}Alarm config_save_alarm_;ConfigCache cache_;ConfigCache memory_only_cache_;bool has_pending_config_save_ = false;
};
- 整个 StorageModule::impl 类,也没有任何 操作方法。 只是将 传入的参数,保存了一下。
5.将module 实体加入到 started_modules_
- 将module 实体加入到 started_modules_
- started_modules_[module] = instance;
4. StorageModule 对外功能
当我们执行完 上述 5 个步骤 后,此时其他模块就可以 正常访问 StorageModule 模块的功能了。 我们来看一下, StorageModule 模块对外都提供了那些接口,供其他模块使用。
1. GetDeviceByLegacyKey
- system/gd/storage/storage_module.h
// Methods to access the storage layer via Device abstraction// - Devices will be lazily created when methods below are called. Hence, no std::optional<> nor nullptr is used in// the return type. User of the API can use the Device object's API to find out if the device has existed before// - Devices with no config values will not be saved to config cache// - Devices that are not paired will also be discarded when stack shutdown// Concept://// BR/EDR Address:// -> Public static address only, begin with 3 byte IEEE assigned OUI number//// BLE Addresses// -> Public Address: begin with IEEE assigned OUI number// -> Static: static public address do not change// -> Private/Variable: We haven't seen private/variable public address yet// -> Random Address: randomly generated, does not begin with IEEE assigned OUI number// -> Static: static random address do not change// -> Private/Variable: private random address changes once so often// -> Resolvable: this address can be resolved into a static address using identity resolving key (IRK)// -> Non-resolvable: this address is for temporary use only, do not save this address//// MAC addresses are six bytes only and hence are only regionally unique// Get a device object using the |legacy_key_address|. In legacy config, each device's config is stored in a config// section keyed by a single MAC address. For BR/EDR device, this is straightforward as a BR/EDR device has only a// single public static MAC address. However, for LE devices using private addresses, we only learn its real static// address after pairing. Since we still need to store that device's information prior to pairing, we use the// first-seen address of that device, no matter random private or static public, as a "key" to store that device's// config. This method gives you a device object using this legacy key. If the key does not exist, the device will// be lazily created in the configDevice GetDeviceByLegacyKey(hci::Address legacy_key_address);Device StorageModule::GetDeviceByLegacyKey(hci::Address legacy_key_address) {std::lock_guard<std::recursive_mutex> lock(mutex_);return Device(&pimpl_->cache_,&pimpl_->memory_only_cache_,std::move(legacy_key_address),Device::ConfigKeyAddressType::LEGACY_KEY_ADDRESS);
}
单纯的看 GetDeviceByLegacyKey 函数的实现很简单。 就是创建并返回了一个 蓝牙设备对象。但仔细对 注释就可以发现很多隐含的信息。
1. 注释解释
GetDeviceByLegacyKey 功能介绍:
-
通过 Device 抽象层访问存储层的方法
- 设备对象会在 GetDeviceByLegacyKey 方法被调用时“延迟创建”。因此,返回类型中不使用 std::optional<> 或 nullptr。 使用该 API 的用户可以通过 Device 对象本身的 API 判断该设备是否已经存在(即是否真实持久化)
- 没有配置值的设备不会被保存到配置缓存中(即不会持久化)
- 未配对的设备在蓝牙协议栈关闭时也会被丢弃(不会写入配置文件)
-
概念部分:
- BR/EDR 地址:
- 仅包含“公共静态地址”,以 IEEE 分配的 3 字节 OUI(组织唯一标识符)开头
- BLE 地址:
- 公共地址:同样以 IEEE 分配的 OUI 开头
- 静态型:静态公共地址不会改变
- 私有/可变型:目前尚未见到此类公共地址
- 随机地址:由设备随机生成,不以 IEEE OUI 开头
- 静态型:静态随机地址不会改变
- 私有/可变型:私有随机地址会周期性变化
- 可解析类型:该地址可通过 IRK(身份解析密钥)还原为固定地址
- 不可解析类型:该地址仅临时使用,不应保存(即不持久化)
- 公共地址:同样以 IEEE 分配的 OUI 开头
- BR/EDR 地址:
-
MAC 地址仅有 6 字节,因此它们只在区域内具有唯一性(不能全球唯一)
-
使用 legacy_key_address 获取设备对象:
-
使用
legacy_key_address
获取设备对象。在传统配置中,每个设备的配置信息存储在以 MAC 地址为键的配置段中。 -
对于 BR/EDR 设备来说,这很简单,因为它只有一个公共的、静态的 MAC 地址。
-
但对于使用私有地址的 LE 设备,我们只有在配对后才能知道它真正的静态地址
-
由于在配对之前我们仍然需要保存设备的信息,因此无论是随机私有地址还是静态公共地址,都会使用首次发现的地址作为“键”来存储该设备的配置信息。
-
此方法会根据该 legacy key 返回一个设备对象。如果该 key 不存在,则会延迟创建该设备(不会立即写入配置)。
这段注释主要说明了:
- 设备的创建是“延迟”的:只有在真正需要操作设备时才会生成
Device
对象。 - 返回值不是 optional 或指针:直接返回对象,由调用者通过 API 判断其是否存在于配置中。
- 未配对或无配置的设备不会被持久化:配置文件只保存有效信息。
- 设备地址有多种类型,尤其是 BLE 设备使用的随机地址在配对前无法确认其真实身份,因此使用“首次发现地址”作为配置键。
- 函数提供的是 legacy key(旧地址)所对应的设备对象,符合传统配置系统的逻辑。
2. 地址分类
类型 | 说明 | 是否可保存 |
---|---|---|
BR/EDR Public Static | 固定 MAC,3 字节 OUI 开头 | ✅ 可以保存 |
BLE Public Static | 固定 MAC,3 字节 OUI 开头 | ✅ 可以保存 |
BLE Random Static | 伪随机但固定 | ✅ 可以保存 |
BLE Random Private Resolvable | 会变,但可通过 IRK 解析 | ⚠️ 后续可替换 |
BLE Random Private Non-resolvable | 仅临时使用 | ❌ 不保存 |
在“未配对”情况下,我们只知道设备的某个临时地址,所以只能用这个地址作为“临时配置 key”,即 legacy key。
3. 为什么用 “legacy key address”?
- 在 Bluetooth BR/EDR(传统蓝牙)中,每个设备有唯一的 静态公共 MAC 地址,所以可以直接作为 key。
- 在 BLE(低功耗蓝牙)中,设备可能使用 随机地址(尤其是未配对前),这些地址不是长期固定的。
- 所以:我们在初次发现设备时,用“当时第一次见到的地址”作为
legacy_key_address
存储其配置。
4. 简单理解 GetDeviceByLegacyKey
功能:根据传入的 legacy_key_address(通常是首次发现时的地址)获取对应的 Device 对象,
如果不存在则延迟构造一个 Device(暂不落盘),用于配置读取或写入。
用途:支持对“尚未配对”但已被发现的设备进行设置或读取操作。
线程安全:内部加锁,防止多线程状态竞争。
设计理念:
- 延迟初始化设备对象;
- 设备对象接口可判断自己是否已经存在于配置中;
- 非配对设备或无配置数据的设备在堆栈关闭时将被丢弃。
2. GetDeviceByClassicMacAddress
- system/gd/storage/storage_module.h
// A classic (BR/EDR) or dual mode device can be uniquely located by its classic (BR/EDR) MAC addressDevice GetDeviceByClassicMacAddress(hci::Address classic_address);Device StorageModule::GetDeviceByClassicMacAddress(hci::Address classic_address) {std::lock_guard<std::recursive_mutex> lock(mutex_);return Device(&pimpl_->cache_,&pimpl_->memory_only_cache_,std::move(classic_address),Device::ConfigKeyAddressType::CLASSIC_ADDRESS);
}
1. 作用:
该函数的作用是:
根据一个经典蓝牙(BR/EDR)设备的 MAC 地址,获取对应的
Device
对象。
这适用于以下情况:
- 设备是经典蓝牙设备(BR/EDR only),或
- 设备是双模设备(同时支持 BR/EDR 和 BLE)
- 这些设备拥有一个 唯一且固定的公共 MAC 地址
2. 内部逻辑分析:
- 函数持有一个递归互斥锁
mutex_
,用于线程安全。 - 调用构造函数创建
Device
对象时,传入的地址类型是:
Device::ConfigKeyAddressType::CLASSIC_ADDRESS
这表示我们确定该地址是经典蓝牙设备的公共地址,可直接作为配置键。
3. 与 GetDeviceByLegacyKey
的区别
比较项 | GetDeviceByClassicMacAddress | GetDeviceByLegacyKey |
---|---|---|
适用设备类型 | 仅适用于经典蓝牙(BR/EDR)或双模设备 | 主要用于 BLE 设备,尤其是尚未配对、地址可能不固定的设备 |
地址含义 | 地址是设备唯一的、固定的公共 BR/EDR MAC 地址 | 地址可能是首次看到的 BLE 随机地址,用作“临时键” |
稳定性 | 地址稳定且可以长期作为唯一标识 | 地址可能会变化(如 BLE 私有地址),真实身份需配对后才能确认 |
地址类型标识 | Device::ConfigKeyAddressType::CLASSIC_ADDRESS | Device::ConfigKeyAddressType::LEGACY_KEY_ADDRESS |
配置键作用 | 直接用于配置文件中的设备段标识 | 用作“临时配置键”,直到设备配对后可替换为真实身份地址 |
典型使用场景 | 经典蓝牙耳机、扬声器、手机等设备 | BLE 手环、传感器、Beacon 等尚未配对的设备 |
4. 案例
// bt_config.conf[70:8f:47:91:b0:62] ← 蓝牙远端设备的 MAC 地址(作为唯一键)
Name = cbx ← 设备名称(可来自蓝牙广播或 SDP 查询)
DevClass = 5898764 ← 设备类别(Device Class)
DevType = 3 ← 设备类型(BR/EDR/LE/Dual)
AddrType = 0 ← 地址类型(Public/Random)
SdpDiManufacturer = 29 ← 设备 SDP 中的制造商 ID
SdpDiModel = 4608 ← SDP 中的设备型号
SdpDiHardwareVersion=...← SDP 中的硬件版本号
SdpDiVendorIdSource = 1 ← Vendor ID 来源类型
Service = ... ← SDP 中支持的服务 UUID 列表
Timestamp = 1745510776 ← 上次交互时间戳(Unix 时间)
LinkKeyType = 8 ← Link Key 类型(身份验证方式)
PinLength = 0 ← PIN 长度(传统配对时使用)
LinkKey = ... ← 用于身份验证的配对密钥
MetricsId = 25 ← 用于度量标识的 ID
AvdtpVersion = 0301 ← 远端设备支持的 AVDTP 协议版本
// 当我们调用如下函数, 就可以从 我们的配置文件中,生成 70:8f:47:91:b0:62 设备对应的 Device.
GetDeviceByClassicMacAddress("70:8f:47:91:b0:62")
字段名 | 说明 |
---|---|
[70:8f:47:91:b0:62] | 蓝牙设备的 MAC 地址,用于唯一标识配对设备。 |
Name = cbx | 蓝牙设备名称,可能来自广播包或 SDP。 |
DevClass = 5898764 | 蓝牙设备类别(Device Class),十进制形式,拆成 bit 字段可表示主要/次要设备类别和服务类别。 |
DevType = 3 | 设备类型:1 = BR/EDR 2 = LE 3 = Dual(支持 BR/EDR + LE) |
AddrType = 0 | 地址类型:0 = Public Address 1 = Random Address |
SdpDiManufacturer = 29 | SDP 的 Device Identification Profile 中的 manufacturer ID(制造商编号,29 代表 Samsung) |
SdpDiModel = 4608 | SDP 中的 Model ID,厂商自定义字段,表明型号 |
SdpDiHardwareVersion = 5174 | SDP 中的硬件版本号(例如可能表示 HW rev 5.1.74) |
SdpDiVendorIdSource = 1 | Vendor ID 来源类型:1 = Bluetooth SIG 分配的 Vendor ID2 = USB-IF 分配的 Vendor ID |
Service = UUID 列表 | SDP 查询发现的支持服务 UUID(以空格分隔):例如 0000110a... 是 A2DP Sink具体服务见下表 |
Timestamp = 1745510776 | 最后一次交互时间戳(Unix 时间,单位:秒) |
LinkKeyType = 8 | Link Key 类型,表示加密方式:4 = Unauthenticated 5 = Authenticated 6 = Changed combination 8 = Secure connections |
PinLength = 0 | 用于旧配对方式的 PIN 长度,常为 0 表示未使用。 |
LinkKey = … | BR/EDR 模式下配对生成的密钥,用于身份验证。是一个 16 字节的十六进制字符串。 |
MetricsId = 25 | Android Bluetooth Metrics 用于标识配对设备来源的 ID,方便统计用途 |
AvdtpVersion = 0301 | 表示远端支持的 AVDTP 版本号,例如:0301 = 版本 1.3.1(用于 A2DP) |
UUID | 服务 | 功能 |
---|---|---|
00001105-… | OBEX Object Push | 发送名片、图片等文件 |
0000110a-… | A2DP Sink | 接收音频(如车机、音箱) |
0000110c-… | A/V Remote Control Target | 接收遥控命令 |
0000110e-… | Hands-Free | 免提通话协议 |
00001112-… | Headset Audio Gateway | 耳机音频通道 |
00001115-… | PANU | 个人区域网用户 |
0000111f-… | Handover | 用于 NFC 触发蓝牙连接 |
0000112d/2f | PBAP | 电话簿访问协议 |
00001132-… | Message Access Profile | 短信访问协议 |
00001200-… | Generic Access Profile (GAP) | 基本服务发现 |
自定义 UUID | 比如 2c042b0a-... 、8fa9c715-... | 厂商自定义服务 UUID,用于特定应用协议 |
5. 总结
-
GetDeviceByClassicMacAddress
是用于经典蓝牙设备的标准接口,因为这类设备的地址具有唯一性与稳定性,可以直接作为配置的主键使用。 -
GetDeviceByLegacyKey
是为了解决 BLE 设备使用随机地址的配对前场景,在无法获取真实身份地址前,临时使用首次看到的地址作为键来保存配置。
前者是正式识别用地址,后者是临时过渡用地址。
3. GetDeviceByLeIdentityAddress
- system/gd/storage/storage_module.h
// A LE or dual mode device can be uniquely located by its identity address that is either:// -> Public static address// -> Random static address// If remote device uses LE random private resolvable address, user of this API must resolve its identity address// before calling this method to get the device object//// Note: A dual mode device's identity address is normally the same as its BR/EDR address, but they can also be// different. Hence, please don't make such assumption and don't use GetDeviceByBrEdrMacAddress() interchangeablyDevice GetDeviceByLeIdentityAddress(hci::Address le_identity_address);Device StorageModule::GetDeviceByLeIdentityAddress(hci::Address le_identity_address) {std::lock_guard<std::recursive_mutex> lock(mutex_);return Device(&pimpl_->cache_,&pimpl_->memory_only_cache_,std::move(le_identity_address),Device::ConfigKeyAddressType::LE_IDENTITY_ADDRESS);
}
1. 注释介绍
// A LE or dual mode device can be uniquely located by its identity address that is either: // -> Public static address
// -> Random static address
BLE 或双模设备都拥有一个唯一的 身份地址(Identity Address),这个地址有以下两种合法类型:
-
Public static address(公开静态地址):以 IEEE 分配的 OUI 开头,是制造商固定分配的地址;
-
Random static address(随机静态地址):使用随机算法生成,但一段时间内保持不变。
这两种地址都能唯一标识 BLE 设备,是建立配对关系、保存配置、建立连接的“身份证”。
// If remote device uses LE random private resolvable address, user of this API must resolve its identity address// before calling this method to get the device object
如果远程设备使用的是:
- Resolvable Private Address(RPA,可解析随机地址)
那么调用方必须先通过本地保存的 IRK(Identity Resolving Key) 解析出对方的真实身份地址(即 Public Static 或 Random Static),再调用本方法。
否则你可能会用一个临时地址来查找设备,查找会失败或返回新对象。
// Note: A dual mode device's identity address is normally the same as its BR/EDR address, but they can also be// different. Hence, please don't make such assumption and don't use GetDeviceByBrEdrMacAddress() interchangeably
重要警告:不要混用 BLE 身份地址 和 BR/EDR 地址!
- 对于双模设备(支持 BLE 和 BR/EDR),其 BLE 身份地址 通常和 BR/EDR MAC 地址相同,但这并不是强制的。
- 因此:
- 不能假设它们相同!
- 不要用
GetDeviceByBrEdrMacAddress()
来获取 BLE 设备对象,应使用GetDeviceByLeIdentityAddress()
。
2. 作用
该函数用于通过 LE 设备的身份地址(Identity Address) 获取对应的 Device
对象,即代表某个 BLE 或双模设备(BR/EDR + BLE)的对象。
3. 使用时机和条件
使用场景 | 是否适用 |
---|---|
设备是 BLE-only 或 Dual-mode | ✅ 适用 |
已知对方的身份地址(public static 或 random static) | ✅ 适用 |
对方使用的是 RPA(可解析地址) | ❌ 必须先解析出身份地址才能使用此方法 |
只知道 BR/EDR 地址 | ❌ 应使用 GetDeviceByClassicMacAddress() |
不清楚地址类型(例如首次见到的随机地址) | ❌ 应使用 GetDeviceByLegacyKey() 保存首次信息 |
4. 与其它获取设备方法对比
方法 | 适用地址类型 | 场景 | 是否需已知身份 |
---|---|---|---|
GetDeviceByLegacyKey() | 任意首次见到的地址 | 初次遇到设备,尚未配对或解析身份时 | ❌ |
GetDeviceByClassicMacAddress() | BR/EDR MAC | 经典蓝牙设备(或双模的 BR/EDR 部分) | ✅ |
GetDeviceByLeIdentityAddress() | Public static / Random static | BLE 设备(或双模 BLE 部分),身份已解析 | ✅ |
5.例子
假设你车机上连接了一个 BLE 心率带(Heart Rate Monitor):
- 心率带使用 RPA 地址广播;
- 你使用 IRK 成功解析出其
identity address = A4:C1:38:11:22:33
; - 然后你可以:
Device heart_rate_device = storage_module.GetDeviceByLeIdentityAddress(Address::FromString("A4:C1:38:11:22:33"));
此时 Device
对象会对应到配置文件中 [A4:C1:38:11:22:33]
段,所有配置信息(配对信息、特征值、服务等)都与这个地址关联。
4. GetAdapterConfig
- system/gd/storage/storage_module.h
// A think copyable, movable, comparable object that is used to access adapter level informationAdapterConfig GetAdapterConfig();AdapterConfig StorageModule::GetAdapterConfig() {std::lock_guard<std::recursive_mutex> lock(mutex_);return AdapterConfig(&pimpl_->cache_, &pimpl_->memory_only_cache_, kAdapterSection);
}
1. AdapterConfig 介绍
AdapterConfig 是一个:
- 轻量级(thin) 对象:开销小
- 可复制(copyable)
- 可移动(movable)
- 可比较(comparable)
它的设计目的是:用于访问“适配器级别(adapter-level)”的信息。
这里的“适配器”一般是指 蓝牙本地控制器(Bluetooth Adapter),也就是主机设备自身的蓝牙模块,而不是连接的外部设备。
2. 作用:
获取一个表示当前本地蓝牙适配器配置的
AdapterConfig
对象。
这个对象可以被用来:
- 读取本地蓝牙模块的各种状态配置(如名称、地址、功能支持情况)
- 写入/修改这些配置值(例如设置本地设备名)
- 比较不同配置是否相等
- 被用于持久化或在不同模块间传递
3. 参数说明:
AdapterConfig(&pimpl_->cache_, &pimpl_->memory_only_cache_, kAdapterSection)
AdapterConfig
被初始化时传入了三个参数:
参数 | 含义 |
---|---|
&pimpl_->cache_ | 持久化配置缓存,读取自 config 文件,用于保存“有用的设备信息” |
&pimpl_->memory_only_cache_ | 仅保存在内存中的配置项,不会写入磁盘,适合存放临时信息 |
kAdapterSection | 表示“配置文件中哪一段”是适配器相关配置,一般是 [Adapter] |
这个构造函数的逻辑是:
在两个缓存中查找和操作
kAdapterSection
(也就是适配器配置段)的属性。
4. 使用场景举例
我们可以用 AdapterConfig
做如下操作:
读取属性
auto config = storage_module.GetAdapterConfig();
auto name = config.GetProperty("Name"); // 读取适配器名称
写入属性
config.SetProperty("Name", "MyBluetoothCar");
删除属性
config.RemoveProperty("DiscoverableTimeout");
这些修改通常会在适当的时候触发保存(例如通过 SaveDelayed()
)。
5. 与设备配置的区别
类别 | 接口 | 描述 |
---|---|---|
适配器配置 | GetAdapterConfig() | 访问和操作 本地蓝牙适配器的属性(如本地设备名称、可发现性等) |
设备配置 | GetDeviceByClassicMacAddress() 等 | 针对每一个连接的远程设备,分别维护其配置信息(如配对密钥、设备类型等) |
6. 案例
[Adapter]
Address = 22:22:96:de:b1:39
LE_LOCAL_KEY_IRK = 7c39750923b27df313f988bbfee74bb8
LE_LOCAL_KEY_IR = ad109bf638e98981aa439c9f0570bfe9
LE_LOCAL_KEY_DHK = d48e846c3fb280a572fbec8e096a137d
LE_LOCAL_KEY_ER = 74a3b80a8145351e9b0943600e8a68e3
ScanMode = 0
DiscoveryTimeout = 120
Name = cheji_8295
配置项 | 示例值 | 含义与功能说明 |
---|---|---|
Address | 22:22:96:de:b1:39 | 本地蓝牙模块的 MAC 地址,是蓝牙适配器在 BR/EDR 和 BLE 中都使用的物理地址。它由硬件指定,唯一标识当前设备。 |
LE_LOCAL_KEY_IRK | 7c39750923b27df313f988bbfee74bb8 | Identity Resolving Key (IRK):用于在 BLE 中解析对方设备的 可解析随机地址(Resolvable Private Address)。是隐私保护机制的一部分。 |
LE_LOCAL_KEY_IR | ad109bf638e98981aa439c9f0570bfe9 | Identity Root (IR):用作生成本地 BLE 安全密钥(如 IRK)的根密钥,AOSP BLE 栈通过它生成 IRK。是 BLE 安全子系统的基础密钥之一。 |
LE_LOCAL_KEY_DHK | d48e846c3fb280a572fbec8e096a137d | Diversifier Hash Key (DHK):用于计算加密用的 Diversifier(DIV) 的散列,在密钥分发和存储时使用。 |
LE_LOCAL_KEY_ER | 74a3b80a8145351e9b0943600e8a68e3 | Encryption Root (ER):用于派生加密相关的 BLE 密钥,如 LTK(长期密钥)。是 BLE 的核心加密密钥根。 |
ScanMode | 0 | 蓝牙适配器的 可发现性模式: - 0 :不可发现,不可连接 - 1 :仅限连接(不可被搜索到) - 2 :可发现且可连接 |
DiscoveryTimeout | 120 | 蓝牙在“可发现模式”下的持续时间(单位:秒),超过这个时间后将自动退出发现状态。 |
Name | cheji_8295 | 本地蓝牙设备名称(如在手机、耳机列表中看到的名称)。默认显示给外部设备的名称,车机中常被设置为“设备名”。 |
关于 BLE 密钥项的小总结
密钥项 | 用途分类 | 作用 |
---|---|---|
IRK | 隐私 | 解析远程设备的 Resolvable Private Address |
IR | 身份根密钥 | 用于派生 IRK |
ER | 加密根密钥 | 用于派生 LTK、STK 等 |
DHK | 散列辅助密钥 | 用于生成身份散列与验证 |
这些密钥是在 BLE 的配对和加密过程中 本地设备生成和使用的私有密钥,通常保存在本地配置中,确保在设备重启后仍能保持配对关系。
示例应用场景
- 车机初次启动时,蓝牙模块读取
bt_config.conf
中的[Adapter]
段,恢复 BLE 密钥、名称、扫描模式等信息。 - BLE 连接中,系统通过
IRK
解析陌生的随机地址,判断是否为熟悉设备。 - ScanMode 与 DiscoveryTimeout 结合使用,控制设备是否暴露在搜索中,避免长时间保持可发现节省功耗。
7. 总结
GetAdapterConfig()
返回的是一个轻量级对象,用于访问 本地蓝牙适配器(即车机或手机自身)相关的配置。- 它支持读写、比较,适合被广泛传递使用。
- 与
GetDeviceXXX()
系列函数返回的是远程设备的配置不同,这里关注的是本地适配器级别的设置(Adapter Section)。
5. GetBondedDevices
- system/gd/storage/storage_module.h
// Get a list of bonded devices from configstd::vector<Device> GetBondedDevices();std::vector<Device> StorageModule::GetBondedDevices() {std::lock_guard<std::recursive_mutex> lock(mutex_);/*1. GetConfigCache() 返回的是配置缓存对象(通常是对 bt_config.conf 的抽象表示)。2. GetPersistentSections() 会获取所有持久化的 section 名称,也就是 config 文件中类似于 [XX:XX:XX:XX:XX:XX] 的条目3. 这些 section 通常是代表 已绑定设备 的 MAC 地址段。*/auto persistent_sections = GetConfigCache()->GetPersistentSections();std::vector<Device> result; // 创建结果数组 result,类型为 std::vector<Device>result.reserve(persistent_sections.size()); // 预先分配空间以提升性能,避免多次内存分配/*1. 遍历每个配置段名(通常是 MAC 地址);2. 为每个 section 构造一个 Device 对象,并将其加入结果列表中;3. 这些 Device 对象通过配置 section 名(通常是地址)定位,并绑定了:1. pimpl_->cache_: 永久性缓存(从 config 文件中读取);2. pimpl_->memory_only_cache_: 内存中的缓存(运行时变化,但不持久化);3. section: 该设备的唯一标识(如 MAC 地址);*/for (const auto& section : persistent_sections) {result.emplace_back(&pimpl_->cache_, &pimpl_->memory_only_cache_, section);}return result;
}
1. 作用
从配置中获取所有已绑定(Bonded)的设备列表,并以 std::vector<Device>
的形式返回。
2. 什么是 Bonded Device?
在蓝牙中,“Bonded” 表示 两台设备已经完成配对,并在配置中保存了配对信息(如密钥),以便后续自动连接。
在 Android 和 AOSP 中,bonded 设备的身份信息会被写入 bt_config.conf
,如下所示:
[00:1b:dc:f4:b1:83]
Timestamp = 1745558511
DevClass = 1049092
DevType = 3
AddrType = 0
Name = PTS-PBAP-B183
LinkKeyType = 8
PinLength = 0
LinkKey = a88655950fc0ad9xxxxxxx31e96bde
MetricsId = 21
Service = 0000110a-0000-1000-8000-00805f9b34fb
AvdtpVersion = 0001[70:8f:47:91:b0:62]
Name = cbx
DevClass = 5898764
DevType = 3
AddrType = 0
SdpDiManufacturer = 29
SdpDiModel = 4608
SdpDiHardwareVersion = 5174
SdpDiVendorIdSource = 1
Service = 00001105-0000-1000-8000-00805f9b34fb 0000110a-0000-1000-8000-00805f9b34fb 0000110c-0000-1000-8000-00805f9b34fb 0000110e-0000-1000-8000-00805f9b34fb 00001112-0000-1000-8000-00805f9b34fb 00001115-0000-1000-8000-00805f9b34fb 00001116-0000-1000-8000-00805f9b34fb 0000111f-0000-1000-8000-00805f9b34fb 0000112d-0000-1000-8000-00805f9b34fb 0000112f-0000-1000-8000-00805f9b34fb 00001132-0000-1000-8000-00805f9b34fb 00001200-0000-1000-8000-00805f9b34fb 2c042b0a-7f57-4c0a-afcf-1762af70257c 8fa9c715-bd1f-596c-a1b0-13162b15c892 9fed64fd-e91a-499e-88dd-73dfe023feed
Timestamp = 1745510776
LinkKeyType = 8
PinLength = 0
LinkKey = 6453dc8978aexxxxxxx0b4e7b90
MetricsId = 25
AvdtpVersion = 0301
这类信息只有配对完成后才会存在。
3. 小结
内容 | 说明 |
---|---|
功能 | 返回所有已绑定设备(从配置文件读取) |
数据来源 | bt_config.conf 中所有持久化的 [section] (通常是设备地址) |
条件 | 已绑定设备才会出现在 config 文件中 |
返回类型 | std::vector<Device> ,每个元素表示一个绑定设备 |
线程安全 | 是(加了 mutex 锁) |
常见用途 | 蓝牙启动后恢复已配对设备、构建连接设备列表、展示配对历史等 |
6. Modify
- system/gd/storage/storage_module.h
// Modify the underlying config by starting a mutation. All entries in the mutation will be applied atomically when// Commit() is called. User should never touch ConfigCache() directly.Mutation Modify();Mutation StorageModule::Modify() {std::lock_guard<std::recursive_mutex> lock(mutex_);return Mutation(&pimpl_->cache_, &pimpl_->memory_only_cache_);
}
1. 功能:
- 提供一种 安全且原子的方式 修改底层配置(如
bt_config.conf
)。 - 返回一个
Mutation
对象,允许你设置多个键值对。 - 所有改动在
Commit()
前不会生效,这确保了改动的 原子性(要么全生效,要么全不生效)。
2. Mutation 是什么?
Mutation mutation = storage_module.Modify();
mutation.SetProperty(address, "Name", "my_device");
mutation.SetProperty(address, "LinkKey", "AABBCC...");
mutation.Commit();
1. Mutation
的工作流程:
阶段 | 操作 | 说明 |
---|---|---|
开始修改 | Mutation mutation = Modify() | 获取一个修改器对象 |
设置内容 | mutation.SetProperty(...) | 设置一个或多个配置项 |
提交 | mutation.Commit() | 所有配置原子性地写入 config 文件 |
2. 意义和优势
特性 | 说明 |
---|---|
原子性 | 所有配置更改必须通过 Commit() 提交,保证一致性 |
安全性 | 多线程访问加锁,防止并发读写冲突 |
避免误用 | 不允许直接访问 ConfigCache() ,必须通过 Mutation 间接操作 |
灵活性 | 一次修改多个配置字段,不需要单独写入磁盘多次 |
统一接口 | 所有对配置的修改都封装在 Mutation 内,便于维护和扩展 |
3. 使用场景示例
以下是几个典型的使用情境:
1. 蓝牙配对完成后写入配对信息
auto mutation = storage->Modify();
mutation.SetProperty(mac_address, "LinkKey", link_key);
mutation.SetProperty(mac_address, "KeyType", key_type);
mutation.Commit();
2. 用户更改蓝牙本地设备名称
auto mutation = storage->Modify();
mutation.SetAdapterProperty("Name", "My_Car_Device");
mutation.Commit();
3. 清除某个设备的配置信息
auto mutation = storage->Modify();
mutation.RemoveDevice(device_address);
mutation.Commit();
4. 小结
项目 | 内容 |
---|---|
函数名 | StorageModule::Modify() |
返回值 | Mutation 对象 |
作用 | 开启一段“配置修改事务” |
线程安全 | 是(加锁) |
提交方式 | 通过 Mutation::Commit() 原子性写入 |
好处 |
相关文章:
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
1. 背景 我们在 gd_shim_module 介绍章节中,看到 我们将 StorageModule 模块加入到了 modules 中。 // system/main/shim/stack.cc modules.add<storage::StorageModule>();在 ModuleRegistry::Start 函数中我们对 加入的所有 module 挨个初始化。 而在该函…...
Datawhale 5月llm-universe 第1次笔记
课程地址:GitHub - datawhalechina/llm-universe: 本项目是一个面向小白开发者的大模型应用开发教程,在线阅读地址:https://datawhalechina.github.io/llm-universe/ 难点:配置conda环境变量 我用的vscode github方法 目录 重要…...
Linux架构篇、第五章git2.49.0部署与使用
Linux_架构篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:git2.49.0部署与使用 版本号: 1.0,0 作者: 老王要学习 日期: 2025.05.13 适用环境: Centos7 文档说明 这份文档聚焦于在 CentOS 7 环境下部署和…...
南方科技大学Science! 自由基不对称催化新突破 | 乐研试剂
近日,南方科技大学刘心元教授团队联合浙江大学洪鑫教授团队在自由基不对称催化领域取得新进展。课题组开发了一系列大位阻阴离子 N,N,P-配体,用于铜催化未活化外消旋仲烷基碘与亚砜亚胺的不对称胺化反应。该反应表现出广泛的底物兼容性,涵盖具…...
手机换IP真的有用吗?可以干什么?
在当今数字化时代,网络安全和个人隐私保护日益受到重视。手机作为我们日常生活中不可或缺的工具,其网络活动痕迹往往通过IP地址被记录和追踪。那么,手机换IP真的有用吗?它能为我们带来哪些实际好处?本文将为你一一解答…...
【C++详解】类和对象(上)类的定义、实例化、this指针
文章目录 一、类的定义1、类定义格式2、访问限定符3、类域 二、实例化1、实例化概念2、对象大小 三、this指针 一、类的定义 1、类定义格式 class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中…...
C语言—再学习(数据的存储类别)
在c语言中,每个变量和函数都有两个属性:数据类型和数据的存储类别 C的存储类别包括4种:自动挡(auto)、静态的(static)、寄存器的(register)、外部的(extern&…...
软考软件评测师——计算机组成与体系结构(分级存储架构)
一、虚拟存储技术 虚拟存储系统通过软硬件协同实现内存扩展,其核心特征包括: 逻辑容量扩展能力:实际物理内存与外存结合,呈现远大于物理内存的连续地址空间动态加载机制:程序运行时仅加载必要部分到内存,…...
需求跟踪矩阵准确性的5大策略
需求跟踪矩阵的准确性可显著提升软件项目质量,确保需求的全面覆盖、减少遗漏和偏差,有利于优化变更管理,降低返工风险,最终保障产品符合用户预期和业务目标。如果不能保证跟踪矩阵的准确性,可能会导致需求遗漏、测试覆…...
【调度算法】MAPF多智能体路径规划问题
参考链接:https://blog.csdn.net/qq_43353179/article/details/129396325 在这篇博客的基础上对一些省略的部分进行补充。 网站:https://mapf.info/ 可行性判断 1. k-鲁棒性(k-robust MAPF) 在经典 MAPF 中,只要所有…...
迅龙3号基于兆讯MH22D3适配CST328多点触摸驱动开发笔记
MH22D3芯片是兆讯公司新推出的基于cortex-M3内核的新一代芯片,专注于显示应用,其主频高达216Mhz,64KB SRAM,512KB Flash,开发UI应用游刃有余。详细介绍请看:MH22D3新一代显控应用性价比之王 新龙微基于MH22…...
推荐算法工程化:ZKmall模板商城的B2C 商城的用户分层推荐策略
在 B2C 电商竞争激烈的市场环境中,精准推荐已成为提升用户体验、促进商品销售的关键。ZKmall 模板商城通过推荐算法工程化手段,深度挖掘用户数据价值,制定科学的用户分层推荐策略,实现 “千人千面” 的个性化推荐,帮助…...
你对于JVM底层的理解
JVM(Java虚拟机)是一个执行Java字节码的虚拟机,负责将Java程序的代码转化为能够在不同操作系统上运行的机器码。为了深入理解JVM的底层工作原理,可以从以下几个方面入手: 1. 类加载机制 JVM的类加载机制是其核心之一…...
深入探讨 Java 性能术语与优化实践
在 Java 开发中,性能优化是确保应用程序高效运行的关键。无论是构建实时处理系统还是大规模分布式服务,理解性能术语和分析方法都至关重要。本文将详细介绍 Java 性能中的核心术语,包括延迟(Latency)、吞吐量(Throughput)、利用率(Utilization)、效率(Efficiency)、…...
简单介绍Qt的属性子系统
深入理解Qt的属性系统 笔者最近正在大规模的开发Qt的项目和工程,这里笔者需要指出的是,这个玩意在最常规的Qt开发中是相对比较少用的,笔者也只是在Qt的QPropertyAnimation需要动画感知笔者设置的一个属性的时候方才知道这个东西的。因此&…...
【PmHub后端篇】PmHub中基于自定义注解和AOP的服务接口鉴权与内部认证实现
1 引言 在现代软件开发中,尤其是在微服务架构下,服务接口的鉴权和内部认证是保障系统安全的重要环节。本文将详细介绍PmHub中如何利用自定义注解和AOP(面向切面编程)实现服务接口的鉴权和内部认证,所涉及的技术知识点…...
消息~组件(群聊类型)ConcurrentHashMap发送
为什么选择ConcurrentHashMap? 在开发聊天应用时,我们需要存储和管理大量的聊天消息数据,这些数据会被多个线程频繁访问和修改。比如,当多个用户同时发送消息时,服务端需要同时处理这些消息的存储和查询。如果用普通的…...
掌控随心 - 服务网格的流量管理艺术 (Istio 实例)
掌控随心 - 服务网格的流量管理艺术 (Istio 实例) 想象一下,没有服务网格的时候,我们要实现像“将 1% 的用户流量导入到新版本应用”、“根据用户设备类型访问不同后端”、“模拟下游服务故障”这类高级流量策略,通常需要在代码、负载均衡器、API 网关等多个地方进行复杂且分…...
Github 2025-05-13 Python开源项目日报 Top10
根据Github Trendings的统计,今日(2025-05-13统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目10TypeScript项目1 ComfyUI:强大而模块化的稳定扩散GUI 创建周期:399 天开…...
Spring Boot 自动装配原理详解
Spring Boot 的自动装配(Auto-Configuration)是其核心特性之一,它极大地简化了 Spring 应用的配置过程。通过自动装配,Spring Boot 能够根据项目中的依赖(例如,添加了 Spring Data JPA 依赖后自动配置数据库…...
Python核心数据类型全解析:字符串、列表、元组、字典与集合
导读: Python 是一门功能强大且灵活的编程语言,而其核心数据类型是构建高效程序的基础。本文深入剖析了 Python 的五大核心数据类型——字符串、列表、元组、字典和集合,结合实际应用场景与最佳实践,帮助读者全面掌握这些数据类型…...
索尼(sony)摄像机格式化后mp4的恢复方法
索尼(sony)的Alpha 7 Ⅳ系列绝对称的上是索尼的“全画幅标杆机型”,A7M4配备了3300万像素的CMOS,以及全新研发的全画幅背照式Exmor R™CMOS影像传感器,搭载BIONZ XR™影像处理器,与旗舰微单™Alpha 1如出一辙。下面我们来看看A7M4…...
Kubernetes容器运行时:Containerd vs Docker
Containerd 和 Docker 是容器技术领域的两个核心组件,它们在功能定位、架构设计、性能特点及适用场景上有显著差异。以下是两者的详细对比分析: 一、定位与功能 特性DockerContainerd核心定位完整的容器平台,包含构建、运行、编排等全生命周…...
免费专业级 PDF 处理!SolidPDF OCR 识别 + 精准转换批量处理
各位办公小能手们!今天咱来聊聊一款超牛的软件——SolidConverterPDF。这可是个专业的多功能PDF处理工具,啥格式转换、文档编辑、扫描识别,它都能搞定!下面我就给大伙详细唠唠它的厉害之处。 先说说它的核心功能。 一是PDF格式转换…...
电子电器架构 --- 区域计算架构(Zonal Compute)备战下一代电子电气架构
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
API的学习总结(上)
在 Java 中,API 指的是 Java 提供的一系列类、接口、方法和工具,用于开发 Java 应用程序。Java API 是 Java 平台的核心组成部分,它提供了丰富的功能,包括基础数据类型、集合框架、输入输出、网络编程、多线程、数据库连接等。 核…...
Spring Boot之Web服务器的启动流程分析
如何判断创建哪种web容器:servlet?reactive? 我们在启动Spring Boot程序的时候,会使用SpringApplication.run方法来启动,在启动流程中首先要判断的就是需要启动什么类型的服务器,是servlet?或者…...
代码随想录算法训练营第六十三天| 图论9—卡码网47. 参加科学大会,94. 城市间货物运输 I
每日被新算法方式轰炸的一天,今天是dijkstra(堆优化版)以及Bellman_ford ,尝试理解中,属于是只能照着代码大概说一下在干嘛。 47. 参加科学大会 https://kamacoder.com/problempage.php?pid1047 dijkstra(…...
RAG之大规模解析 PDF 文档全流程实战
PDF 文档在商业、学术和政府领域无处不在,蕴含着大量宝贵信息。然而,从 PDF 中提取结构化数据却面临着独特的挑战,尤其是在处理数千甚至数百万个文档时。本指南探讨了大规模解析 PDF 的策略和工具。 PDF解析挑战 PDF 的设计初衷是为了提供一致的视觉呈现,而非数据提取。这…...
uart16550详细说明
一、介绍 uart16550 ip core异步串行通信IP连接高性能的微控制器总线AXI,并为异步串行通信提供了 控制接口。软核设计连接了axilite接口。 二、特性 1.axilite接口用于寄存器访问和数据传输 2.16650串口和16450串口的软件和硬件寄存器都是兼容的 3.默认的core配置参数…...
Docker 环境安装(2025最新版)
Docker在主流的操作系统和云平台上都可以使用,包括Linux操作 系统(如Ubuntu、 Debian、Rocky、Redhat等)、MacOS操作系统和 Windows操作系统,以及AWS等云平 台。 Docker官网: https://docs.docker.com/ 配置宿主机网…...
Comparator不满足自反性错误,Comparison method violates its general contract
APP运行退出,跟踪信息 java.lang.IllegalArgumentException: Comparison method violates its general contract! Collections.sort(idxsList);//按score升序排列 查看idxs类 public int compareTo(Idxs o) { //重写compareTo方法 return (int) (this.g…...
[Java实战]Spring Boot 3 整合 Apache Shiro(二十一)
[Java实战]Spring Boot 3 整合 Apache Shiro(二十一) 引言 在复杂的业务系统中,安全控制(认证、授权、加密)是核心需求。相比于 Spring Security 的重量级设计,Apache Shiro 凭借其简洁的 API 和灵活的扩…...
如何界定合法收集数据?
首席数据官高鹏律师团队 在当今数字化时代,数据的价值日益凸显,而合法收集数据成为了企业、机构以及各类组织必须严守的关键准则。作为律师,深入理解并准确界定合法收集数据的范畴,对于保障各方权益、维护法律秩序至关重要。 一…...
Flask+HTML+Jquery 文件上传下载
HTML 代码: <div id"loadingIndicator" style"display:none;"><div class"spinner"></div> </div> <!-- 请求过程中转圈圈 --> <form action"" method"post" enctype"m…...
MapReduce打包运行
(一)maven打包 MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序(例如:jar…...
国产化Word处理控件Spire.Doc教程:如何使用 C# 从 Word 中提取图片
通过编程方式从 Word 文档中提取图片,可以用于自动化文档处理任务。E-iceblue旗下Spire系列产品是国产文档处理领域的优秀产品,支持国产化,帮助企业高效构建文档处理的应用程序。本文将演示如何使用 C# 和 Spire.Doc for .NET 库从 Word 文件…...
07 mysql之DQL
一、什么是DQL DQL 是 SQL 的一部分,专门用于查询数据。核心命令是 SELECT,是最常用的命令,支持: 简单查询条件过滤排序与分页多表连接聚合统计子查询与复杂逻辑二、基础查询语法 SELECT 字段1, 字段2, ... FROM 表名 WHERE 条件表达式 GROUP BY 分组字段 HAVING 分组条件…...
spark-standalone
一、定义:Standalone 模式是一种独立的集群部署模式,自带完整服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统。 二、配置步骤 1.和前面一样拉到hadoop101的/opt/module这个目录里面。 2.压缩 3.重命名为spark-sta…...
运行Spark程序-在shell中运行 --SparkConf 和 SparkContext
SparkConf 类用于配置 Spark 应用程序的各种参数。通过 SparkConf 类,你可以设置应用程序的名称、运行模式(如本地模式、集群模式)、资源分配(如内存、CPU 核心数)等。主要作用配置应用程序参数:可以设置 S…...
分割任务 - 数据增强
语义分割 - FCN : 数据预处理/数据增强 算法源码实例 base_size520 crop_size480 flip_prob0.5if train_val train:self.transforms transforms.Compose([transforms.RandomResize(int(base_size*0.5), int(base_size*2)),transforms.RandomHorizontalFlip(flip_…...
基于C#+MySQL实现(WinForm)企业设备使用信息管理系统
企业设备使用信息管理系统 引言 企业的设备管理在企业的生产制造和管理过程之中意义比较重大,明确企业的设备的产权和维护成本对于企业的成本控制和财务管理之中起到了重要的作用。随着市场竞争的加剧,现代企业所处的市场环境发生了深刻的变革…...
JavaScript异步编程 Async/Await 使用详解:从原理到最佳实践
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》…...
Babylon.js学习之路《四、Babylon.js 中的相机(Camera)与视角控制》
文章目录 1. 引言:为什么相机是 3D 场景的“眼睛”?1.1 相机的核心作用1.2 常见相机类型概览 2. 相机基础参数解析2.1 通用属性2.2 相机坐标系 3. 详解常用相机类型3.1 自由相机(FreeCamera)3.2 弧形旋转相机(ArcRotat…...
MCP Server多节点滚动升级一致性治理
飞书云文档原链接地址:https://ik3te1knhq.feishu.cn/wiki/W8ctwG2sAiPkrXkpl7ocP0g0njf [!TIP] MCP Server 多节点部署时,滚动发布,MCP Client 侧使用的 Client 连接保证使用的是最新的工具配置信息 后续推进:按比例使用旧、新实…...
多线程(二)
今天先来了解一个上一期的遗留概念 —— 前台线程与后台线程 一 . 前台线程与后台线程 大家应该多多少少都听过酒桌文化,咱们平常吃饭,座位次序是没有那么多讲究的,但是在跟领导吃饭,或者出席宴会和一些重要场所的饭局时&#…...
2025年,大模型LLM还有哪些可研究的方向?
近两年LLM在学术界与工业界的发展大家都有目共睹。到了今年,以预训练LLM为代表的大模型PK上半场已然结束,接下来就要进入下半场大模型2.0时代了。 那么在这新赛道,关于大模型我们还有什么可做的创新?要知道,如今的大模…...
VS打断点调试,无法命中断点或断点失效,解决方法
1.打开需要打断点的模块,点击属性,将C/C常规的调试信息格式改为程序数据库(/Zi) 2.将C/C的优化禁用(/Od) 3.将链接器中的生成调试信息改为生成调试信息(/DEBUG) 注:如果需…...
ELF文件详解
ELF 文件不仅仅是一个格式,它是 Linux 世界中程序的"灵魂容器",承载着程序从编译到执行的整个生命周期。 今天咱们来聊一个看起来高深,实际上理解起来其实挺简单的话题—— ELF 文件。 不知道你有没有想过:我们敲下./…...
【学习笔记】Shell编程---流程控制语句
最近学了好多个流程控制语句,都有点混乱了,赶紧先把各种用法记录下来! if 语句 语法格式: if 条件测试命令串 then 条件为真时执行的命令 else 条件为假时执行的命令 fi 以关键字if开头,后跟条件测试表达式&…...