Android 14+系统5G NSA网络下如何同时获取LTE与NR小区信息?
Android 14+系统5G NSA网络下如何同时获取LTE与NR小区信息?
我太懂这种郁闷了!在5G NSA组网下,Android系统居然把LTE锚点小区和NR小区的信息交替返回——单次调用allCellInfo只能拿到其中一组,自己写的APP明明逻辑没问题,结果却和G-NET这类工具的表现不一样,换成SA模式就一切正常,这明显是系统底层的快照机制搞的鬼!
为什么会出现这种情况?
NSA(非独立组网)的核心是5G NR依附在LTE锚点小区上工作,Android系统在这种模式下,并没有把LTE和NR的小区信息合并在同一个allCellInfo返回结果里,而是分成了两个独立的快照。每次调用allCellInfo只能获取到当前的「活跃快照」——要么是LTE锚点的全套信息,要么是NR的信息,所以你轮询的时候会看到两者交替出现。
可行的解决思路
核心思路就是合并多次获取的结果,维护一个去重的缓存列表,具体可以这么做:
1. 不要每次获取到新信息就清空列表,而是合并结果
你之前的代码每次轮询就清空列表再添加新数据,自然只能看到当前快照的内容。正确的做法是维护一个全局缓存:
- 把每次获取到的新条目(不管是LTE还是NR)加入缓存
- 通过小区唯一标识(比如CI+PCI)判断是否已经存在,避免重复添加
- 给每个缓存条目设置过期时间,定期清理失效的旧数据,避免缓存膨胀
2. 改用TelephonyCallback替代主动轮询(更高效)
主动轮询不仅耗电,还容易错过快照切换的时机。Android 10+提供了TelephonyManager.registerTelephonyCallback接口,通过回调接收小区信息变化,能更精准地捕捉到交替出现的LTE和NR快照。
修改后的代码示例
@RequiresApi(Build.VERSION_CODES.Q) class MainActivity : ComponentActivity() { private lateinit var telephonyManager: TelephonyManager // 维护缓存的小区信息,包含去重后的LTE和NR条目 private val cachedCellInfos = mutableStateListOf<CachedCellInfo>() // 小区信息过期时间(可根据实际网络情况调整) private val CELL_EXPIRY_DURATION = 5000L data class CachedCellInfo(val cellInfo: CellInfo, val timestamp: Long) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager requestPermissionsIfNeeded() setContent { MaterialTheme { CellInfoScreen(cachedCellInfos.map { it.cellInfo }) } } setupTelephonyCallback() // 启动定期清理过期条目的任务 startExpiryCleanup() } private fun requestPermissionsIfNeeded() { val permissions = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE ) ActivityCompat.requestPermissions(this, permissions, 1) } private fun setupTelephonyCallback() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.CellInfoListener { override fun onCellInfoChanged(cellInfoList: MutableList<CellInfo>?) { cellInfoList?.forEach { newCellInfo -> // 检查缓存中是否已有相同小区 val isAlreadyCached = cachedCellInfos.any { cached -> isSameCell(cached.cellInfo, newCellInfo) } if (!isAlreadyCached) { cachedCellInfos.add(CachedCellInfo(newCellInfo, System.currentTimeMillis())) } else { // 若已存在,更新时间戳避免被误清理 cachedCellInfos.find { isSameCell(it.cellInfo, newCellInfo) }?.timestamp = System.currentTimeMillis() } } } } // 注册回调,用主线程处理结果 telephonyManager.registerTelephonyCallback(mainExecutor, telephonyCallback) } else { // 兼容Q-R版本的轮询方案(Android14+可忽略) startRepeatingUpdates() } } // 判断两个CellInfo是否为同一个小区 private fun isSameCell(a: CellInfo, b: CellInfo): Boolean { if (a.javaClass != b.javaClass) return false return when (a) { is CellInfoLte -> { val bLte = b as CellInfoLte a.cellIdentity.ci == bLte.cellIdentity.ci && a.cellIdentity.pci == bLte.cellIdentity.pci } is CellInfoNr -> { val bNr = b as CellInfoNr a.cellIdentity.nci == bNr.cellIdentity.nci && a.cellIdentity.pci == bNr.cellIdentity.pci } is CellInfoWcdma -> { val bWcdma = b as CellInfoWcdma a.cellIdentity.cid == bWcdma.cellIdentity.cid && a.cellIdentity.pci == bWcdma.cellIdentity.pci } is CellInfoGsm -> { val bGsm = b as CellInfoGsm a.cellIdentity.cid == bGsm.cellIdentity.cid && a.cellIdentity.arfcn == bGsm.cellIdentity.arfcn } else -> false } } private fun startExpiryCleanup() { lifecycleScope.launch { while (isActive) { val currentTime = System.currentTimeMillis() // 清理超过过期时间的旧条目 cachedCellInfos.removeAll { currentTime - it.timestamp > CELL_EXPIRY_DURATION } delay(1000) // 每秒清理一次 } } } // 兼容旧版本的轮询方案(Android14+可忽略) private fun startRepeatingUpdates() { lifecycleScope.launch { while (isActive) { val info = telephonyManager.allCellInfo info?.forEach { newCellInfo -> val isAlreadyCached = cachedCellInfos.any { cached -> isSameCell(cached.cellInfo, newCellInfo) } if (!isAlreadyCached) { cachedCellInfos.add(CachedCellInfo(newCellInfo, System.currentTimeMillis())) } } delay(300) } } } } @Composable fun CellInfoScreen(cellInfoList: List<CellInfo>) { LazyColumn { items(cellInfoList.size) { index -> val cellInfo = cellInfoList[index] Card( modifier = Modifier .fillMaxWidth() .padding(4.dp), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column(modifier = Modifier.padding(8.dp)) { Text("Type: ${cellInfo.javaClass.simpleName}") Text("Registered: ${cellInfo.isRegistered}") Text("CI: ${getCi(cellInfo)}") Text("PCI: ${getPci(cellInfo)}") Text("TAC: ${getTac(cellInfo)}") Text("ARFCN: ${getArfcn(cellInfo)}") Text("Signal: ${getSignalStrength(cellInfo)}") } } } } } // 以下工具方法保留原逻辑即可 fun getCi(info: CellInfo): Int? = when (info) { is CellInfoLte -> info.cellIdentity.ci is CellInfoNr -> info.cellIdentity.nci.toInt() is CellInfoWcdma -> info.cellIdentity.cid is CellInfoGsm -> info.cellIdentity.cid else -> null } fun getPci(info: CellInfo): Int? = when (info) { is CellInfoLte -> info.cellIdentity.pci is CellInfoNr -> info.cellIdentity.pci else -> null } fun getTac(info: CellInfo): Int? = when (info) { is CellInfoLte -> info.cellIdentity.tac is CellInfoNr -> info.cellIdentity.tac else -> null } fun getArfcn(info: CellInfo): Int? = when (info) { is CellInfoLte -> info.cellIdentity.earfcn is CellInfoNr -> info.cellIdentity.nrarfcn is CellInfoWcdma -> info.cellIdentity.uarfcn is CellInfoGsm -> info.cellIdentity.arfcn else -> null } fun getSignalStrength(info: CellInfo): String = when (info) { is CellInfoLte -> "${info.cellSignalStrength.dbm} dBm" is CellInfoNr -> "${info.cellSignalStrength.dbm} dBm" is CellInfoWcdma -> "${info.cellSignalStrength.dbm} dBm" is CellInfoGsm -> "${info.cellSignalStrength.dbm} dBm" else -> "N/A" }
代码关键点说明
- 缓存管理:用
CachedCellInfo封装小区信息和时间戳,既能合并LTE和NR数据,又能通过时间戳清理旧数据 - 去重逻辑:
isSameCell方法通过小区唯一标识判断是否重复,避免缓存中出现同一个小区的多条记录 - 高效回调:用
TelephonyCallback替代轮询,系统会在小区信息变化时主动通知,比主动轮询更省电、更及时 - 权限检查:确保
ACCESS_FINE_LOCATION和READ_PHONE_STATE权限已被用户授予,这是获取小区信息的必要条件
额外注意事项
- 可以根据实际网络情况调整过期时间和清理间隔,如果小区切换频繁,可适当缩短过期时间
- NSA下的这个现象是Android系统底层机制导致的,SA模式下因为是独立组网,单次调用就能拿到所有小区信息,所以不会有这个问题
内容来源于stack exchange
相关产品推荐
相关产品推荐

