Android属性系统

一、属性系统基础认知

Android属性系统是平台架构中一项关键的基础设施,它为系统各组件提供了一种轻量级、高效率的全局配置共享与状态同步机制。深入理解属性系统的格式规范、命名约定以及各类属性的行为特征,是进行系统定制、性能优化和问题诊断的重要前提。

1.1 属性格式与命名约定

Android系统中的属性采用点分字符串格式,形如 [prefix].[subsystem].[specific_name],其命名规则在视觉上类似于域名的反向层级结构。

text 复制代码
// 合法的属性名称示例
ro.product.model
persist.vendor.camera.af.mode
init.svc.zygote
sys.boot_completed
ctl.start

这种层级化的命名方式具有以下优势:

  • 命名空间隔离 :通过前缀区分属性的归属域,如 ro. 表示只读属性,persist. 表示持久化属性,vendor. 表示厂商自定义属性。
  • 可读性强:点分字符串便于开发者和调试工具直观理解属性的功能含义。
  • 权限划分基础:SELinux 安全策略可以基于属性名称的前缀模式进行细粒度的访问控制。

1.2 属性系统的核心特点

属性系统在设计上围绕全局共享和跨进程访问展开,具有以下显著特征:

  1. 全局可见性:属性值存储于由 init 进程管理的共享内存区域中,理论上系统中任意进程均具备读取任意属性值的能力。
  2. 跨语言访问支持:Android 框架为 C/C++ 原生层、Java 框架层以及 Shell 命令行环境提供了统一的属性读写 API,确保了不同运行时环境下的行为一致性。
  3. 进程运行状态感知 :众多系统守护进程和服务组件在启动及运行过程中依赖特定属性值作为条件判断依据。例如,sys.boot_completed 属性标志着系统启动流程的完成,init.svc.zygote 属性则指示 Zygote 进程的运行状态。
  4. 写入操作的集中管控:尽管属性数据存放于共享内存中,但任何对属性值的修改操作都必须经由位于 init 进程内部的"属性服务"统一受理。这一设计有效防止了多进程并发写入导致的数据不一致问题,并为 SELinux 强制访问控制提供了统一的校验入口。

1.3 属性的分类与行为特性

通过执行 getprop 命令,我们可以观察到系统中数百个属性按照特定前缀进行逻辑分组。以下对几种常见且重要的属性类别进行详细解析。

1.3.1 服务状态属性:init.svc.xxx

init.svc. 前缀下的属性用于反映由 init 进程解析 .rc 脚本所启动的各类守护服务的实时运行状态。

bash 复制代码
$ getprop | grep "init.svc"
[init.svc.vold]: [running]
[init.svc.wificond]: [running]
[init.svc.zygote]: [running]
[init.svc.zygote_secondary]: [running]
  • 含义 :属性的键名对应服务名称,值则表示该服务的当前状态,典型取值包括 runningstoppedrestarting
  • 应用场景:系统服务监控工具、CTS 测试用例以及某些需要依赖特定服务就绪后才能执行后续逻辑的组件,会通过轮询这类属性来判断服务是否已成功启动。

1.3.2 只读属性:ro.xxx

ro. 开头的属性为只读属性,其取值在系统启动的早期阶段(如内核命令行解析、build.prop 文件加载)被设定,在随后的系统运行周期内禁止被修改。

bash 复制代码
[ro.build.version.sdk]: [34]
[ro.product.manufacturer]: [Google]
[ro.kernel.version]: [6.1]
  • 不可变性 :任何尝试通过 property_set 函数或 setprop 命令修改 ro. 属性的操作都会被属性服务直接拒绝。
  • 用途:这类属性通常用于描述设备硬件信息、编译版本号、平台基线配置等不应在运行时发生变化的静态元数据。

1.3.3 持久化属性:persist.xxx

persist. 为前缀的属性具备断电不丢失的持久化能力,是属性系统中唯一能够在设备重启后依然保留其设定值的类型。

持久化原理剖析

常规属性数据仅驻留在由 ashmemtmpfs 支持的易失性共享内存中,设备掉电或重启后即被清空。而 persist. 属性的特殊之处在于:属性服务在接收到对其的写入请求后,除了更新内存中的数值外,还会同步将其键值对写入 /data/property/persistent_properties 文件中。

bash 复制代码
# 查看持久化属性文件内容
emulator_x86_64:/ $ cat /data/property/persistent_properties
persist.sys.timezoneAmerica/New_York
persist.adb.wifi.guidadb-EMULATOR34X1X1X0-M4r1Jy

系统重启时,init 进程在初始化属性服务阶段会重新解析该文件,将其中记录的持久化属性加载回共享内存,从而实现"永久保存"的效果。

注意 :这一机制也意味着,若执行恢复出厂设置或手动清除 /data/ 分区,persist. 属性的定制值将被一并抹除。

1.3.4 控制属性:ctl.startctl.stopctl.restart

ctl. 前缀的属性并不用于存储配置数据,而是作为一种特殊的进程间控制信令。当客户端向这些属性写入特定服务名称时,init 进程中的属性服务会拦截该请求,并执行相应的服务生命周期管理操作。

bash 复制代码
# 通过设置 ctl.stop 属性来停止名为 "vendor.gatekeeper-1-0" 的服务
setprop ctl.stop vendor.gatekeeper-1-0

# 通过设置 ctl.start 属性来启动该服务
setprop ctl.start vendor.gatekeeper-1-0

这种控制机制的底层流程如下图所示:

1.3.5 系统状态属性:sys.xxx

sys. 前缀涵盖了大量反映系统动态运行状态和计算结果的属性。

bash 复制代码
[sys.boot_completed]: [1]          # 系统启动流程是否已完成
[sys.boot.reason]: [reboot]        # 本次启动的原因
[sys.usb.state]: [mtp,adb]         # 当前 USB 连接模式

其中 sys.boot_completed 是一个非常重要的里程碑属性。当该属性值被设置为 1 时,标志着系统启动流程已完全结束,Launcher 已就绪。许多后台服务会监听此属性以确定何时开始执行非关键性初始化任务,避免与开机关键路径争抢系统资源。


二、核心技术原理与安全机制

Android 属性系统的设计在轻量、高效与安全之间取得了精巧的平衡。其核心架构围绕共享内存与集中式服务展开,并深度集成了 SELinux 强制访问控制机制,确保系统配置数据在多进程环境下的可靠性与安全性。

