ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

安卓互联手写实现:解决版本升级API失效痛点

安卓互联手写实现:解决版本升级API失效痛点 安卓互联手写实现:解决版本升级API失效痛点 版本升级后 API 全变了,旧代码直接报错,这种崩溃感谁懂?别再找那些过时的教程了,直接上手手写实现一套稳定的安卓互联方案,才能从根子上解决问题。 项目目标 咱们做技术博客,最怕的就是“水土不服”。很多读者反馈,照着网上 2021 年的代码跑,到 2024 年 Android 14 上直接崩,原因就是系统权限收紧、API 废弃。我们的目标很明确:不依赖第三方臃肿 SDK,从零手写一个轻量级、跨版本兼容的安卓互联模块。 这个模块主要解决三个场景:设备发现:局域网内快速发现其他安卓设备。 数据同步:大文件(如视频、文档)的高效传输。 状态心跳:保持连接活跃,自动重连,避免“假死”。为什么非要手写?因为第三方库往往封装得太深,出了 bug 你只能干瞪眼。手写实现,每一行代码都在你手里,哪里断了你知道,怎么修你也清楚。这就是安卓互联实战的核心价值——可控、可维护、可复现。 目录结构 为了保持工程化整洁,我们采用标准的 Gradle 模块结构。不要把所有逻辑堆在一个 Activity 里,那是新手才会犯的错。 com.example.androidconnect/ ├── app/ │ ├── java/ │ │ └── com/example/androidconnect/ │ │ ├── MainActivity.kt // 入口,UI 展示 │ │ ├── model/ │ │ │ ├── DeviceInfo.kt // 设备数据模型 │ │ │ └── TransferState.kt // 传输状态枚举 │ │ ├── service/ │ │ │ ├── DiscoveryService.kt // 核心:设备发现服务 | | ├── TransferManager.kt // 核心:文件传输管理 │ │ └── util/ │ │ ├── SocketUtils.kt // 底层 Socket 封装 │ │ └── PermissionHelper.kt // 权限申请工具 │ ├── res/ │ └── AndroidManifest.xml // 权限声明重点 └── build.gradle.kts注意 service 包下的两个核心类,这是整个安卓互联逻辑的大脑。DiscoveryService 负责“找人”,TransferManager 负责“传东西”。这种职责分离的设计,是为了方便后续单元测试和模块化扩展。 核心代码实现 这里是重头戏。我们不贴满屏代码,只讲最关键的手写实现逻辑,并逐行拆解为什么这么写。 1. 设备发现:UDP 广播的坑与解法 很多教程教你用 Wi-Fi Direct,但配置复杂且兼容性差。在局域网内,UDP 广播是最稳的。但 Android 7.0+ 对后台网络访问限制极严,直接 sendto 会静默失败。 // DiscoveryService.kt 片段 class DiscoveryService : Service() {private var socket: DatagramSocket? = nullprivate val handler = Handler(Looper.getMainLooper())private val discoveredDevices = mutableListOfDeviceInfo()override fun onCreate() {super.onCreate()// 关键:必须获取运行时权限,否则广播包发不出去if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED) {// 启动 Activity 请求权限return}initSocket()}private fun initSocket() {try {socket = DatagramSocket(0) // 系统自动分配端口socket!!.setReuseAddress(true) // 允许多实例绑定socket!!.setSoTimeout(5000) // 设置超时,避免无限阻塞// 启动监听线程,接收其他设备的广播包Thread {val buffer = ByteArray(1024)while (isRunning) {try {val packet = DatagramPacket(buffer, buffer.size)socket!!.receive(packet)val data = String(packet.data).trim()// 解析 JSON 格式的广播包,包含 IP, 端口, 设备名val device = parseDeviceInfo(data)if (device.ip != getLocalIp()) { // 排除自己addOrUpdateDevice(device)}} catch (e: SocketTimeoutException) {// 超时正常,继续循环} catch (e: Exception) {e.printStackTrace()}}}.start()// 定时发送广播,告诉其他设备“我在这”handler.postDelayed(sendBroadcastRunnable, 3000)} catch (e: IOException) {e.printStackTrace()}}private val sendBroadcastRunnable = Runnable {try {val localIp = getLocalIp()// 构造广播包:IP:Port:DeviceNameval msg = $localIp:$port:$deviceNameval bytes = msg.toByteArray()val broadcastIp = InetAddress.getByName(255.255.255.255)val packet = DatagramPacket(bytes, bytes.size, broadcastIp, BROADCAST_PORT)socket!!.send(packet)} catch (e: Exception) {e.printStackTrace()}// 3秒后再次发送,保持活跃handler.postDelayed(this, 3000)} }逐行讲解重点:DatagramSocket(0):传 0 表示让系统分配随机端口,避免端口冲突。 setSoTimeout(5000):这是很多新手忽略的。不设超时,线程会永久阻塞在 receive(),导致无法退出或响应停止指令。 广播地址:使用 255.255.255.255 是最通用的局域网广播地址,比计算子网掩码更简单可靠,适合 C 类网络(大多数家庭/办公网)。2. 文件传输:TCP 粘包处理 UDP 适合发现,TCP 适合传输。但 TCP 是流式协议,存在“粘包”问题。如果不处理,接收端可能一次收到多个文件头,或者把一个文件拆成多段。 // TransferManager.kt 片段 fun sendFile(file: File, socket: Socket) {val outputStream = socket.getOutputStream()val randomAccessFile = RandomAccessFile(file, r)// 1. 先发送文件元数据:长度(8字节) + 文件名长度(4字节) + 文件名outputStream.write(Long.valueOf(randomAccessFile.length()).toByteArray())val fileNameBytes = file.name.toByteArray()outputStream.write(Int.toByte(fileNameBytes.size))outputStream.write(fileNameBytes)// 2. 分块发送文件内容,每块 1024 字节val buffer = ByteArray(1024)var bytesRead: Intwhile ((bytesRead = randomAccessFile.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead)}outputStream.flush()randomAccessFile.close() }fun receiveFile(socket: Socket, saveDir: File) {val inputStream = socket.getInputStream()// 1. 读取元数据val lenBytes = ByteArray(8)inputStream.read(lenBytes)val fileSize = ByteBuffer.wrap(lenBytes).longval nameLenByte = inputStream.read()val nameBytes = ByteArray(nameLenByte)inputStream.read(nameBytes)val fileName = String(nameBytes)val destFile = File(saveDir, fileName)val fileOutputStream = FileOutputStream(destFile)// 2. 按文件大小读取内容val buffer = ByteArray(1024)var totalRead = 0Lwhile (totalRead fileSize) {val bytesRead = inputStream.read(buffer)if (bytesRead == -1) breakfileOutputStream.write(buffer, 0, bytesRead)totalRead += bytesRead}fileOutputStream.close() }避坑指南:字节序问题:Long.toByte() 和 ByteBuffer 默认是大端序(Big-Endian)。如果发送端和接收端架构不同(虽然 Android 都是小端,但 Java 网络流通常用大端),务必显式指定 order(ByteOrder.BIG_ENDIAN),否则文件大小会解析成天文数字。 缓冲区分块:不要一次性 read() 整个文件,内存会爆。1024 或 4096 字节是经验值,兼顾性能与内存占用。运行与测试 代码写完了,怎么验证?别光看 Logcat 没报错就以为成功了。模拟多设备环境: 由于测试手机可能只有一台,建议在 Android Studio 中启动两个 Emulator(虚拟机),分别配置不同的 Wi-Fi 网络(同一网段)。或者使用两台真机,连接同一个 Wi-Fi。抓包验证: 使用 Wireshark 或 Android Studio 自带的 Network Profiler。观察 UDP 广播包是否真的发出去了,以及 TCP 连接建立后的数据流是否符合预期。重点看是否有 RST 包,如果有,说明连接被重置,检查防火墙设置。弱网测试: 在 Network Profiler 中设置 Slow 3G 或 Offline。观察 DiscoveryService 是否能自动重连,TransferManager 是否能断点续传(当前代码未实现断点续传,这是进阶方向,但在弱网下必须考虑)。常见报错排查:java.net.SocketException: Permission denied:检查 AndroidManifest.xml 是否声明了 INTERNET 和 ACCESS_NETWORK_STATE 权限,且运行时权限已获取。 Connection refused:目标设备的端口未开放,或目标设备 TransferManager 服务未启动。确保在接收端先启动监听线程。优化扩展 基础功能跑通后,如何让它更“工程化”?引入 OkHttp 替代原生 Socket? 不建议。OkHttp 是 HTTP 协议,不适合这种自定义二进制协议。手写实现的优势就在于协议层的绝对控制。但可以用 AsyncTask 或 Coroutine 替代原生 Thread,避免阻塞主线程,提升代码可读性。加密传输: 公网环境下,明文传输是危险的。可以在 TCP 握手阶段增加 RSA 密钥交换,后续数据用 AES 加密。参考 OpenSSL 官方文档中的 TLS 握手流程,理解非对称加密与对称加密的结合使用。状态管理: 使用 ViewModel + LiveData 或 StateFlow 管理设备列表和传输进度。避免在 Activity 销毁时丢失传输状态。将 TransferManager 封装为单例,通过 EventBus 或 Flow 向 UI 层推送进度更新。断点续传: 在文件元数据中增加 Last-Modified 时间戳和 File-ID。接收端如果检测到相同 File-ID 且已存在部分文件,则从上次中断位置继续发送。这需要修改协议头,增加偏移量字段。小结 通过手写实现这套安卓互联方案,我们绕过了第三方 SDK 的黑盒,彻底解决了版本升级后 API 失效的痛点。从 UDP 广播发现设备,到 TCP 流式传输文件,每一步都基于底层原理,而非堆砌 API。 技术不是背出来的,是踩坑踩出来的。你在这个项目中遇到过什么奇奇怪怪的 Bug?或者在权限申请上有什么独门技巧? 还有什么不懂的?评论区留言挨个回
返回列表