资讯动态

Android Wi-Fi Direct实战:官方Demo原理、常见坑与改造方案

发布时间:2026/10/15 18:23:12 来源:尧图企业网站定制
简介Android官方Wi-Fi P2P示例项目WiFiDirectDemo是一份面向Android开发者的设备直连技术参考演示了不经过无线接入点即可完成设备发现、群组创建、连接管理与高速文件传输的完整流程。压缩包共88个文件约244KB包含7个Java源码、14个XML布局与清单、26个class编译产物以及32张界面与流程截图资源包内同时提供src源码与res资源目录方便逐步跟踪各回调状态也保留了编译中间产物适合对照学习Wi-Fi P2P从权限配置到核心API调用如WifiP2pManager、discoverPeers、connect的实现细节。已有781人学习下载。通过这份官方Demo读者可得到可直接导入工程学习的源码结构、构建产物及运行说明覆盖设备发现、连接请求、群组创建维护、Socket文件传输等模块对理解设备发现状态机、群组主从协商和异步回调处理尤其有帮助并可在此基础上扩展文件分享、联机对战或投屏等应用。1. Android官方Wifi P2P Demo花一个下午跑通省一个月的弯路Wi-Fi Direct 听起来很硬核但官方这份 Demo 的核心目标其实非常朴素让两台 Android 设备在没有路由器、没有热点、没有数据流量的前提下直接通过 Wi-Fi 找到彼此并传数据。很多开发者第一次接触它都是因为业务里出现了「局域网内文件互传」「设备配网」「IoT 设备间点对点通信」这类需求然后搜到这个 Demo。它的价值在于把 Android 系统里最绕的一套 P2P 协议栈包装成了几个可调用的 APIWifiP2pManager 负责发现和连接BroadcastReceiver 负责接收系统状态ServerSocket 和 Socket 负责传输。但这份 Demo 有它的年代感直接搬进项目里会踩很多坑。这篇文章我会从协议链路、工程结构、最小可运行实现、常见的翻车现场到进阶改造一路讲完尽量让你少走弯路。2. Wi-Fi Direct 的链路本质从发现设备到建立连接官方 Demo 帮你走完哪几步很多人第一次打开官方 Demo 时会被目录里的 Fragment、Receiver、Manager 等十几个类绕晕。实际上它做的事可以压缩成四步发现附近设备、展示设备列表、发起连接、建立 Socket 传输。这一章把每一步背后系统到底发生了什么讲清楚你后面调参和排错才有依据。2.1 四个广播事件决定了 Demo 的所有逻辑分支Android 的 Wi-Fi Direct 不像蓝牙那样靠回调就能完成大部分操作它的状态变化主要是通过系统广播通知应用的。官方 Demo 里的 WiFiDirectBroadcastReceiver 本质就是监听四个关键广播WIFI_P2P_STATE_CHANGED_ACTION 表示 Wi-Fi Direct 功能开关状态变化WIFI_P2P_PEERS_CHANGED_ACTION 表示附近发现的设备列表有更新WIFI_P2P_CONNECTION_CHANGED_ACTION 表示连接状态有变化比如成功成为 Group Owner 或者连接断开WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 表示本机设备信息有变化。理解这四个广播后你就明白官方 Demo 的主流程是事件驱动而不是调用驱动。也就是说你调用 discoverPeers() 之后系统并不会立刻把设备列表塞给你而是过一会发一个 PEERS_CHANGED 广播你在这个广播里调用 requestPeers() 才能拿到列表。这个异步机制是新手最容易懵的地方明明调用成功了为什么列表是空的因为广播还没到。这类事件驱动模式也解释了为什么官方 Demo 里大量代码都在处理「当前状态是什么就显示什么界面」。它本质上是一个状态机初始化、正在搜索、已发现设备、正在连接、已连接、传输中。你把状态机画清楚再回头看代码就会觉得很有条理。2.2 从 discoverPeers 到 connectGroup Owner 协商机制Wi-Fi Direct 里没有路由器但两台设备直连时必须有「一台负责管理连接」的角色这个角色叫 Group Owner简称 GO。GO 相当于迷你版 AP另一台设备作为 Client 连入。谁当 GO 由协商决定也就是 connect() 时的 groupOwnerIntent 参数取值范围 0 到 15数值越大越倾向于成为 GO。官方 Demo 在连接阶段的做法是用户点击列表里的某个设备Demo 读取该设备的 MAC 地址构造 WifiP2pConfig然后调用 connect()。系统随后进行 GO 协商期间会经历几次内部握手。协商完成后你会收到 CONNECTION_CHANGED 广播此时需要调用 requestGroupInfo() 拿到 GO 的 IP 地址。注意只有 GO 能拿到自己作为服务端的 IPClient 端通常在 WifiP2pInfo 里通过 groupOwnerAddress 字段拿到 GO 的地址。我第一次在模拟项目X里跑这块时犯了最经典的错误Client 端拿自己的 IP 去监听端口结果永远连不上。正确做法是 Client 端使用 groupOwnerAddress 作为 Socket 连接目标GO 端则在 0.0.0.0 上监听。官方 Demo 为了简化把传输逻辑做成了一台设备开 ServerSocket、另一台设备主动连接的模式本质上就是传统 TCP 通信只是 IP 来源从 P2P 信道里拿而已。2.3 官方 Demo 的协议栈分层和你的业务层应该怎么划分官方 Demo 里典型的类包括 MainActivity、WiFiDirectBroadcastReceiver、DeviceListFragment、DeviceDetailFragment、WiFiDirectService。如果你仔细看它们的职责会发现系统其实把「链路层」和「业务层」分得很清楚WifiP2pManager 和 BroadcastReceiver 属于链路层负责发现、连接、状态维护Socket 传输和业务数据属于业务层官方 Demo 用的是最原始的文本消息传输没有任何协议封装。这就引出一个重要的设计建议你从官方 Demo 迁移到自己的项目时不要把它当成品而要把它当参考实现。链路层可以保留并适度封装业务层应该全部替换掉。我自己通常会额外抽象一个 ConnectionManager 类对外暴露发现、连接、断开三个 suspend 方法内部把 WifiP2pManager 的 Callback 转成协程回调。这样业务层就不用再去订阅广播而是等待一个连接成功的对象然后直接通过 TCP 交换数据。3. 把官方 Demo 搬进 Android Studio目录结构、权限清单与最小运行配置这一章的实用价值在于拿到官方 Demo 源码后不要直接按下 Run。你需要先看懂它的工程切入点在哪、哪些文件是 AndroidManifest 必须声明的、哪些配置会直接影响能不能跑起来。在没有头绪的源码包面前按下面的顺序做能省下大半天时间。3.1 官方 Demo 的文件结构哪些类值得保留哪些可以删掉官方 Demo 的代码量不大但类与类之间的引用比较绕。一个典型的文件清单如下你可以对照自己的工程看哪些改动成本最低。文件清单表格文件职责是否建议保留MainActivity初始化 manager/channel注册广播接收器控制 Fragment 跳转保留但建议瘦身WiFiDirectBroadcastReceiver监听四个系统广播转发给 Activity保留可改造成独立类DeviceListFragment展示发现的对等设备列表保留UI 可以重写DeviceDetailFragment展示连接后的设备信息和传输入口建议删除业务迁移后替换WiFiDirectService封装 Socket 连接的 IntentService建议删除换成协程封装你可能会问为什么不直接照抄因为官方 Demo 的 DeviceListFragment 用的是 ListFragment 加自定义 adapter这种写法放在现代项目里太偏老而 WiFiDirectService 那个 IntentService 在传输大文件时很容易被系统限制。我建议只留下核心的链路层片段UI 和传输层完全可以按自己的技术栈重写。3.2 权限清单从 Android 6 到 Android 14缺一个都跑不起来Wi-Fi Direct 牵扯到 Wi-Fi 状态、位置信息、附近设备权限。不同 Android 版本对权限的要求差异很大这是新手最容易「翻车」的地方。通常 AndroidManifest 里需要声明以下权限权限用途最低版本要求ACCESS_FINE_LOCATION扫描附近 Wi-Fi Direct 设备时系统要求Android 6.0 及以上需要动态申请CHANGE_WIFI_STATE修改 Wi-Fi 状态启用 Wi-Fi Direct 扫描需要所有版本ACCESS_WIFI_STATE获取当前 Wi-Fi 状态所有版本INTERNET建立 TCP Socket 连接所必需所有版本NEARBY_WIFI_DEVICES替代部分位置权限的新版附近设备权限Android 13 及以上需要动态申请这里有个关键点Android 13 及以后的系统引入了 NEARBY_WIFI_DEVICES 这个运行时权限它看名字是普通权限但官方标注为「不需要位置权限即可使用附近的 Wi-Fi 设备」。不过如果你要兼容老版本估计还是得动态申请位置权限。我见过很多项目在 Android 14 设备上怎么都扫描不到设备查来查去最后发现是位置权限没有弹窗授权。另一个非常隐蔽的坑只声明权限但没在系统设置里打开「位置服务」开关同样收不到 PEERS_CHANGED 广播。因为 Android 的 Wi-Fi Direct 扫描接口依赖系统位置服务提供基础信息开关没开时扫描接口会静默失败。所以你在调试时必须主动判断权限和开关两个维度而不是只看代码逻辑。3.3 最小运行配置模拟器跑不了必须是两台真机很多人在模拟器上跑官方 Demo然后发现自己和自己当然搜不到任何设备。这不是你的代码有问题而是模拟器的无线网卡不支持和实体设备交互。正确测试方法是一台机器作为 GO、一台作为 Client或者准备两台真机。如果你的机型支持「双 Wi-Fi Direct」功能也可以拿同一台设备开两个 App 实例来模拟但不是所有机器都支持这个模式。我建议的测试配置是两台 Android 10 以上、支持 5GHz 频段的手机关闭其中一台的移动数据把两台设备放在一米内的距离。这样能排除频段和距离对扫描结果的影响。如果你手头只有一台设备那就退回到传统 TCP 测试先用 PC 开一个临时服务器把 App 改成连接固定 IP先把 Socket 链路跑通再回来测 P2P 发现。4. 一个可改可跑的最小实现从「扫描到对端」到「自定义字节传输」官方 Demo 是参考实现但它把「发现」和「传输」绑定在界面上不利于直接复用。这一章我会给出一套基于官方思路、但更利于改造成业务代码的最小实现。你可以把它视为官方 Demo 的「协程版」每段代码都贴出来并标注关键参数的含义。4.1 初始化 WifiP2pManager、Channel 与广播接收器的标准写法下面这段代码是 Wi-Fi Direct 的基础骨架。通常在 Application 或主 Activity 的 onCreate 里完成。public class P2pManagerHolder { private final WifiP2pManager manager; private final WifiP2pManager.Channel channel; private final BroadcastReceiver receiver; private final Context context; public P2pManagerHolder(Context context) { this.context context.getApplicationContext(); manager (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE); channel manager.initialize(context, context.getMainLooper(), null); receiver new P2pBroadcastReceiver(); context.registerReceiver(receiver, getIntentFilter()); } private static IntentFilter getIntentFilter() { IntentFilter filter new IntentFilter(); filter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); filter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); return filter; } }这段代码的关键点在 channel 的初始化。initialize() 的第二个参数是 Looper必须传主线程的 Looper不要自己 new 一个 HandlerThread 的 Looper否则所有回调都会脱离主线程UI 更新时容易引发崩溃。第三个参数是 ChannelListener用于监听通道关闭事件官方 Demo 里经常传 null但在实际业务里建议传一个回调在 channel 失效时重新初始化。registerReceiver 的调用时机也很重要。我一般放在 Application 的 onCreate 里而不是 MainActivity 里因为它要对整个应用生命周期负责。如果你放在 Activity 里每次旋转屏幕都会重复注册LeakCanary 马上就会报警。另外静态注册 BroadcastReceiver 在 Android 8.0 以后对隐式广播基本失效这里必须用动态注册。4.2 扫描设备与监听对等设备列表把回调改成协程扫描设备的传统写法是 manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {...})。问题在于回调是嵌套的业务代码一复杂就变成回调地狱。我习惯用 suspendCancellableCoroutine 把它包装成挂起函数。suspend fun discoverPeers(): Boolean suspendCancellableCoroutine { continuation - manager.discoverPeers(channel, object : WifiP2pManager.ActionListener() { override fun onSuccess() { continuation.resume(true) } override fun onFailure(reasonCode: Int) { Log.e(TAG, discoverPeers failed, reason: $reasonCode) continuation.resume(false) } }) }reasonCode是排错的重要线索WifiP2pManager.P2P_UNSUPPORTED表示设备不支持 Wi-Fi Direct你不用继续往下走了BUSY表示系统正在处理上一个请求需要稍后重试ERROR表示内部错误。发现成功之后通过requestPeers拿到WifiP2pDeviceList这里的 WifiP2pDevice 对象包含设备名、MAC 地址、状态和 capabilities。状态字段里有一个坑device.status 的取值可能是 AVAILABLE、INVITED、CONNECTED 等。如果设备已经连接它的列表中可能仍然显示状态为 INVITED 或 CONNECTED这并不代表当前可以发起新的连接。我建议在界面展示时只对 AVAILABLE 状态的设备开放点击事件。4.3 建立连接groupOwnerIntent 与连接超时处理当你从设备列表里选中一台设备后需要构造 WifiP2pConfig 并调用 connect()。fun connectToDevice(device: WifiP2pDevice) { val config WifiP2pConfig().apply { deviceAddress device.deviceAddress groupOwnerIntent 6 } manager.connect(channel, config, object : WifiP2pManager.ActionListener() { override fun onSuccess() { // 连接请求已发出真正成功要看 CONNECTION_CHANGED 广播 } override fun onFailure(reasonCode: Int) { // 常见 reasonCode: P2P_BUSY, P2P_NO_SERVICE } }) }groupOwnerIntent默认值是 7范围是 0 到 15。0 表示设备不希望成为 GO15 表示设备强烈希望成为 GO。如果两台设备同时发起连接可能会出现协商冲突所以我的习惯是主动发起连接的一端设为 6 到 10被连接的一端不调用 connect直接等待系统自动加入或通过createGroup来建立群组。deviceAddress是设备的 MAC 地址字符串注意不要传成deviceName。连接过程中最常见的现象是「一直在转圈最后超时」。这个超时不是 connect() 自动触发的系统默认的 Wi-Fi Direct 连接超时在 30 秒左右。你最好在业务层自己起一个 15 秒的定时器超时就提示用户。因为系统底层的超时在被 App 层面感知到之前用户早就失去耐心了。4.4 获取 Group Owner 信息与建立 Socket 传输连接成功后会收到 CONNECTION_CHANGED 广播典型的回调代码是下面的样子。fun handleConnectionChanged(networkInfo: NetworkInfo, p2pInfo: WifiP2pInfo) { if (networkInfo.isConnected p2pInfo.groupFormed) { if (p2pInfo.isGroupOwner) { // 本机是 GO监听固定端口 startServerSocket(PORT) } else { // 本机是 Client连接 GO 的 IP val address p2pInfo.groupOwnerAddress.hostAddress startClientSocket(address, PORT) } } else { // 连接断开 } }groupOwnerAddress在其他资料里也写作InetAddress它的值通常是一个 192.168.49.x 的局域网地址这是 Wi-Fi Direct 默认分配的网段。这里有一个特别值得注意的点Wi-Fi Direct 群组建立后GO 和 Client 处于同一个局域网段但该网段与设备原本连接的 AP 网段不是同一个所以不能直接使用设备原来的局域网 IP。Socket 传输部分使用传统的ServerSocket和Socket即可。不要在客户端刚连接时就立刻发送大块数据最好先做一个握手客户端发送一个自定义字节头如0xAA服务端收到后回复0xBB双方确认后再进入正式的数据传输。这段「先握手再传数据」并不是官方 Demo 的做法但对于业务系统来说它能显著减少连接刚建立时信道尚未稳定的丢包概率。5. 官方 Demo 的五个典型坑现象、根因与排错方法这一章我挑出自己在实操中反复碰到的问题按统一格式写现象描述根因分析解决方式。这些问题如果你没经历过很难从代码层面直接定位但它们几乎都会在集成官方 Demo 后的第一天出现。5.1 两台真机互相扫描不到设备常见现象两台设备都在主界面点「搜索设备」按钮没有任何反馈或短暂转圈后列表仍为空。Wi-Fi 和蓝牙都已打开系统权限全部授予。根因多数情况下不是 App 的问题。第一Android 6.0 以上的扫描依赖位置服务系统「位置信息」开关被关闭则 scanning 静默失败第二部分厂商 ROM 对 Wi-Fi Direct 有兼容性问题比如某些定制系统默认禁用了 5GHz 频段的 Wi-Fi Direct 扫描第三两台设备距离过远或中间有金属障碍物导致信号丢失。解决先确认系统 Wi-Fi 已经连接到一个网络不一定非要有外网但要处于 Wi-Fi 开启状态。再把两台设备距离缩短到半米以内重启 Wi-Fi 开关在系统设置里打开位置服务。如果还不行把通道里的groupOwnerIntent临时调到最大值重启一次扫描。我遇到过一个厂商设备必须完全关闭蓝牙才能扫描到对端属于极端兼容性问题只能做设备适配兜底。5.2 discoverPeers 回调成功但 peers 列表为空常见现象onSuccess()已经触发了说明系统已经开启了发现流程但等了一两分钟 PEERS_CHANGED 广播始终不来请求 peers 拿到的列表是空的。根因这个场景通常是目标设备处于休眠状态或没有进入可被发现状态。Wi-Fi Direct 的发现过程依赖目标设备周期性发送信标帧如果目标设备屏幕关闭时间较长系统会自动进入省电模式并关闭信标发送。另一种可能是设备已经处于某个群组中系统不会向新的扫描方暴露自己。解决确保目标设备屏幕保持常亮最好保持在系统自带的 Wi-Fi Direct 设置页并停留在那个界面。如果目标设备已经在别的群组中先退出该群组再回到可被发现状态。这里还有一个经验值连续调用discoverPeers的时间间隔不要小于 10 秒因为系统在频繁扫描时会触发节流机制导致回调迟迟不来。一旦你的代码里出现了循环调用discoverPeers把循环去掉改成「点击一次扫描一次」的模式。5.3 connect 后一直处于「邀请中」状态常见现象点击某台设备发起连接后目标设备也弹出了邀请确认框用户点了「接受」但连接状态始终停留在「邀请中」CONNECTION_CHANGED 广播迟迟不触发。根因系统在 GO 协商阶段发生了频繁的内部握手失败。常见原因有两种一是两台设备同时在对方的界面上发起 connect导致双方都尝试成为 GO协商轮流切换直到超时二是设备所处的信道拥塞特别是周围有大量 2.4GHz Wi-Fi 信号时协商报文丢失概率变大。解决在业务上设定明确的「主动方」和「被动方」。被邀请方不要自己调用connect()只需自动接受系统弹窗即可。主动方的groupOwnerIntent设为 8 到 10被邀请方设备不要设置该字段。如果你要做一个传输工具 App这个「谁发起连接」的规则应该由用户操作来隐式决定先点击对方设备的用户是主动方对方只要等待弹窗确认。这里的经验是connect()后两秒内目标设备如果没有弹出确认框主动方自己主动调用一次cancelConnect()再重试能杀掉很多悬挂状态。5.4 Socket 连接成功但数据收不到常见现象两台设备显示已连接TCP 也成功建立了对端连接但发送方传数据后接收方没有触发 read 回调或者过很长时间才收到一次。根因这块不太可能出在 Wi-Fi Direct 协议上而是你的传输代码把 Socket 的读取方式写错了。最容易犯的错误是只写了outputStream.write()但没有调用flush()。Java 的BufferedOutputStream只有在缓冲区满或显式 flush 时才会真正发数据。第二个错误是inputStream.read()在数据到来之前会阻塞你如果在主线程直接读取UI 会卡死。解决写数据后一定调用outputStream.flush()。读数据放到子线程典型写法是用while (true)循环去 read并用一个volatile boolean标志来控制退出。如果你传的是文本最好约定用\n作为消息结尾并readLine()。这种「两端协商一个结束符」的方式比固定长度字节头更容易排错。5.5 Activity 旋转屏幕后连接状态丢失界面不刷新常见现象连接已经建立旋转屏幕后界面回到初始状态设备列表清空连接状态变成「未连接」但 Wi-Fi Direct 实际上还保持着连接。根因官方 Demo 把所有状态都放在 Activity 里旋转导致 Activity 重建后WifiP2pManager.Channel对象失效广播接收器被注销状态自然全部丢失。Wi-Fi Direct 的底层连接并未被系统切断但你的应用已经没有通道去感知它了。解决把P2pManagerHolder或核心状态对象放到 ViewModel 或 Application 级别的单例中持有Channel只在初始化时创建一次。Activity 重建时通过 ViewModel 恢复状态再重新注册 BroadcastReceiver。如果你的项目用了 AppCompat还要注意 Android 13 上动态广播注册会增加RECEIVER_EXPORTED参数Wi-Fi Direct 广播属于系统广播通常不需要 exported 标志传Context.RECEIVER_EXPORTED即可。6. 进阶改造从 Demo 到可用模块的三项值得做的事官方 Demo 里有一句话很重要它演示的是链路层的用法不提供企业级的稳定性。当你决定在业务里正式使用 Wi-Fi Direct 时至少要完成以下三项改造。第一项改造是建立自己的连接超时与重试机制。上面提到的连接超时只是表面问题更深层的是设备在出现异常状态时系统很容易把后续请求都标记为BUSY。我的做法是封装一个ConnectionSupervisor它会监控所有 ActionListener 回调如果连续三次收到BUSY强制重启 Wi-Fi Direct 通道并重置状态机。看起来不复杂但在生产环境里能减少一半的客服反馈。第二项改造是把文件传输和消息传输分开。Wi-Fi Direct 本身和 TCP 的传输能力是绑定在一起的但大文件传输时直接写Socket很容易阻塞最好引入一个简单的分包协议固定 8 字节包头表示总长度和当前分片序号每次分片大小设为 64KB。Wi-Fi Direct 的实际吞吐量在不同设备上差异很大有的旧设备只能跑 5 MB/s但在一些手机上能做到 20 MB/s不要按理论值去设并发数或缓冲区。第三项改造是增加一个设备级的自检接口。这个接口用来在正式通信前确认对方 App 是否在线。我之前在模拟项目X里就是一个 Android 端和一个嵌入式 Linux 端互相连嵌入式端不懂 Android 的 Wi-Fi Direct 附加服务于是双方约定连接建立后先发一个PROBE_DEVICE指令收到PROBE_RESPONSE后才开始正式业务。这个「应用层 ping」机制可以过滤掉大量「连接成功但业务不可用」的假象。最后说一个我踩过的坑连接建立后系统可能因为网络切换或休眠而悄悄断开底层的 P2P 群组但你的 App 完全没有收到通知。Wi-Fi Direct 默认不提供心跳机制所以我在业务层每秒发送一次心跳数据连续三次没有回应就判定连接失效并自动发起重连。这个机制后来救过我很多次尤其是在设备被用户锁定屏幕后系统会进入省电模式可能导致群组被系统自动关闭。维护一个 8 字节的心跳包成本极低但带来的稳定性提升非常明显。如果你刚接触 Wi-Fi Direct建议先按第五章的清单把官方 Demo 完整跑通一次别急着设计复杂的业务协议。链路层的手感和坑是写不出来的只能靠真机反复试。等到你能闭着眼睛说出「现在设备处于什么状态、下一步该调用什么 API」的时候再动手改造也不迟。希望这些经验能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