2.1 整体架构模型

属性系统采用经典的客户端-服务器 架构,其中属性服务作为唯一的写入仲裁者,所有客户端进程的写入请求均需经由该服务处理。

架构要点解读:

组件 角色 说明
客户端 属性读取方/写入请求方 包括 Shell 命令、C/C++ 程序、Java 应用等
属性服务 写入操作唯一仲裁者 运行于 init 进程内,负责接收写入请求并执行权限校验
共享内存 属性数据实际存储载体 以文件形式挂载于 /dev/__properties__,允许所有进程只读映射
持久化文件 持久化属性后备存储 /data/property/persistent_properties 保存 persist 前缀属性

2.2 共享内存存储机制

属性数据并非存放于单一连续内存块,而是根据 SELinux 安全上下文被拆分为多个独立的属性文件,统一挂载在 /dev/__properties__/ 目录下。

text 复制代码
emulator_x86_64:/ # ls -lah /dev/__properties__/                                                                             total 1.3M

-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:aac_drc_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:aaudio_config_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:ab_update_gki_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:adaptive_haptics_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:adbd_config_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:adbd_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:apex_ready_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:apexd_config_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:apexd_payload_metadata_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:apexd_prop:s0
-r--r--r--  1 root root 128K 2026-04-21 05:19 u:object_r:apexd_select_prop:s0

... ...

2.2.1 文件命名与安全上下文绑定

该目录下的每个文件,其文件名即为该文件内所有属性的 SELinux 安全上下文标签。当进程读取某个属性时,系统通过以下步骤定位数据:

  1. 查询 /dev/__properties__/property_info 元数据文件,获取目标属性对应的安全上下文。
  2. 打开以该安全上下文命名的共享内存文件。
  3. 在文件内部的数据结构(通常为字典树)中查找键值对。

2.2.2 空间限制与存储结构

Android14每个属性文件的大小被硬性限制为 128K(131072 字节)。单个文件内可容纳多条属性,属性名称与值以紧凑的树形结构存储,兼顾查找效率与空间利用率。

text 复制代码
属性名称                        →  安全上下文标签
---------------------------------------------------------
ro.build.version.sdk          →  u:object_r:system_prop:s0
persist.vendor.camera.mode    →  u:object_r:vendor_prop:s0
ctl.start                     →  u:object_r:ctl_start_prop:s0

2.3 属性服务的写入控制流程

任何对属性值的修改操作,最终都必须经过属性服务的统一仲裁。属性服务在处理写入请求时,会依据发起进程的 SELinux 上下文和目标属性的安全标签进行严格的权限判定。

2.3.1 SELinux 权限校验细则

属性访问控制依赖于 SELinux 策略中对 property_service 类的授权。以下为一个完整的策略配置示例,用于允许某个自定义守护进程写入 vendor.my.work.time 属性及操作 ctl.start 控制属性。

第一步:定义属性类型 (property.te)

selinux 复制代码
# 自定义属性类型,并继承 vendor_property_type 以通过 neverallow 检查
type vendor_my_work_prop, property_type, vendor_property_type;

第二步:绑定属性名称与类型 (property_contexts)

text 复制代码
vendor.my.work.time    u:object_r:vendor_my_work_prop:s0
ctl.start              u:object_r:ctl_start_prop:s0

第三步:授予进程域写入权限 (propertydemo.te)

selinux 复制代码
# 使用 set_prop 宏简化授权语法
set_prop(propertydemo_dt, vendor_my_work_prop)
set_prop(propertydemo_dt, ctl_start_prop)
set_prop(propertydemo_dt, ctl_stop_prop)

set_prop 宏展开后等价于:

selinux 复制代码
allow propertydemo_dt vendor_my_work_prop:property_service set;

2.3.2 不同属性类型的权限隔离

通过将不同前缀或用途的属性分配以不同的安全上下文标签,系统实现了细粒度的访问控制隔离:

属性前缀 典型安全上下文标签 权限特征
ctl.start u:object_r:ctl_start_prop:s0 仅允许特定系统组件写入,普通应用无权修改
ctl.stop u:object_r:ctl_stop_prop:s0 同上
ro. u:object_r:system_prop:s0 对全部进程只读,init 进程也禁止运行时修改
persist.sys. u:object_r:system_prop:s0 需要 system_app 或系统服务权限方可写入
vendor. u:object_r:vendor_prop:s0 厂商分区进程(vendor_file_type)可写

2.3.3 权限拒绝的审计日志

当权限校验失败时,内核 SELinux 模块会输出 AVC 拒绝记录,这是排查属性写入权限问题的一手线索。

text 复制代码
avc: denied { set } for property=vendor.my.work.time pid=2763 uid=1000 gid=1000 
scontext=u:r:propertydemo_dt:s0 tcontext=u:object_r:vendor_my_work_prop:s0 
tclass=property_service permissive=0

日志含义:

  • scontext:源上下文,即发起写入操作的进程域。
  • tcontext:目标上下文,即被写入属性的安全标签。
  • tclass:操作对象类别,属性写入固定为 property_service
  • permissive=0:表明当前处于强制模式,操作已被实际拒绝。

2.4 属性文件的来源与加载顺序

属性共享内存的初始内容由编译阶段生成的静态属性文件填充,而持久化属性则在稍后的启动阶段加载,以覆盖同名默认值。

2.4.1 静态属性文件的分布

构建系统会根据不同分区的职责将属性文件输出到相应位置:

bash 复制代码
# 在 out/target/product/<product_name>/ 目录下执行查找
$ find . -name "*.prop"
./system/build.prop
./vendor/build.prop
./product/etc/build.prop
./system_ext/etc/build.prop
./odm/etc/build.prop
./vendor_dlkm/etc/build.prop
./system_dlkm/etc/build.prop

2.4.2 加载顺序与覆盖规则

init 进程在启动第一阶段(first stage)通过 PropertyLoadBootDefaults() 函数加载静态属性文件。其加载顺序遵循 产品特定性从低到高 的原则:

