Android 手机号获取实战:TelephonyManager、SmsManager 与权限配置全解析
1. 为什么 Android 获取手机号总是不生效很多刚接触 Android 电话相关开发的朋友第一次写获取手机号的代码时都会遇到同一个困惑明明调用了TelephonyManager.getLine1Number()返回的却是一个空字符串。这不是代码写错了而是 Android 从设计上就没有保证这个接口一定能拿到号码。手机号本质上属于运营商写入 SIM 卡的用户数据是否写入、写入哪个字段完全由运营商决定。国内三大运营商在大部分 SIM 卡上并不会把本机号码写进getLine1Number()能读到的位置所以你会看到网上大量「获取手机号失败」的帖子。真正能稳定拿到号码的场景通常是运营商定制机、部分物联网卡或者用户自己在系统设置里手动填过「本机号码」。那实际项目里怎么办常见有三条路一是直接读TelephonyManager能拿到最好拿不到就走兜底二是通过SmsManager发一条查询短信再监听回执短信解析出号码三是干脆放弃本地读取改用服务端短信验证码登录。这篇就把前两条链路完整走一遍从权限声明、运行时申请到读取代码、短信监听再到用 TaoToken 的 API 通道验证接口调用结果给你一套能直接抄的骨架。适合谁看正在做 Android 端手机号相关功能、被getLine1Number()返回空坑过的开发者想搞清楚READ_PHONE_STATE、READ_SMS、RECEIVE_SMS这几个权限到底怎么配的人以及想把本地号码获取和服务端接口验证串起来落地的同学。2. 前置准备权限、TaoToken Key 与项目配置2.1 权限清单先理清楚Android 获取手机号涉及的权限分两类一类是普通声明即可的一类是危险权限必须运行时申请。权限作用是否运行时申请READ_PHONE_STATE读取设备电话状态、getLine1Number是Android 6.0READ_PHONE_NUMBERSAndroid 8.0 读取本机号码更精确是SEND_SMS发送查询短信是RECEIVE_SMS监听回执短信是READ_SMS读取短信内容部分场景是在AndroidManifest.xml里先静态声明uses-permission android:nameandroid.permission.READ_PHONE_STATE / uses-permission android:nameandroid.permission.READ_PHONE_NUMBERS / uses-permission android:nameandroid.permission.SEND_SMS / uses-permission android:nameandroid.permission.RECEIVE_SMS / uses-permission android:nameandroid.permission.READ_SMS /注意READ_PHONE_NUMBERS在 Android 8.0API 26才引入低版本设备上申请会直接失败代码里要做版本判断。2.2 TaoToken 统一 Key 通道准备本地拿到号码之后通常还要把号码送到服务端做校验或绑定。这时候如果每个接口都单独配一套鉴权调试起来很烦。我习惯用 TaoToken 做统一 Key 通道一个 Key 走所有模型和接口调用验证阶段特别省事。先去控制台创建 API Key控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Key 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys创建好之后把 Key 存到本地local.properties或环境变量里别硬编码进代码。API 基础地址是https://taotoken.net/api这个地址不带任何查询参数直接作为baseUrl用。后面验证接口调用结果时我们会用它发一个请求确认号码上报链路是通的。3. 可复制配置TelephonyManager 读取号码骨架3.1 运行时权限申请先写一个权限申请工具方法把电话相关权限一次性申请private val phonePermissions mutableListOfString().apply { add(Manifest.permission.READ_PHONE_STATE) add(Manifest.permission.SEND_SMS) add(Manifest.permission.RECEIVE_SMS) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { add(Manifest.permission.READ_PHONE_NUMBERS) } } private fun requestPhonePermissions(activity: Activity, requestCode: Int) { val denied phonePermissions.filter { ContextCompat.checkSelfPermission(activity, it) ! PackageManager.PERMISSION_GRANTED } if (denied.isEmpty()) { onPermissionReady() } else { ActivityCompat.requestPermissions(activity, denied.toTypedArray(), requestCode) } }在onRequestPermissionsResult里判断结果全部授予后再执行读取逻辑。这里有个坑READ_PHONE_NUMBERS和READ_PHONE_STATE是两个独立权限只申请前者在部分机型上依然读不到号码建议两个都申请。3.2 读取本机号码fun readLineNumber(context: Context): String { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) ! PackageManager.PERMISSION_GRANTED) { return } val tm context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager return try { // getLine1Number 在多数国内 SIM 卡上返回空串 tm.line1Number ?: } catch (e: SecurityException) { Log.e(PhoneNumber, 读取号码被拒绝: ${e.message}) } }实测下来line1Number返回空是常态所以一定要有兜底逻辑。可以顺便读一下simOperatorName和networkOperatorName用来判断当前是哪家运营商方便后面决定发查询短信的号码。fun getCarrierInfo(context: Context): PairString, String { val tm context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val simName tm.simOperatorName ?: unknown val netName tm.networkOperatorName ?: unknown return simName to netName }3.3 SmsManager 发送查询短信当line1Number为空时走短信查询兜底。不同运营商查询指令不同下面这张表是常见对照运营商查询号码指令内容中国移动10086发送「本机号码」或「501」中国联通10010发送「本机号码」中国电信10000发送「本机号码」发送代码fun sendQuerySms(context: Context, targetNumber: String, content: String) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) ! PackageManager.PERMISSION_GRANTED) { return } val smsManager if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { context.getSystemService(SmsManager::class.java) } else { SmsManager.getDefault() } val sentIntent PendingIntent.getBroadcast( context, 0, Intent(SMS_SENT_ACTION), PendingIntent.FLAG_IMMUTABLE ) smsManager.sendTextMessage(targetNumber, null, content, sentIntent, null) }注意Android 12API 31之后SmsManager.getDefault()被标记为废弃推荐用context.getSystemService(SmsManager::class.java)并且PendingIntent必须带FLAG_IMMUTABLE否则会直接抛异常。4. 验证请求监听回执短信并解析号码4.1 注册短信广播接收器回执短信到达后系统会发android.provider.Telephony.SMS_RECEIVED广播。写一个BroadcastReceiver来接收class SmsReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action ! Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return val messages Telephony.Sms.Intents.getMessagesFromIntent(intent) val sb StringBuilder() var sender for (msg in messages) { sender msg.displayOriginatingAddress ?: sb.append(msg.displayMessageBody ?: ) } Log.d(SmsReceiver, 来自 $sender 的内容: $sb) val number extractPhoneNumber(sb.toString()) if (number.isNotEmpty()) { Log.d(SmsReceiver, 解析到本机号码: $number) // 这里可以把号码上报到服务端 } } }注册接收器Android 13API 33之后动态注册必须显式指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTEDval filter IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { context.registerReceiver(SmsReceiver(), filter, Context.RECEIVER_EXPORTED) } else { context.registerReceiver(SmsReceiver(), filter) }4.2 从短信文本里提取 11 位号码回执短信里往往夹杂套餐名称、余额、日期等一堆数字直接取第一个数字大概率是错的。用正则把所有连续数字段抓出来再筛选长度等于 11 且以 1 开头的fun extractPhoneNumber(text: String): String { val regex Regex(\\d{11}) val matches regex.findAll(text) for (m in matches) { val candidate m.value if (candidate.startsWith(1)) { return candidate } } return }这个正则比原版\\d*更稳因为\\d*会匹配空串遍历时容易拿到一堆空结果。限定\\d{11}直接锁定手机号长度再校验首位是 1误判率低很多。4.3 用 TaoToken 接口验证上报结果号码解析出来后一般要上报到服务端。验证阶段可以用 TaoToken 的 API 通道发一个请求确认链路通不通。先看接入文档确认请求格式接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc用 curl 快速验证curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [ {role: user, content: 验证手机号上报链路返回 ok} ] }如果返回正常的 JSON 结构说明 Key 和网络通道都没问题。你也可以在模型对话页面直接手动测一条模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentchat成功返回的样子大概是这样{ id: chatcmpl-xxx, object: chat.completion, choices: [ { index: 0, message: { role: assistant, content: ok } } ] }看到choices数组里有内容就说明整条调用链是通的。这时候再把 Android 端解析出的号码拼进请求体就能完成「本地读取 → 服务端校验」的闭环。5. 本篇常见错排查5.1 getLine1Number 返回空字符串这是最高频的问题前面已经说过根因是运营商没把号码写进 SIM 卡对应字段。排查顺序先确认READ_PHONE_STATE和READ_PHONE_NUMBERS都授予了再确认设备插了 SIM 卡且信号正常最后接受「读不到是正常的」这个事实走短信兜底或服务端验证码方案。不要在这上面死磕浪费时间。5.2 短信广播收不到先检查RECEIVE_SMS权限是否授予。Android 8.0 之后广播是隐式的动态注册时如果没加RECEIVER_EXPORTED在 Android 13 上会直接抛SecurityException。另外部分国产 ROM 有短信权限二次管控需要在系统设置里手动允许应用读取短信。5.3 PendingIntent 报 FLAG_IMMUTABLE 错误Android 12 起所有PendingIntent必须显式指定可变性标志。发送短信时用的sentIntent加上PendingIntent.FLAG_IMMUTABLE即可。如果业务需要接收方修改 Intent 内容才用FLAG_MUTABLE但短信场景基本不需要。5.4 正则匹配到错误号码回执短信里可能包含客服电话、日期、金额等数字。用\\d{11}加首位为 1 的校验能过滤掉绝大部分干扰。如果还是误判可以进一步限定号码前一位不是数字后一位不是数字避免从更长数字串里截取。5.5 TaoToken 请求返回 401先确认Authorization头格式是Bearer 你的Key中间有一个空格。再确认 Key 没有过期或被删除去 API Keys 页面核对一下。如果本地用环境变量传 Key注意 shell 里$TAOTOKEN_API_KEY是否真的被赋值了可以echo一下确认。6. 落地建议与后续接入把上面几块拼起来一个完整的手机号获取流程就是申请权限 → 读line1Number→ 为空则发查询短信 → 监听回执 → 正则解析 → 上报服务端验证。这套链路在大部分 Android 设备上都能跑通读不到号码时也有兜底不会卡死。如果你后续要做更复杂的编码任务比如把号码校验逻辑接到 Agent 工作流里或者需要长期跑批量接口调用可以了解一下 Coding PlanCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan接入文档里对请求参数、返回结构、错误码都有详细说明遇到报错先翻文档比到处搜快得多接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc最后提醒一句getLine1Number能拿到号码是运气拿不到是常态产品设计时别把「自动获取本机号码」当成必选路径留一条手动输入或验证码登录的退路用户体验会稳很多。