
1. 项目概述为什么我们需要获取Android设备信息在Android开发中获取设备信息是一个看似基础实则贯穿应用开发、测试、运营全生命周期的核心技能。无论是为了适配不同屏幕尺寸、针对特定系统版本做功能兼容、统计用户设备分布以指导产品决策还是为了排查线上用户反馈的“在我手机上就闪退”这类玄学问题都离不开对设备信息的精准掌握。这个项目标题——“Android 获取手机设备信息名称、型号、系统版本号、厂商、设备制造商、SDK版本、系统语言等等”——几乎涵盖了开发者日常所需的所有基础设备维度。它不是一个炫技的功能而是一个开发者的“基本功”。但恰恰是这个基本功里面藏着不少门道哪些信息可以直接获取哪些需要权限不同Android版本间API有何差异获取到的信息是否可靠如何避免因厂商定制如小米的MIUI、华为的EMUI导致的奇葩问题我见过不少新手开发者直接照搬网上的Build类代码结果在线上环境遇到了各种兼容性问题。今天我就结合自己多年的踩坑经验把这套“基本功”掰开揉碎了讲清楚从核心原理到代码实现再到避坑指南让你不仅能写出代码更能理解背后的逻辑写出健壮、可靠的设备信息获取模块。2. 核心信息获取渠道与API深度解析获取设备信息主要依赖于Android SDK提供的几个核心类android.os.Build、android.provider.Settings.System、java.util.Locale以及通过PackageManager获取的上下文信息。它们各有分工也各有“脾气”。2.1android.os.Build硬件与系统构建信息Build类是最常用、最直接的信息源它提供的是设备在出厂时就确定的、相对静态的信息大部分无需任何权限。但请注意这里的“静态”是相对的有些信息可能被厂商或用户修改。核心字段详解Build.MODEL(设备型号): 这是用户通常在手机设置-关于手机里看到的型号名称例如 “SM-G998B” (三星S21 Ultra) 或 “Mi 10”。注意这个字段的格式完全由厂商决定可能包含空格、横杠等特殊字符甚至同一型号在不同地区发售的版本其MODEL值也可能不同。在做精确匹配时要格外小心。Build.MANUFACTURER(设备制造商): 生产厂商的标识如 “samsung”, “xiaomi”, “huawei”。这个值通常比较规范是进行厂商特定逻辑判断如处理不同厂商的推送服务的关键依据。Build.BRAND(品牌): 产品的品牌有时和MANUFACTURER相同有时不同。例如一款谷歌Pixel手机BRAND可能是 “google”而MANUFACTURER可能是 “Google” 或代工厂商。BRAND更偏向于市场品牌。Build.DEVICE(设备代号): 设备的内部开发代号或硬件名称如 “coral” (Pixel 4 XL)。这个值对普通用户无意义但在开发者社区或排查特定设备问题时非常有用。Build.PRODUCT(产品名称): 产品的整体名称通常是BRAND和MODEL的组合但格式不统一不建议用于用户界面显示。Build.HARDWARE(硬件名称): 设备的硬件平台如 “qcom” (高通)。可用于粗略判断芯片平台。Build.BOARD(主板平台): 设备底板的名称。系统版本相关字段Build.VERSION.RELEASE(系统版本号): 用户友好的版本字符串如 “11”, “12”, “13”。这就是我们常说的Android版本。注意它是一个String类型进行版本比较时不能直接用它。Build.VERSION.SDK_INT(SDK版本号):这是进行系统版本判断的黄金标准。它是一个int类型的常量对应着各个Android版本的API Level例如33对应 Android 13。在代码中做版本兼容性判断时必须使用SDK_INT与类似Build.VERSION_CODES.TIRAMISU这样的常量进行比较。if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { // 执行Android 13及以上版本才有的逻辑 }Build.VERSION.CODENAME(版本代号): 当前版本的开发代号如 “Tiramisu”。在正式版中它通常是 “REL”。重要提示Build类中的信息特别是MODEL和MANUFACTURER理论上可以被有Root权限的用户或某些深度定制ROM修改。因此绝对不要将这些信息用于安全校验或唯一身份标识。2.2android.provider.Settings.System与Settings.Global系统设置中存储了一些动态或可配置的设备信息获取它们通常需要声明权限。设备名称 (Settings.System.getString(contentResolver, “device_name”)): 这是用户在系统设置中为自己设备设置的名称如 “小明的手机”。从Android 8.0 (API 26) 开始获取此信息需要READ_PHONE_STATE权限且方式有所变化更推荐使用BluetoothAdapter.getName()或Settings.Global.getString(contentResolver, “device_name”)但后者需要WRITE_SECURE_SETTINGS权限系统应用权限。其他设置如屏幕超时时间、字体大小等都存储在这里。2.3 系统语言与区域信息获取用户的语言和区域偏好对于应用本地化至关重要。这里主要使用Locale类。Locale.getDefault(): 获取系统当前默认的Locale。这个Locale对象包含了语言和地区信息。val defaultLocale Locale.getDefault() val language defaultLocale.language // 如 zh, en val country defaultLocale.country // 如 CN, US val displayLanguage defaultLocale.getDisplayLanguage(defaultLocale) // 本地化的语言名称如“中文”多语言支持用户可能为应用单独设置了语言在Android 7.0及以上版本。要获取应用当前实际使用的语言应该使用上下文Context的resources.configuration.localesAPI 24或resources.configuration.locale已废弃来获取。2.4 通过PackageManager获取应用与设备能力信息PackageManager可以告诉你设备支持什么以及应用自身的信息。系统功能 (hasSystemFeature): 可以检查设备是否支持某项硬件或软件功能如蓝牙、NFC、摄像头闪光灯等。这在决定是否启用某些功能入口时非常有用。val pm context.packageManager val hasNfc pm.hasSystemFeature(PackageManager.FEATURE_NFC)应用版本信息: 虽然不属于设备信息但常一起获取用于反馈或更新检查。val packageInfo pm.getPackageInfo(context.packageName, 0) val appVersionName packageInfo.versionName // 如 1.2.3 val appVersionCode packageInfo.versionCode // 内部版本号数字2.5 显示信息屏幕尺寸与密度设备信息中显示参数对于UI适配至关重要。主要通过DisplayMetrics获取。val displayMetrics Resources.getSystem().displayMetrics val screenWidthPx displayMetrics.widthPixels val screenHeightPx displayMetrics.heightPixels val screenDensityDpi displayMetrics.densityDpi // 屏幕密度DPI val screenDensity displayMetrics.density // 缩放密度因子 val scaledDensity displayMetrics.scaledDensity // 用于字体缩放获取屏幕物理尺寸通常需要android.view.WindowManager但注意在Activity生命周期早期或后台获取时可能不准确。3. 实战构建一个健壮的设备信息获取工具类理解了各个信息源之后我们来动手封装一个工具类。这个类的目标不仅是获取信息还要处理兼容性、权限和异常确保在生产环境中稳定运行。3.1 工具类设计与依赖注入我们不建议使用纯静态工具类而是依赖应用上下文 (Context)这样更便于测试和管理生命周期。这里我们使用一个简单的单例模式并在应用启动时初始化。import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.provider.Settings import java.util.* object DeviceInfoHelper { private lateinit var appContext: Context fun init(context: Context) { appContext context.applicationContext } // 确保已初始化防止空指针 private fun checkInitialized() { if (!::appContext.isInitialized) { throw IllegalStateException(DeviceInfoHelper must be initialized with Context first.) } } }3.2 核心信息获取方法实现我们在工具类中逐一实现获取各种信息的方法并加上详细的注释和异常处理。1. 获取基础构建信息 (无需权限)data class BuildInfo( val model: String Build.MODEL, val manufacturer: String Build.MANUFACTURER, val brand: String Build.BRAND, val device: String Build.DEVICE, val product: String Build.PRODUCT, val hardware: String Build.HARDWARE, val board: String Build.BOARD ) fun getBuildInfo(): BuildInfo { return BuildInfo() }2. 获取系统版本信息data class SystemVersionInfo( val releaseVersion: String Build.VERSION.RELEASE, // 用户可见版本如“13” val sdkInt: Int Build.VERSION.SDK_INT, // API Level用于逻辑判断 val codeName: String Build.VERSION.CODENAME // 版本代号 ) fun getSystemVersionInfo(): SystemVersionInfo { return SystemVersionInfo() }3. 获取设备名称 (需要处理权限)这是一个典型的需要处理权限和版本兼容性的场景。fun getDeviceName(): String { checkInitialized() return try { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { // Android 8.0尝试从Settings.Global获取但需要系统权限。 // 更通用的做法是使用蓝牙名称或者直接返回一个默认值/组合值。 // 这里我们提供一个降级方案返回“品牌 型号” ${Build.BRAND} ${Build.MODEL} } else { // Android 8.0 以下尝试从Settings.System获取 Suppress(DEPRECATION) Settings.Secure.getString(appContext.contentResolver, bluetooth_name) ?: Settings.System.getString(appContext.contentResolver, device_name) ?: ${Build.BRAND} ${Build.MODEL} } } catch (e: SecurityException) { // 没有权限时的回退方案 ${Build.BRAND} ${Build.MODEL} } catch (e: Exception) { // 其他异常 Unknown Device } }4. 获取系统语言与区域data class LocaleInfo( val language: String, // ISO 639 语言代码 val country: String, // ISO 3166 国家代码 val displayLanguage: String, // 本地化显示的语言名 val displayCountry: String // 本地化显示的国家名 ) fun getLocaleInfo(): LocaleInfo { val defaultLocale Locale.getDefault() return LocaleInfo( language defaultLocale.language, country defaultLocale.country, displayLanguage defaultLocale.getDisplayLanguage(defaultLocale), displayCountry defaultLocale.getDisplayCountry(defaultLocale) ) } // 获取应用实际使用的语言考虑应用单独设置语言 fun getAppLocaleInfo(): LocaleInfo { checkInitialized() val configuration appContext.resources.configuration val locale if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { configuration.locales[0] } else { Suppress(DEPRECATION) configuration.locale } return LocaleInfo( language locale.language, country locale.country, displayLanguage locale.getDisplayLanguage(locale), displayCountry locale.getDisplayCountry(locale) ) }5. 获取屏幕显示信息import android.util.DisplayMetrics data class DisplayInfo( val widthPixels: Int, val heightPixels: Int, val densityDpi: Int, val density: Float, val scaledDensity: Float, val xdpi: Float, val ydpi: Float ) fun getDisplayInfo(): DisplayInfo { checkInitialized() val displayMetrics appContext.resources.displayMetrics return DisplayInfo( widthPixels displayMetrics.widthPixels, heightPixels displayMetrics.heightPixels, densityDpi displayMetrics.densityDpi, density displayMetrics.density, scaledDensity displayMetrics.scaledDensity, xdpi displayMetrics.xdpi, ydpi displayMetrics.ydpi ) }6. 获取应用版本信息data class AppVersionInfo( val versionName: String, val versionCode: Long // 注意API 28 使用 long 类型的 versionCode ) fun getAppVersionInfo(): AppVersionInfo { checkInitialized() return try { val packageInfo if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { appContext.packageManager.getPackageInfo(appContext.packageName, 0) } else { Suppress(DEPRECATION) appContext.packageManager.getPackageInfo(appContext.packageName, 0) } val versionCode if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { packageInfo.longVersionCode } else { Suppress(DEPRECATION) packageInfo.versionCode.toLong() } AppVersionInfo( versionName packageInfo.versionName ?: Unknown, versionCode versionCode ) } catch (e: PackageManager.NameNotFoundException) { // 理论上不会发生因为获取的是自身包名 AppVersionInfo(Unknown, 0) } }3.3 工具类的使用与初始化在你的Application类中进行初始化class MyApp : Application() { override fun onCreate() { super.onCreate() DeviceInfoHelper.init(this) } }在AndroidManifest.xml中声明Applicationapplication android:name.MyApp ... ... /application在Activity或ViewModel中获取信息val buildInfo DeviceInfoHelper.getBuildInfo() Log.d(DeviceInfo, Model: ${buildInfo.model}, Manufacturer: ${buildInfo.manufacturer}) val systemVersion DeviceInfoHelper.getSystemVersionInfo() if (systemVersion.sdkInt Build.VERSION_CODES.R) { // 执行Android 11的逻辑 } val localeInfo DeviceInfoHelper.getLocaleInfo() if (localeInfo.language zh) { // 加载中文资源 }4. 权限申请与隐私合规要点随着Android系统对隐私保护的加强获取某些设备信息需要用户授权且必须遵循隐私政策。4.1 可能需要的权限READ_PHONE_STATE用途在旧版本API中用于获取设备唯一标识如IMEI现已受限以及设备电话状态。对于获取设备名称在部分API中也可能需要。注意此权限属于危险权限需要运行时动态申请。从Android 10开始普通应用几乎无法获取IMEI等持久性设备标识。BLUETOOTH_CONNECT(API 31)用途在Android 12及以上版本如果通过BluetoothAdapter.getName()获取设备名称需要此权限。4.2 动态权限申请示例以READ_PHONE_STATE为例展示如何在Activity或Fragment中申请。// 在 Activity 中 private val requestCodePhoneState 1001 fun requestDeviceInfoPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { val permission Manifest.permission.READ_PHONE_STATE if (checkSelfPermission(permission) ! PackageManager.PERMISSION_GRANTED) { // 解释为什么需要这个权限可选但推荐 if (shouldShowRequestPermissionRationale(permission)) { // 向用户展示一个解释对话框 AlertDialog.Builder(this) .setTitle(需要电话权限) .setMessage(此权限用于准确获取您的设备信息以便更好地适配和提供服务。) .setPositiveButton(确定) { _, _ - requestPermissions(arrayOf(permission), requestCodePhoneState) } .setNegativeButton(取消, null) .show() } else { // 直接请求权限 requestPermissions(arrayOf(permission), requestCodePhoneState) } } else { // 已有权限执行获取信息操作 fetchDeviceInfoWithPermission() } } else { // 低于 Android 6.0无需运行时权限 fetchDeviceInfoWithPermission() } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { requestCodePhoneState - { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { fetchDeviceInfoWithPermission() } else { // 权限被拒绝使用降级方案如不获取设备名称 Toast.makeText(this, “权限被拒绝部分设备信息可能不准确”, Toast.LENGTH_SHORT).show() fetchDeviceInfoWithoutPermission() } } } }4.3 隐私合规要求明确告知在应用的《隐私政策》中清晰说明你收集了哪些设备信息如设备型号、系统版本、语言以及收集的目的如用于故障诊断、功能适配、统计分析。最小必要原则只收集业务功能必需的信息。例如如果你的应用不需要蓝牙功能就不要申请BLUETOOTH_CONNECT权限去获取设备名称。避免敏感信息严格避免收集IMEI、MAC地址、序列号等能够唯一、持久标识设备的敏感信息。Google Play政策对此有严格限制。用户控制在应用设置中应提供让用户查看和管理被收集信息的选项如果适用。5. 厂商定制系统ROM的兼容性陷阱与解决方案国内各手机厂商的定制系统MIUI、EMUI、ColorOS等是Android开发者的一大挑战。它们在系统API和行为上可能存在修改。5.1 常见问题后台限制在省电模式下你的应用可能无法在后台运行导致定时上报设备信息的任务失败。权限管理严格即使你动态申请了权限用户也可以在系统设置中单独禁止你的应用某项权限或者授予“仅在使用中允许”。你的代码需要能处理这些情况。Build信息可能被修改极少数深度定制的ROM可能会修改Build.MODEL等字段的值。默认语言问题有些ROM允许为每个应用单独设置语言但Locale.getDefault()返回的可能是系统全局语言而非应用当前语言。5.2 解决方案与健壮性编码后台任务使用WorkManager来执行后台信息上报任务。WorkManager能很好地兼容各厂商的后台限制策略确保任务最终能被执行。权限检查在每次执行需要权限的操作前都使用ContextCompat.checkSelfPermission()检查权限是否被授予。不要依赖一次申请的结果。信息校验与降级对获取到的Build.MODEL等信息不要做过于严格的字符串匹配。如果业务逻辑严重依赖设备型号考虑建立一个“厂商-型号”的映射白名单对于未知的型号提供降级或通用逻辑。使用应用上下文获取资源如前所述获取当前应用语言时务必使用appContext.resources.configuration而不是系统默认的Locale。// 示例一个更健壮的设备信息上报函数 fun reportDeviceInfoSafely() { val infoMap mutableMapOfString, Any() // 1. 添加无需权限的信息 infoMap[model] Build.MODEL infoMap[sdk_int] Build.VERSION.SDK_INT // ... 其他 Build 信息 // 2. 尝试获取需要权限的信息并处理异常 try { val deviceName getDeviceName() // 我们之前封装的方法内部有try-catch infoMap[device_name] deviceName } catch (e: Exception) { infoMap[device_name] fetch_error Log.w(Report, Failed to get device name, e) } // 3. 获取应用上下文相关的信息 infoMap[app_locale] getAppLocaleInfo().language infoMap[screen_density] getDisplayInfo().density // 4. 使用 WorkManager 在后台上报 val workRequest OneTimeWorkRequestBuilderUploadWorker() .setInputData(workDataOf(device_info to infoMap.toJsonString())) // 假设有toJsonString方法 .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 仅在联网时执行 .build() ) .build() WorkManager.getInstance(appContext).enqueue(workRequest) }6. 进阶获取更多设备状态与标识谨慎使用除了基础信息有时我们还需要了解设备的更详细状态。但请注意以下部分信息可能涉及隐私或权限且随着Android版本更新获取限制越来越多。6.1 网络状态import android.net.ConnectivityManager import android.net.NetworkCapabilities fun getNetworkType(context: Context): String { val connectivityManager context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { val network connectivityManager.activeNetwork val caps connectivityManager.getNetworkCapabilities(network) when { caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) true - WIFI caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) true - CELLULAR caps?.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) true - ETHERNET else - UNKNOWN } } else { Suppress(DEPRECATION) val networkInfo connectivityManager.activeNetworkInfo networkInfo?.typeName ?: UNKNOWN } }需要声明ACCESS_NETWORK_STATE权限普通权限无需动态申请。6.2 设备存储空间import android.os.StatFs import java.io.File fun getStorageInfo(context: Context): PairLong, Long { // 返回总空间和可用空间字节 val path Environment.getDataDirectory() // 内部存储 val stat StatFs(path.path) val blockSize if (Build.VERSION.SDK_INT Build.VERSION_CODES.JELLY_BEAN_MR2) { stat.blockSizeLong } else { Suppress(DEPRECATION) stat.blockSize.toLong() } val totalBlocks if (Build.VERSION.SDK_INT Build.VERSION_CODES.JELLY_BEAN_MR2) { stat.blockCountLong } else { Suppress(DEPRECATION) stat.blockCount.toLong() } val availableBlocks if (Build.VERSION.SDK_INT Build.VERSION_CODES.JELLY_BEAN_MR2) { stat.availableBlocksLong } else { Suppress(DEPRECATION) stat.availableBlocks.toLong() } val totalSpace totalBlocks * blockSize val freeSpace availableBlocks * blockSize return Pair(totalSpace, freeSpace) }6.3 关于设备唯一标识的警告重要在Android 10及以上版本获取持久性、不可重置的设备唯一标识如IMEI、序列号、MAC地址受到严格限制。普通应用无法获取这些信息。替代方案是使用Advertising ID或Instance ID但它们也各有特点Advertising ID (Google Play服务)用户可在系统设置中重置。适用于广告和分析。Instance ID / Firebase Installation ID与应用安装绑定重装应用会变化。绝对不要尝试通过读取/proc/cpuinfo或其它系统文件来生成唯一ID这些方法不可靠且可能违反平台政策。7. 常见问题排查与调试技巧在实际开发中你肯定会遇到设备信息获取不准或报错的情况。这里分享一些排查思路。7.1 信息获取为null或空字符串可能原因厂商ROM删除了某些Build字段或者系统设置中该值为空。排查在多个不同品牌、型号的真机上测试。使用Log打印出所有Build字段的值进行对比。解决在代码中做好空值判断提供合理的默认值。val deviceModel Build.MODEL ?: Unknown Model7.2 权限已授予但依然获取失败可能原因厂商权限管理用户可能授予了“仅在使用中允许”权限而你的代码在后台运行时尝试获取。Target API 限制如果你的targetSdkVersion较高某些API的行为会发生变化如作用域存储。权限组在Android 11权限授予变得更加精细。排查检查应用是否在前台。在设备设置中仔细查看你的应用权限详情。查阅官方文档确认你使用的API在当前targetSdkVersion下的行为。解决确保在合适的时机如用户交互时请求权限和获取信息。对于后台需求使用WorkManager并处理好权限缺失的降级逻辑。7.3 模拟器与真机差异现象在模拟器上一切正常在真机上崩溃或信息不对。排查模拟器的Build信息是固定的如generic_x86而真机千差万别。模拟器可能没有某些硬件如蓝牙导致相关API返回异常。模拟器的系统版本和内核可能与真机有细微差别。解决永远不要只依赖模拟器测试。必须使用多种品牌和型号的真机进行充分测试。可以利用云测平台如Firebase Test Lab覆盖更多设备。7.4 使用ADB快速验证在开发调试时adb shell命令是你的好朋友。# 获取设备型号和厂商 adb shell getprop ro.product.model adb shell getprop ro.product.manufacturer adb shell getprop ro.product.brand # 获取系统版本 adb shell getprop ro.build.version.release adb shell getprop ro.build.version.sdk # 查看系统语言设置 adb shell getprop persist.sys.locale adb shell settings get system system_locales # 查看设备名称蓝牙名称 adb shell settings get secure bluetooth_name通过对比ADB命令的输出和你应用获取的值可以快速定位问题是出在系统层面还是你的代码逻辑。7.5 信息上报的数据格式与安全当你将设备信息上报到服务器时建议使用JSON格式并注意字段命名的一致性。同时确保传输过程使用HTTPS加密。{ device_info: { model: SM-G998B, manufacturer: samsung, sdk_int: 33, os_version: 13, language: zh-CN, screen_density: 3.5, screen_width: 1440, screen_height: 3200, app_version: 1.2.3, network_type: WIFI } }最后一点心得获取设备信息本身不难难的是写出能在成千上万种不同设备、不同系统版本、不同厂商ROM下都能稳定运行的代码。关键就在于永远不要假设做好空值判断、异常捕获、权限检查和版本兼容。把这些细节处理好你的应用就离崩溃远了一步离用户满意近了一步。