cpp 复制代码
// system/core/init/property_service.cpp - 逻辑简化示意
void PropertyLoadBootDefaults() {
    std::map<std::string, std::string> properties;
    
    // 1. 基础系统属性
    load_properties_from_file("/system/build.prop", nullptr, &properties);
    load_properties_from_file("/system_ext/etc/build.prop", nullptr, &properties);
    
    // 2. 硬件抽象层及厂商定制
    load_properties_from_file("/vendor/build.prop", nullptr, &properties);
    load_properties_from_file("/vendor_dlkm/etc/build.prop", nullptr, &properties);
    
    // 3. 设备及产品特定配置(优先级最高)
    load_properties_from_file("/odm/etc/build.prop", nullptr, &properties);
    load_properties_from_file("/product/etc/build.prop", nullptr, &properties);
    
    // 4. 批量写入共享内存
    for (const auto& [name, value] : properties) {
        PropertySetNoSocket(name, value, &error);
    }
}
  • 普通属性覆盖规则: 同一键名若出现在多个文件中,后加载的值将覆盖先加载的值。加载顺序通常遵循"产品特定性从低到高"的原则(如 /system -> /vendor -> /odm -> /product),因此产品定制分区的属性可以覆盖基础系统的出厂默认值。

  • 只读属性铁律(ro.\*): 这是覆盖规则中最重要的例外。对于所有以 ro. 开头的只读属性,Android 严格遵循**"先入为主"(Write-Once)**原则。一旦某个 ro.* 属性被第一次成功加载或设置,后续生命周期内任何试图覆盖、修改它的请求都会被系统直接丢弃。

2.4.3 持久化属性(persist.\*)的加载时机

持久化属性的值保存在 /data/property/persistent_properties 文件中。它的加载时机必须延后,绝对不会在第一阶段或 Socket 刚建立时同步加载。

该文件主要采用 Protobuf 二进制序列化格式,专门存储 Android 系统中所有以 persist. 开头的持久化系统属性;这些数据涵盖了设备跨重启必须保留的关键状态与用户配置,具体类型包括:基础系统偏好(如时区、主题模式)、底层硬件与厂商定制参数(如传感器或相机校准)、系统启动与诊断历史记录、开发者调试安全凭证(如 ADB 配对码),以及核心网络与通信开关等底层"记忆"信息。

完整的加载链路如下:

  1. 服务初始化: 在 Init 进程的第二阶段 (Second stage) 初期,系统调用 StartPropertyService() 建立属性服务的 UNIX Domain Socket 监听。此时,持久化属性尚未加载
  2. 等待分区挂载: Init 引擎开始解析并执行 init.rc 脚本。需要注意的是,第一阶段仅负责挂载核心的只读分区,而 /data 分区(通常涉及 FBE 文件级加密)必须依赖第二阶段的 vold 守护进程。
  3. 触发安全加载:init.rc 顺序执行到 on post-fs-data 触发器时,意味着 /data 分区已成功挂载并解密完毕。此时,系统会触发内置动作 load_persist_props。属性服务这才去读取文件,确保用户态修改过的持久化配置能够安全、合规地(经过 SELinux 权限校验)载入到共享内存中。

2.5 跨分区访问限制与安全边界

Android 通过 SELinux 的 neverallow 规则对属性的跨域访问施加了严格的编译期与运行期约束。以下核心规则禁止普通域直接对底层属性共享内存文件进行违规访问:

selinux 复制代码
# system/sepolicy/private/property.te
neverallow domain {
    property_type
    -system_property_type
    -product_property_type
    -vendor_property_type
}:file no_rw_file_perms;

规则解读: 强制所有属性必须规范地归类到 system、product 或 vendor 这三大体系中,否则禁止任何普通域以读写方式访问其底层物理文件。

更关键的是,针对属性的动态设置(Set),Project Treble 架构引入了极其严格的 Socket 访问阻断机制:

text 复制代码
# 禁止非核心域(如 Vendor)设置系统属性
neverallow { domain -coredomain } { system_property_type }:property_service set;

2.5.1 跨分区设置属性的典型问题与标准解决方案

场景假设:

一个位于 Vendor 分区的原生守护进程,因为业务需要,必须修改 System 分区下的持久化属性 persist.sys.debug。如果直接在 Vendor 进程代码中调用 setprop persist.sys.debug 1,请求会被 System 端的属性服务拦截,内核报出 avc: denied 权限拒绝错误。

推荐的标准解决方案:基于系统域跳板的跨域触发器(只是一个思路,有待验证)

由于 Vendor 域无法绕过限制,我们必须采用基于 init.rc 的"发布-订阅"模式,借助运行在 System 核心域的顶级 init 进程作为高权限跳板:

步骤 1:Vendor 进程仅负责广播意图

Vendor 原生进程不需要直接去写系统属性,而是修改自己拥有完全权限的 Vendor 专属属性(相当于发送一个事件广播):

cpp 复制代码
// Vendor C++ 进程内部
property_set("vendor.debug.request", "1");

步骤 2:在 System 分区配置高权限监听者

必须在 System 分区System_ext 分区 的初始化脚本中定义触发器。因为只有存放在这些核心目录下的脚本,才是由具有最高权限的根 init 进程(coredomain)来执行的。

text 复制代码
# 正确路径:/system_ext/etc/init/system_debug_trigger.rc
# 由顶级 init 进程持续监听该 Vendor 属性的变化
on property:vendor.debug.request=1
    # 借助 init 进程的高权限上下文,代为完成系统属性的写入
    setprop persist.sys.debug 1

三、跨语言API调用接口

Android 属性系统为不同运行时环境提供了统一的编程接口,涵盖 C 语言原生层、C++ 抽象层以及 Java 框架层。尽管各层 API 的语法风格各异,但其底层均通过相同的进程间通信机制与属性服务交互。

3.1 API 层次架构概览

下图展示了三种语言 API 的调用路径及其与属性服务的衔接关系:

层次说明:

层级 所在库/包 适用场景
Java API android.os.SystemProperties 系统服务、系统级应用(非普通 App)
JNI 桥接 android_os_SystemProperties.cpp 连接 Java 层与原生层
C++ API libbase (android-base/properties.h) 原生守护进程、Hal 服务(推荐)
C API libcutils (cutils/properties.h) 传统原生代码、引导程序
底层通信 libc 系统调用封装 实际执行共享内存读取或 Socket 发送

3.2 C 语言 API(libcutils)

C 语言接口是最底层的公开 API,定义于 system/core/libcutils/include/cutils/properties.h,实现位于 properties.cpp。其设计遵循 POSIX 风格,以零结尾字符串传递键名与值。

3.2.1 头文件与函数原型

c 复制代码
#include <cutils/properties.h>

// 读取属性值,返回读取到的字符串长度
int property_get(const char *key, char *value, const char *default_value);

// 设置属性值,成功返回 0,失败返回 -1
int property_set(const char *key, const char *value);

// 便捷类型转换函数
int8_t  property_get_bool(const char *key, int8_t default_value);
int32_t property_get_int32(const char *key, int32_t default_value);
int64_t property_get_int64(const char *key, int64_t default_value);

参数说明:

  • key:属性名称,如 "ro.build.version.sdk"
  • value:输出缓冲区指针,用于接收属性值。
  • default_value:当属性不存在或为空时返回的默认值。
  • 返回值:property_get 返回实际读取的字符串长度(不含结尾 \0);若属性不存在,则将 default_value 复制到 value 并返回其长度。

3.2.2 使用示例

c 复制代码
#include <cutils/properties.h>
#include <stdio.h>

int main() {
    char sdk_version[PROP_VALUE_MAX] = {0};
    
    // 读取 Android SDK 版本号
    int len = property_get("ro.build.version.sdk", sdk_version, "unknown");
    printf("SDK Version: %s (len=%d)\n", sdk_version, len);
    
    // 使用类型转换函数直接获取整数值
    int32_t sdk_int = property_get_int32("ro.build.version.sdk", -1);
    printf("SDK Int: %d\n", sdk_int);
    
    // 设置自定义属性(需要相应 SELinux 权限)
    if (property_set("vendor.my.work.time", "8") == 0) {
        printf("Property set successfully.\n");
    } else {
        printf("Failed to set property.\n");
    }
    
    return 0;
}

注意要点:

  • PROP_VALUE_MAX 宏定义为 92,表示属性值最大长度(含结尾 \0 为 92 字节)。
  • property_set 通过 Socket 与 init 进程中的属性服务通信,可能因 SELinux 权限不足而失败。

3.3 C++ 语言 API(libbase)

现代 Android 原生开发推荐使用 libbase 库提供的 C++ 属性接口。该接口位于 system/libbase/include/android-base/properties.h,通过 std::string 和模板函数提供了更符合 C++ 习惯的类型安全封装。

3.3.1 头文件与函数原型

cpp 复制代码
#include <android-base/properties.h>

namespace android {
namespace base {

// 读取字符串属性
std::string GetProperty(const std::string& key, const std::string& default_value);

// 读取布尔型属性
bool GetBoolProperty(const std::string& key, bool default_value);

// 读取整数型属性(带范围限制)
template <typename T>
T GetIntProperty(const std::string& key,
                 T default_value,
                 T min = std::numeric_limits<T>::min(),
                 T max = std::numeric_limits<T>::max());

// 读取无符号整数型属性
template <typename T>
T GetUintProperty(const std::string& key,
                  T default_value,
                  T max = std::numeric_limits<T>::max());

// 设置属性
bool SetProperty(const std::string& key, const std::string& value);

} // namespace base
} // namespace android

3.3.2 使用示例

cpp 复制代码
#include <android-base/properties.h>
#include <android-base/logging.h>

using android::base::GetProperty;
using android::base::GetBoolProperty;
using android::base::GetIntProperty;
using android::base::SetProperty;

int main() {
    // 读取字符串属性
    std::string model = GetProperty("ro.product.model", "unknown");
    LOG(INFO) << "Product Model: " << model;
    
    // 读取布尔型属性
    bool boot_completed = GetBoolProperty("sys.boot_completed", false);
    LOG(INFO) << "Boot Completed: " << std::boolalpha << boot_completed;
    
    // 读取整数属性,限制取值范围 [0, 100]
    int brightness = GetIntProperty<int>("persist.sys.brightness", 50, 0, 100);
    LOG(INFO) << "Brightness: " << brightness;
    
    // 设置属性
    if (SetProperty("vendor.my.work.time", "8")) {
        LOG(INFO) << "Property set successfully.";
    } else {
        LOG(ERROR) << "Failed to set property.";
    }
    
    return 0;
}

与 C API 的对比优势:

  • 返回值直接为 std::string,无需手动管理缓冲区。
  • 模板函数 GetIntProperty 自动进行类型转换和范围裁剪,减少错误处理代码。
  • SetProperty 返回 bool 直观表示成功与否。

3.3.3 编译依赖配置

Android.bp 中添加对 libbase 的依赖:

blueprint 复制代码
cc_binary {
    name: "property_demo_cpp",
    srcs: ["main.cpp"],
    shared_libs: [
        "libbase",
        "liblog",
    ],
}

3.4 Java 系统级 API(android.os.SystemProperties)

Java 层的属性操作由 android.os.SystemProperties 类提供。该接口专供系统级程序使用,不向普通 App 开发者开放。

3.4.1 类定义与方法签名

java 复制代码
// frameworks/base/core/java/android/os/SystemProperties.java
package android.os;

import android.annotation.SystemApi;
import android.annotation.NonNull;
import android.annotation.Nullable;

public class SystemProperties {
    /**
     * Get the String value for the given key.
     * @hide
     */
    @SystemApi
    @NonNull
    public static String get(@NonNull String key, @Nullable String def) {
        // ...
        return native_get(key, def);
    }

    /**
     * Get the int value for the given key.
     * @hide
     */
    public static int getInt(@NonNull String key, int def) {
        // ...
        return native_get_int(key, def);
    }

    /**
     * Get the long value for the given key.
     * @hide
     */
    public static long getLong(@NonNull String key, long def) {
        // ...
        return native_get_long(key, def);
    }

    /**
     * Get the boolean value for the given key.
     * @hide
     */
    public static boolean getBoolean(@NonNull String key, boolean def) {
        // ...
        return native_get_boolean(key, def);
    }

    /**
     * Set the value for the given key.
     * @hide
     */
    public static void set(@NonNull String key, @Nullable String val) {
        // ...
        native_set(key, val);
    }

    // Native 方法声明
    private static native String native_get(String key, String def);
    private static native int native_get_int(String key, int def);
    private static native long native_get_long(String key, long def);
    private static native boolean native_get_boolean(String key, boolean def);
    private static native void native_set(String key, String def);
}

3.4.2 注解含义与访问限制

注解 含义 影响
@SystemApi 系统 API,仅供系统组件调用 普通第三方应用无法通过编译时引用
@hide 对 SDK 隐藏,不出现在公开文档中 即使通过反射调用,也可能在 CTS 测试中被视为违规

工程实践中的使用方式:

  • 系统服务(如 SystemServerActivityManagerService)可直接导入 android.os.SystemProperties 并调用其方法。
  • 若需在 AOSP 源码树内开发的系统级应用中使用,可在 Android.mk 或 Android.bp 中声明 LOCAL_PRIVATE_PLATFORM_APIS := true

3.4.3 使用示例(系统级应用场景)

java 复制代码
import android.os.SystemProperties;
import android.util.Log;

public class SystemConfigHelper {
    private static final String TAG = "SystemConfigHelper";
    
    public static void applyCustomSettings() {
        // 读取只读属性
        String sdkVersion = SystemProperties.get("ro.build.version.sdk", "0");
        Log.d(TAG, "Current SDK Version: " + sdkVersion);
        
        // 读取布尔属性
        boolean isDebuggable = SystemProperties.getBoolean("ro.debuggable", false);
        Log.d(TAG, "Debuggable: " + isDebuggable);
        
        // 设置自定义属性(需要相应 SELinux 权限)
        SystemProperties.set("vendor.my.work.time", "8");
    }
}

3.5 JNI 桥接层简析

Java 层的 native_getnative_set 方法最终通过 JNI 调用到原生代码。关键实现位于 frameworks/base/core/jni/android_os_SystemProperties.cpp

cpp 复制代码
// 简化逻辑示意
jstring SystemProperties_getSS(JNIEnv* env, jclass clazz, jstring keyJ,
                               jstring defJ)
{
    jstring ret = defJ;
    ReadProperty(env, keyJ, [&](const char* value) {
        if (value[0]) {
            ret = env->NewStringUTF(value);
        }
    });
    if (ret == nullptr && !env->ExceptionCheck()) {
      ret = env->NewStringUTF("");  // Legacy behavior
    }
    return ret;
}


void SystemProperties_set(JNIEnv *env, jobject clazz, jstring keyJ,
                          jstring valJ)
{
    ScopedUtfChars key(env, keyJ);
    if (!key.c_str()) {
        return;
    }
    std::optional<ScopedUtfChars> value;
    if (valJ != nullptr) {
        value.emplace(env, valJ);
        if (!value->c_str()) {
            return;
        }
    }
    bool success;
#if defined(__BIONIC__)
    success = !__system_property_set(key.c_str(), value ? value->c_str() : "");
#else
    success = android::base::SetProperty(key.c_str(), value ? value->c_str() : "");
#endif
    if (!success) {
        jniThrowException(env, "java/lang/RuntimeException",
                          "failed to set system property (check logcat for reason)");
    }
}

可见,Java 层的 getset 操作本质上是对 C 语言 property_getproperty_set 的薄封装,其行为与原生调用完全一致。

3.6 跨语言调用路径对比

关键结论:

  • 无论使用哪一层 API,读取操作最终均绕过属性服务,直接从共享内存获取数据,因此效率极高且无需额外权限。
  • 写入操作则统一通过 Socket 提交至属性服务,受 SELinux 策略严格约束。

四、属性文件的编译生成与产品定制

属性系统运行所需的基础数据源自源码编译阶段生成的属性文件,这些文件随系统镜像烧录至设备各分区。理解属性文件的生成机制与定制方法,是进行产品差异化配置、性能调优以及系统裁剪的核心技能。

4.1 属性文件的来源与分布

Android 构建系统会将散布于源码树中的属性定义聚合并输出为若干以 .prop 结尾的属性文件,随后根据功能归属放置到不同的分区镜像中。

4.1.1 属性文件的物理分布

out/target/product/<product_name>/ 编译输出目录下执行查找,可观察到属性文件广泛分布于多个子目录中:

bash 复制代码
$ find out/target/product/emulator_x86_64 -name "*.prop" | head -15
./ramdisk/system/etc/ramdisk/build.prop
./debug_ramdisk/adb_debug.prop
./odm/etc/build.prop
./system_dlkm/etc/build.prop
./odm_dlkm/etc/build.prop
./recovery/root/default.prop
./system/build.prop
./vendor_dlkm/etc/build.prop
./product/etc/build.prop
./vendor/build.prop
./system_ext/etc/build.prop

各分区属性文件的作用域如下表所示:

文件路径 所属分区 作用域说明
/system/build.prop system 系统核心框架属性,Android 平台基线配置
/system_ext/etc/build.prop system_ext 系统扩展功能属性,AOSP 之外的扩展组件
/vendor/build.prop vendor 硬件抽象层(HAL)及 SoC 相关属性
/vendor_dlkm/etc/build.prop vendor_dlkm 厂商可加载内核模块相关属性
/product/etc/build.prop product 产品级差异化配置(品牌、型号、功能开关)
/odm/etc/build.prop odm 原始设计制造商(ODM)定制属性
/odm_dlkm/etc/build.prop odm_dlkm ODM 可加载内核模块属性
/default.prop ramdisk 早期启动阶段的默认属性,包含 ro.debuggable 等安全关键项

4.1.2 属性文件的内部格式

/system/build.prop 为例,其内容为纯文本键值对,并包含注释行指明属性的生成来源:

text 复制代码
# begin build properties
# autogenerated by buildinfo.sh
ro.build.id=TD1A.220804.031
ro.build.version.sdk=34
ro.build.version.release=14
ro.product.brand=Android
ro.product.manufacturer=Google
# end build properties

# from variable ADDITIONAL_SYSTEM_PROPERTIES
ro.actionable_compatible_property.enabled=true
debug.atrace.tags.enableflags=0

# from variable PRODUCT_SYSTEM_PROPERTIES
ro.product.first_api_level=31

4.2 属性文件的生成机制

属性文件并非手工编写,而是由构建系统通过聚合多种变量来源自动生成。下图展示了从源码定义到最终属性文件输出的完整流程:

核心聚合变量说明:

变量名称 作用 最终去向
PRODUCT_SYSTEM_PROPERTIES 产品级 system 分区属性 /system/build.prop
PRODUCT_SYSTEM_DEFAULT_PROPERTIES system 分区默认属性 /system/etc/prop.default
PRODUCT_VENDOR_PROPERTIES 厂商分区属性 /vendor/build.prop
PRODUCT_PRODUCT_PROPERTIES 产品分区属性 /product/etc/build.prop
PRODUCT_ODM_PROPERTIES ODM 分区属性 /odm/etc/build.prop
ADDITIONAL_SYSTEM_PROPERTIES 附加 system 属性 /system/build.prop
ADDITIONAL_VENDOR_PROPERTIES 附加 vendor 属性 /vendor/build.prop
PRODUCT_PROPERTY_OVERRIDES 通用属性覆盖(向后兼容) 根据 BOARD_PROPERTY_OVERRIDES_SPLIT_ENABLED 决定去向

4.3 属性定制方法

Android 提供了多层次的属性定制手段,开发者可根据属性的作用范围和分区归属选择合适的方式。

4.3.1 方法一:通过 device 目录下的 .prop 文件定制(推荐)

这是最规范、最便于维护的定制方式。在 device/<vendor>/<product>/ 目录下创建与分区对应的属性文件,构建系统会自动将其内容合并到对应分区的属性文件中。

文件命名对应关系:

源文件(位于 device/ 目录) 目标属性文件
system.prop /system/build.prop
vendor.prop /vendor/build.prop
product.prop /product/etc/build.prop
odm.prop /odm/etc/build.prop
system_ext.prop /system_ext/etc/build.prop

示例:device/linaro/hikey/system.prop

text 复制代码
# This overrides settings in the products/generic/system.prop file
#
# rild.libpath=/system/lib/libreference-ril.so
# rild.libargs=-d /dev/ttyS0

在产品的 device.mk 文件中,无需额外配置,构建系统会自动扫描并处理这些 .prop 文件。

4.3.2 方法二:通过 Makefile 变量追加属性

device.mkproduct.mk 中直接向构建系统变量追加属性定义,适用于少量属性的快速添加。

makefile 复制代码
# device/google/cuttlefish/vsoc_x86_64/device.mk

# 使用分区专属变量(推荐)
PRODUCT_SYSTEM_PROPERTIES += \
    ro.hardware.egl=swiftshader \
    ro.kernel.qemu=1

PRODUCT_VENDOR_PROPERTIES += \
    ro.vendor.perf.scroll_opt=true

# 使用附加变量
ADDITIONAL_SYSTEM_PROPERTIES += \
    ro.actionable_compatible_property.enabled=true

ADDITIONAL_VENDOR_PROPERTIES += \
    vendor.display.lbe_density=320

4.3.3 方法三:通过 PRODUCT_PROPERTY_OVERRIDES 全局覆盖

PRODUCT_PROPERTY_OVERRIDES 是一个历史悠久的变量,用于向后兼容地覆盖属性。

在现代 Android 编译体系中,为了严格区分分区,更推荐直接使用:

  • PRODUCT_VENDOR_PROPERTIES
  • PRODUCT_PRODUCT_PROPERTIES
  • PRODUCT_SYSTEM_PROPERTIES

使用示例(在 device.mk 中):

makefile 复制代码
PRODUCT_VENDOR_PROPERTIES += ro.soc.manufacturer=Qualcomm
PRODUCT_VENDOR_PROPERTIES += ro.soc.model=SM7150

注意 :在新版本 Android 中,更推荐使用方法一(.prop 文件)或方法二(分区专属变量),因为它们的分区归属更加明确,且符合 Treble 架构对接口边界的严格划分。

4.3.4 方法四:通过编译脚本变量定义

部分属性来源于构建系统内置脚本。例如,build/make/tools/buildinfo.sh 脚本会根据当前构建环境动态生成以下属性:

bash 复制代码
# buildinfo.sh 片段
echo "ro.build.id=$BUILD_ID"
echo "ro.build.version.sdk=$PLATFORM_SDK_VERSION"
echo "ro.build.version.release=$PLATFORM_VERSION"
echo "ro.build.date=`date`"
echo "ro.build.type=$TARGET_BUILD_TYPE"

这些由构建环境决定的属性一般不适宜直接手动修改,但可以通过调整构建环境变量间接影响其取值。

4.4 属性覆盖优先级

当同一属性在多个来源中被定义时,后加载的值将覆盖先加载的值。构建系统在聚合属性时的处理顺序决定了最终的生效取值。**(除了ro.只读属性,这个是有最开始确定的)*

这就不细讲了,在第二部分有详细说明。

4.5 验证属性定制结果

直接查看生成的属性文件内容:

bash 复制代码
# 查看 system/build.prop 中是否包含自定义属性
$ cat out/target/product/emulator_x86_64/system/build.prop | grep "my.custom"
ro.my.custom.property=hello_world

4.5.2 运行时检查

将镜像烧录至设备后,通过 getprop 命令验证:

bash 复制代码
$ adb shell getprop ro.my.custom.property
hello_world

4.5.3 确认属性的 SELinux 上下文

若自定义属性涉及写入操作,还需确认其 SELinux 上下文配置正确:

bash 复制代码
$ adb shell getprop -Z | grep "my.custom"
[ro.my.custom.property]: [u:object_r:system_prop:s0]

4.6 定制实践:完整示例

以下示例演示如何为 emulator_x86_64 产品添加一个位于 vendor 分区的自定义属性,并使其在运行时可通过原生程序写入。

4.6.1 写一个c++实战接口代码

cpp 复制代码
#include <cutils/properties.h>  // 引入 Android 属性 API
#include <string>

#define LOG_TAG "PropertyDemo"
#include <log/log.h>

// 获取属性
void getSystemProperty(const std::string& property) {
    char value[PROP_VALUE_MAX];
    if (property_get(property.c_str(), value, "") > 0) {
        ALOGD("Property Value for %s: %s", property.c_str(), value);
    } else {
        ALOGE("Failed to get property: %s", property.c_str());
    }
}

// 设置属性
void setSystemProperty(const std::string& property, const std::string& value) {
    if (property_set(property.c_str(), value.c_str()) == 0) {
        ALOGD("Successfully set %s to %s", property.c_str(), value.c_str());
    } else {
        ALOGE("Failed to set property: %s", property.c_str());
    }
}

int main() {
    // 获取系统属性
    ALOGD("Getting system property 'ro.build.version.release':");
    getSystemProperty("ro.build.version.release");

    // 设置系统属性
    // ALOGD("Setting system property 'persist.sys.debug' to '1'");
    // setSystemProperty("persist.sys.debug", "1");

     setSystemProperty("vendor.my.work.time", "8");


    // 停止某个服务
    ALOGD("Stopping  service...");
    setSystemProperty("ctl.stop", "vendor.gatekeeper-1-0");

    // 启动某个服务
    ALOGD("Starting service...");
    setSystemProperty("ctl.start", "vendor.gatekeeper-1-0");

    while(1){
        usleep(1000*1000);
        ALOGD("sleep for one second.");
    }
    return 0;
}

4.6.2 Android.bp编译脚本编写:

blueprint 复制代码
cc_binary {
    name: "propertydemo",  // 模块名称
    srcs: [
        "main.cpp",               // 源代码文件
    ],
    shared_libs: [
        "liblog",                 // 依赖 Android 日志库
                "libcutils",      //依赖属性接口
    ],
    cflags: [
        "-Wall",                  // 编译时启用所有警告
        "-std=c++11",             // 使用 C++11 标准
    ],
        vendor: true,
}

4.6.3 sepolicy内容编写

text 复制代码
file_contexts  property_contexts  propertydemo.te  property.te
file_contexts: 应用程序安全上下文标签描述
property_contexts :自定义属性上下文标签描述
propertydemo.te : 应用程序selinux策略
property.te: 自定义属性类型
4.6.3.1 file_contexts 编写
text 复制代码
/vendor/bin/propertydemo        u:object_r:propertydemo_dt_exec:s0
4.6.3.2 property_contexts 编写:
text 复制代码
#first version

#second version
#vendor.my.work.time    u:object_r:vendor_my_work_prop:s0
4.6.3.3. propertydemo.te编写
text 复制代码
type propertydemo_dt , domain;
type propertydemo_dt_exec, exec_type, vendor_file_type,file_type;

init_daemon_domain(propertydemo_dt)

domain_auto_trans(shell, propertydemo_dt_exec, propertydemo_dt)


#这个后面再打开
#set_prop(propertydemo_dt , vendor_my_work_prop);
#set_prop(propertydemo_dt , ctl_default_prop);
#set_prop(propertydemo_dt , ctl_start_prop);
#set_prop(propertydemo_dt , ctl_stop_prop);
4.6.3.4 property.te编写
text 复制代码
#first version

#second version
#type vendor_my_work_prop, property_type;

#third version
#type vendor_my_work_prop, property_type, vendor_property_type;
4.6.3.5 添加到编译脚本中

build/make/target/board/emulator_x86_64/BoardConfig.mk,需要在这个文件中配置sepolicy所在的路径,这样系统可以将其加入编译到selinux_policy.

makefile 复制代码
BOARD_SEPOLICY_DIRS += device/hello/propertydemo/sepolicy  

五、SELinux规则编译与Neverallow错误解决

在Android属性系统的实战开发中,SELinux权限配置是最容易出现编译与运行时错误的环节。本节基于文档中的完整排错过程,系统阐述neverallow编译错误与运行时权限拒绝两类典型问题的分析方法及解决方案。

5.1 错误场景总览

当开发一个位于vendor分区的可执行程序propertydemo,并向自定义属性vendor.my.work.time及控制属性ctl.start/ctl.stop进行写入操作时,若未正确配置SELinux策略,将依次遭遇以下两类错误:

阶段 错误类型 表现
编译期 neverallow 违规 编译失败,提示neverallow ... violated by allow ...
运行期 权限拒绝 Permission deniedUnable to set property

5.2 编译期错误:neverallow 冲突分析

5.2.1 错误日志解读

在添加自定义SELinux策略后进行编译,系统抛出如下错误:

text 复制代码
libsepol.report_failure: neverallow on line 64 of system/sepolicy/private/property.te 
violated by allow propertydemo_dt vendor_my_work_prop:file { read open };

该日志明确指示:

  • 违规规则位置:system/sepolicy/private/property.te 第64行附近的 neverallow 语句。
  • 冲突主体:propertydemo_dt(自定义进程域)试图对 vendor_my_work_prop(自定义属性类型)执行 readopen 操作。
  • 违规性质:neverallow 是AOSP策略中绝对禁止的操作组合,即便在自定义.te文件中显式allow,也会在编译阶段被拦截。

5.2.2 定位冲突规则

查看 system/sepolicy/private/property.te 中对应的 neverallow 规则:

selinux 复制代码
# system/sepolicy/private/property.te
neverallow domain {
    property_type
    -system_property_type
    -product_property_type
    -vendor_property_type
}:file no_rw_file_perms;

规则语义分析

  • 禁止任意 domainproperty_type 类型的文件执行读写操作。
  • 例外项(以 - 前缀标识)包括:system_property_typeproduct_property_typevendor_property_type
  • 即:只有继承了上述三种特定属性类型的子类型,才允许被普通域访问。

5.2.3 根本原因

在初始版本中,自定义属性类型仅声明为 property_type

selinux 复制代码
# property.te (Version 2)
type vendor_my_work_prop, property_type;

该类型并未继承 vendor_property_type 这一例外属性,因此落入 neverallow 的禁止范围,触发编译错误。

5.2.4 解决方案

修改 property.te,使自定义属性类型同时继承 property_typevendor_property_type

selinux 复制代码
# property.te (Version 3) ------ 正确版本
type vendor_my_work_prop, property_type, vendor_property_type;

原理vendor_property_typeneverallow 规则中的例外类型之一,继承它后,自定义属性即获得合法访问的"通行证"。

5.3 运行时错误:Permission denied 排查

5.3.1 错误日志分析

完成编译并刷入镜像后,运行 propertydemo 程序,logcat 输出如下:

text 复制代码
12-16 02:25:16.037  2763  2763 W libc    : Unable to set property "persist.sys.debug" to "1": connection failed: Permission denied
12-16 02:25:16.037  2763  2763 E SystemControlDemo: Failed to set property: persist.sys.debug
12-16 02:25:16.037  2763  2763 W libc    : Unable to set property "vendor.my.work.time" to "8": connection failed: Permission denied
12-16 02:25:16.037  2763  2763 E SystemControlDemo: Failed to set property: vendor.my.work.time
12-16 02:25:16.037  2763  2763 W libc    : Unable to set property "ctl.stop" to "vendor.gatekeeper_default": connection failed: Permission denied
12-16 02:25:16.037  2763  2763 W libc    : Unable to set property "ctl.start" to "vendor.gatekeeper_default": connection failed: Permission denied

关键信息

  • 所有写入操作均返回 Permission denied
  • 受影响属性包括:persist.sys.debug(系统属性)、vendor.my.work.time(自定义vendor属性)、ctl.stop/ctl.start(控制属性)。

5.3.2 定位属性安全上下文

要解决权限问题,首先需要明确目标属性对应的SELinux安全上下文标签。

步骤1:查询自定义属性上下文

bash 复制代码
emulator_x86_64:/ # getprop -Z | grep "vendor.my.work.time"
[vendor.my.work.time]: [u:object_r:vendor_my_work_prop:s0]

步骤2:查看控制属性对应的安全上下文文件

bash 复制代码
emulator_x86_64:/dev/__properties__ # ls -la | grep "ctl"
-r--r--r-- 1 root root 1048576 2024-12-16 02:10 u:object_r:ctl_default_prop:s0
-r--r--r-- 1 root root 1048576 2024-12-16 02:10 u:object_r:ctl_start_prop:s0
-r--r--r-- 1 root root 1048576 2024-12-16 02:10 u:object_r:ctl_stop_prop:s0

由此确定,程序需要获得以下属性类型的写入权限:

属性名称 安全上下文标签
vendor.my.work.time vendor_my_work_prop
ctl.start / ctl.stop ctl_start_prop / ctl_stop_prop

5.3.3 添加进程域权限策略

propertydemo.te 文件中,使用 set_prop 宏为进程域授予对上述属性类型的 set 权限:

selinux 复制代码
# propertydemo.te
set_prop(propertydemo_dt, vendor_my_work_prop);
set_prop(propertydemo_dt, ctl_default_prop);
set_prop(propertydemo_dt, ctl_start_prop);
set_prop(propertydemo_dt, ctl_stop_prop);

set_prop 宏展开后等价于:

selinux 复制代码
allow propertydemo_dt vendor_my_work_prop:property_service set;
allow propertydemo_dt ctl_start_prop:property_service set;
allow propertydemo_dt ctl_stop_prop:property_service set;

5.3.4 验证结果

重新编译并推送策略文件后,运行程序得到正确输出:

text 复制代码
12-16 04:32:29.594  2595  2595 D SystemControlDemo: Property Value for ro.build.version.release: 14
12-16 04:32:29.596  2595  2595 D SystemControlDemo: Successfully set vendor.my.work.time to 8
12-16 04:32:29.600  2595  2595 D SystemControlDemo: Successfully set ctl.stop to vendor.gatekeeper_default
12-16 04:32:29.838     0     0 I init    : Control message: Processed ctl.stop for 'vendor.gatekeeper_default' from pid: 2595
12-16 04:32:29.842     0     0 I init    : starting service 'vendor.gatekeeper_default'...
12-16 04:32:29.629  2595  2595 D SystemControlDemo: Successfully set ctl.start to vendor.gatekeeper_default

属性写入与服务控制均成功执行,SELinux权限问题完全解决。

5.4 思维扩展:跨分区属性访问的替代方案

在实际工程中,一个vendor分区的进程如需设置system分区下的持久化属性(如 persist.sys.debug),直接授予vendor域对system属性类型的写入权限会触发更广泛的 neverallow 限制,且违反Treble架构的接口隔离原则。

推荐做法 :通过 init.rc 中的属性触发器间接完成。

init 复制代码
# 在 vendor.rc 或 product.rc 中定义
on property:vendor.sys.request_debug=1
    setprop persist.sys.debug 1

dor_my_work_prop:property_service set;

allow propertydemo_dt ctl_start_prop:property_service set;

allow propertydemo_dt ctl_stop_prop:property_service set;

复制代码
### 5.3.4 验证结果

重新编译并推送策略文件后,运行程序得到正确输出:

```text
12-16 04:32:29.594  2595  2595 D SystemControlDemo: Property Value for ro.build.version.release: 14
12-16 04:32:29.596  2595  2595 D SystemControlDemo: Successfully set vendor.my.work.time to 8
12-16 04:32:29.600  2595  2595 D SystemControlDemo: Successfully set ctl.stop to vendor.gatekeeper_default
12-16 04:32:29.838     0     0 I init    : Control message: Processed ctl.stop for 'vendor.gatekeeper_default' from pid: 2595
12-16 04:32:29.842     0     0 I init    : starting service 'vendor.gatekeeper_default'...
12-16 04:32:29.629  2595  2595 D SystemControlDemo: Successfully set ctl.start to vendor.gatekeeper_default

属性写入与服务控制均成功执行,SELinux权限问题完全解决。

5.4 思维扩展:跨分区属性访问的替代方案

在实际工程中,一个vendor分区的进程如需设置system分区下的持久化属性(如 persist.sys.debug),直接授予vendor域对system属性类型的写入权限会触发更广泛的 neverallow 限制,且违反Treble架构的接口隔离原则。

推荐做法 :通过 init.rc 中的属性触发器间接完成。

init 复制代码
# 在 vendor.rc 或 product.rc 中定义
on property:vendor.sys.request_debug=1
    setprop persist.sys.debug 1

Vendor进程仅需设置 vendor.sys.request_debug 属性(属于vendor分区类型),init进程在接收到属性变更事件后,以其自身的高权限(system域)代为执行目标属性的写入操作。这一机制既规避了SELinux策略冲突,也维护了分区间的边界清晰性。

相关推荐
明天就是Friday2 小时前
Android实战项目③ Room+Clean Architecture开发待办事项App 完整源码详解
android
没有了遇见2 小时前
《彻底搞懂 ViewModel:作用、原理与源码分析》
android
Fate_I_C2 小时前
Kotlin 协程:串行/并行请求、async/await、coroutineScope 管理并发、重试机制
android·代码规范
山河梧念3 小时前
【保姆级教程】VMware虚拟机安装全流程
android·java·数据库
常利兵3 小时前
Kotlin类型魔法:Any、Unit、Nothing 深度探秘
android·开发语言·kotlin
y小花3 小时前
安卓vold服务
android·linux·运维
明天就是Friday3 小时前
Android实战项目⑤ Paging 3开发社交媒体信息流App 完整源码详解
android·媒体
宋拾壹3 小时前
php网站小程序接入抖音团购核销
android·小程序·php
莫逸风4 小时前
【java-core-collections】B+ 树深度解析
android·java·开发语言