简介:安卓官方Wi-Fi P2P示例工程基于谷歌WiFiDirectDemo整理,属于完整可直接运行的安卓工程压缩包,面向需要实现设备点对点直连、局域网高速传输、文件共享与联机功能的开发者。资源共88个文件、约244KB,包含Java源码、XML布局与配置、图片资源,以及编译生成的class、dex、apk等文件,工程中还附带项目配置文件和示例文件,结构紧凑,便于导入工程查看运行效果。目前已有781人学习浏览。示例覆盖设备发现、群组形成、连接建立、文件传输和群组管理等核心环节,详细演示了WifiP2pManager、WifiP2pDeviceList等关键API的调用顺序与异步回调写法,并给出权限配置、资源释放等注意事项,同时说明了设备发现和群组状态变更等典型场景的处理方式。通过学习,读者可以掌握安卓平台点对点通信的底层机制,快速将官方能力迁移到文件快传、多媒体共享、智能外设控制等实际场景。建议具备基础安卓开发经验的工程师结合代码动手实践。
1. Android官方Wi-Fi P2P Demo:一套不传文件的示例,为什么值得反复看
如果你正好在找 Android 官方 Wi-Fi P2P Demo,多半是想做两台设备之间的免流量直连:传文件、分享画面、给硬件配网。但打开这套示例之后,最常见的反应是——设备能搜到,点一下就断,界面跳两下又回到列表,文件传输的入口根本找不着。这套 Demo 真正的价值,不在“传输文件”这个业务动作,而在于它把 Wi-Fi Direct 的“发现—请求—连接—拿到 IP”整条状态机完整演示了出来,业务层留给你自己填。适合做局域网直连、IoT 配网、想在蓝牙和传统热点之外找高带宽方案的 Android 开发者。把它吃透,等于把设备点对点通信的地基打了一遍。
2. 协议与框架:Wi-Fi P2P 在蓝牙与热点之间怎么选
2.1 三种近距离通信方案对照:投入产出比最关键
做设备直连时,我习惯先列一张对比表,把 Wi-Fi P2P、蓝牙、传统热点放在一起看,而不是凭印象选。三种方式都能让两台设备在无外网环境下交换数据,但各自的成本、吞吐、发现方式完全不同。
| 选型 | 实际带宽 | 典型距离 | 设备发现方式 | 功耗 | 主要适用 |
|---|---|---|---|---|---|
| Wi-Fi P2P | 几十 Mbps 量级 | 室内 50 米左右 | 自动发现设备列表 | 高 | 大文件传输、低延迟互投 |
| 经典蓝牙 | 1-3 Mbps | 10 米内 | 配对流程 | 低 | 小数据同步、音频 |
| BLE | 数百 Kbps | 20 米左右 | 广播扫描 | 极低 | 传感器、配网指令 |
| 传统热点 | 几十 Mbps 量级 | 热点信号范围 | 手动开热点、输入密码 | 极高 | 多设备共享网络 |
Wi-Fi P2P 最大的优势是“不需要外部 AP,也不需要用户手动输入 SSID 和密码”。传统热点虽然带宽高,但要求一端打开便携热点、另一端去 Wi-Fi 列表里找名称输密码;蓝牙又太慢。P2P 把建立链路的动作收窄成“搜到对方、点一下连接、对端确认”,体验顺滑得多。
代价是功耗明显高于前两者,而且上层很多业务逻辑要自己补。很多项目选蓝牙,就是因为 P2P 表面上“连上了”,结果发现连接过程的可用性比想象中敏感。我的选择标准很简单:如果传输量在几百 KB 以内,用 BLE;如果要传几十 MB 以上文件,且两端都是 Android,那 Wi-Fi P2P 值得优先试。
2.2 官方 Demo 的组成:权限声明、广播接收器与三层回调
官方 Demo 从结构上分为三块:Manifest 里的权限与 feature 声明、一个监听系统 P2P 事件的 BroadcastReceiver、以及一组 WifiP2pManager 回调接口。先把权限和 feature 的声明抄过来:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-feature android:name="android.hardware.wifi.direct" android:required="true" />ACCESS_WIFI_STATE 负责读取扫描结果和 P2P 状态,CHANGE_WIFI_STATE 负责发起 P2P 操作。ACCESS_FINE_LOCATION 在 Android 10 之后是硬性要求,因为系统把设备列表当成位置信息,不加这个权限,扫描永远返回空。CHANGE_NETWORK_STATE 在部分厂商实现里也必须要有,缺失时 connect 可能静默失败。uses-feature 声明意思是“这个应用必须运行在支持 Wi-Fi Direct 的设备上”,应用商店会据此过滤设备。
广播接收器这边,官方示例注册了四个系统动作:
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); registerReceiver(receiver, filter);这个代码块有两个关键点。第一,四个 Action 是全局性的,应用即使不从 WifiP2pManager 发起任何操作,系统也可能因为别的应用或系统设置变化而广播 P2P 状态,所以接收器里第一步永远是判断 action,不要默认这是自己操作的结果。第二,registerReceiver 的调用时机放在 onResume 里、unregisterReceiver 放在 onPause 里,防止 Activity 不可见时还在收广播导致内存泄漏。
2.3 四个广播各自触发时机:对不上时序就白干
很多人在 Demo 里改来改去,问题出在搞不清四个广播什么时候来。这里列一张触发时机表,平时调试直接对照。
| Action | 触发时机 | 该做的事 |
|---|---|---|
| WIFI_P2P_STATE_CHANGED_ACTION | P2P 功能开关变化 | 刷新 UI,提示当前开关状态 |
| WIFI_P2P_PEERS_CHANGED_ACTION | 附近设备列表发生变化 | 调 requestPeers 拿最新列表 |
| WIFI_P2P_CONNECTION_CHANGED_ACTION | 连接建立或断开 | 调 requestConnectionInfo 拿 IP |
| WIFI_P2P_THIS_DEVICE_CHANGED_ACTION | 本机设备信息变化 | 刷新本机名称、状态显示 |
注意 PEERS_CHANGED_ACTION 不是 discoverPeers 的 onSuccess 回调里立刻触发的。discoverPeers 的 onSuccess 只代表“扫描请求发出成功”,不代表“扫描结果已经回来”。正确写法是在 onSuccess 里等待 PEERS_CHANGED 广播,收到后再去拿设备列表。如果顺序写反,界面上设备列表永远不会更新。
2.4 Demo 的边界:它真的不负责传文件
官方那个跑起来只能看到设备列表、点一下能连接成功的示例,并不包含文件发送代码。数据通道要自己用 Socket 实现。有人觉得这是官方偷懒,其实恰恰是刻意设计:P2P 状态机是所有业务共用的基础,而传文件、传视频、远程控制各自协议不同,绑在一起反而难复用。
系统在连接建立后,会在一方生成 Group Owner 角色,并且两端都会拿到一个局域网 IP。业务层完全可以把 Group Owner 当服务端、另一台当客户端,按 TCP Socket 的方式自由交互。也就是说,官方 Demo 的价值在有链路没业务,我们要做的就是把业务补上去,而不是在官方代码里翻有没有现成的发送方法。
3. 跑通官方 Demo:从环境到首屏一次走通
3.1 环境:模拟器基本不行,真机一定要两台
先解决一个误解:模拟器上跑这个 Demo,大多数情况下是跑不通的,因为模拟器的虚拟 WLAN 不实现 Wi-Fi P2P 协议。我见过有人在模拟器上反复调试,折腾一晚上,最后换真机一次通过。要做这款 Demo,准备两台 Android 真机,都连上同一个 Wi-Fi 或者不连 Wi-Fi 都可以,但必须打开系统 Wi-Fi 开关。
版本方面,P2P 从 API 14 就支持,但不同 targetSdk 下的权限行为差异很大。建议按表格对照自己的目标版本:
| targetSdk 区间 | 权限要求与行为 |
|---|---|
| 28 及以下 | 声明 ACCESS_FINE_LOCATION,系统较少校验位置开关 |
| 29-32 | 动态申请 ACCESS_FINE_LOCATION,且系统位置服务必须打开 |
| 33 及以上 | 可声明 NEARBY_WIFI_DEVICES 并有条件的替代位置权限,实测中部分设备仍要看位置开关 |
这里最常翻车的是 Android 10 以上。我一般调试时先把 targetSdk 临时设置成 29-32 区间,把权限和系统位置开关都打开,跑通后再改回生产 targetSdk 做兼容回归。不是用低 targetSdk 规避问题,而是为了减少变量。
注意:两台设备最好都停留在应用界面里操作,某些定制 ROM 会在一台设备熄屏后自动休眠 P2P 扫描,导致另一端永远搜不到。
3.2 从导入工程到命令行安装:编译安装走一遍
拿到官方示例源码后,Android Studio 直接打开即可,一般同步完 Gradle 就能编。编译产物路径在 app/build/outputs/apk/debug/ 下。我习惯用命令行安装,比 IDE 弹窗排查问题更可控:
adb install -r app-debug.apk adb shell am start -n com.example.android.wifidirect/.WiFiDirectActivity-r 参数表示覆盖安装,避免重复安装时提示版本冲突。am start 的 -n 参数格式是“包名/Activity 全类名”,示例包名是 com.example.android.wifidirect,实际以你导入的工程为准。如果 Activity 路径写错,会报 ActivityNotFoundException。
打开之后用日志过滤器观察 P2P 状态机:
adb logcat -s WifiP2PManager WifiP2PService-s 参数表示只看指定 tag。Wi-Fi Direct 相关日志常见 tag 是 WifiP2PManager 和 WifiP2PService,如果过滤后没有输出,去掉 -s 看全局日志,因为部分厂商把日志打到了其他 tag。
3.3 首屏操作步骤:按钮边界与正确手势
官方 Demo 跑起来之后,界面一般有搜索、列表、连接状态几块区域。首屏操作按这套顺序走:
- 打开应用,先检查本机状态区域是否显示“Wi-Fi Direct 已启用”,没有就先去系统设置打开 Wi-Fi。
- 点击搜索按钮,此时 discoverPeers 被触发,界面上可能有一小段等待。
- 等待设备列表刷新,列表里出现对端设备名称后,点击目标设备。
- 对端可能会弹出连接邀请弹窗,点击接受。
- 等待连接状态变化,连接成功后界面显示已连接。
这里有一个经验:搜索动作发出后,成功率最高的窗口大约在一分钟以内。如果对端没有及时出现,或者搜索到之后隔了很久才点连接,系统很可能会把设备标记成过期,connect 时返回失败。我习惯让两台设备都重新触发一次搜索,再在列表新鲜时立刻点连接。
如果上面全部做完还是连不上,不要反复点按钮,先看 logcat 里的 failure 返回值,常见的 BUSY 与 ERROR 对应的处理方式不一样,这一点下一章展开讲。
4. 核心链路拆解:从 discoverPeers 到 GROUP_OWNER 的完整血线
4.1 初始化与发起扫描:两行代码背后有四层状态
业务对接 P2P 的第一步是拿到 WifiP2pManager 和 Channel。Channel 可以理解为应用与系统 P2P 服务之间的连接通道,初始化时指定回调运行的 Looper,通常用主线程 Looper:
WifiP2pManager manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE); Channel channel = manager.initialize(this, Looper.getMainLooper(), new ChannelListener() { @Override public void onChannelDisconnected() { // 通道断开,需要重新 initialize } }); if (manager == null) { // 当前设备不支持 Wi-Fi P2P return; } manager.discoverPeers(channel, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { // 扫描请求已发出,等待 PEERS_CHANGED 广播 } @Override public void onFailure(int reason) { // reason 常见为 P2P_UNSUPPORTED / BUSY / ERROR } });逻辑上是三步:先拿 Manager,再初始化 Channel,最后调 discoverPeers。Channel 要复用,不要每次操作都重新 initialize,否则系统侧的 P2P 状态容易错乱。
onFailure 里的 reason 参数值得打印出来。BUSY 表示系统 P2P 服务正在处理上一个请求,需要等它完成再发起下一次操作;P2P_UNSUPPORTED 表示硬件不支持,只能提示用户换设备;ERROR 属于通用错误,多数情况下重启 Wi-Fi 开关可以恢复。
4.2 PeerListListener:设备列表的刷新逻辑
收到 PEERS_CHANGED 广播后,调用 requestPeers,系统通过 PeerListListener 回调返回当前扫描到的设备列表:
private class MyPeerListListener implements WifiP2pManager.PeerListListener { @Override public void onPeersAvailable(WifiP2pDeviceList peers) { List<WifiP2pDevice> devices = new ArrayList<>(peers.getDeviceList()); // 过滤掉状态不是 AVAILABLE / INVITED 的设备 List<WifiP2pDevice> available = new ArrayList<>(); for (WifiP2pDevice device : devices) { if (device.status == WifiP2pDevice.AVAILABLE || device.status == WifiP2pDevice.INVITED) { available.add(device); } } adapter.setData(available); } }设备列表里会包含附近所有支持 Wi-Fi Direct 的设备,包括对方手机、打印机、电视盒子。不区分类型就全量展示,容易让用户点到不支持的设备。过滤条件可以按设备类型做,但至少要把状态为 UNAVAILABLE 的过滤掉。
onPeersAvailable 回调运行在初始化时指定的 Looper 上。如果用的是主 Looper,就不要在回调里做文件操作或数据库读写,否则列表刷新会卡顿。
4.3 connect 参数与 Group Owner 协商:0 到 15 的含义
点击列表中的设备后,构造 WifiP2pConfig 并发起连接:
WifiP2pDevice targetDevice = (WifiP2pDevice) adapter.getItem(position); WifiP2pConfig config = new WifiP2pConfig(); config.deviceAddress = targetDevice.deviceAddress; config.groupOwnerIntent = 15; // 0-15,15 表示本机尽量成为 Group Owner manager.connect(channel, config, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { // 连接请求已发出,结果看 CONNECTION_CHANGED 广播 } @Override public void onFailure(int reason) { // 常见 BUSY:上一次操作还没结束就再次调用 connect } });groupOwnerIntent 是单个最容易被忽略的参数。它表示本机成为 Group Owner 的意愿强度,0 表示最不想当,15 表示最想当。两端协商时会根据这个值决定谁最终成为 Group Owner。如果你明确知道自己这端要当服务端,把它设为 15;对端设成 0。两端都设 15 会额外增加协商时间,两端都设 0 时系统可能选择困难,表现为连接过程偏慢。
connect 的 onSuccess 只代表连接请求发出,不代表连接成功。实际结果通过 WIFI_P2P_CONNECTION_CHANGED_ACTION 广播通知,随后再调 requestConnectionInfo 确认状态。
4.4 拿到 GROUP_OWNER 地址:连接的临门一脚
连接广播到达后,requestConnectionInfo 回调里会拿到 WifiP2pInfo,其中包含关键的两个字段:groupFormed 和 groupOwnerAddress:
manager.requestConnectionInfo(channel, new WifiP2pManager.ConnectionInfoListener() { @Override public void onConnectionInfoAvailable(WifiP2pInfo info) { if (!info.groupFormed) { return; // 连接还没真正建立 } String goAddr = info.groupOwnerAddress.getHostAddress(); if (info.isGroupOwner) { // 本机是服务端,创建 ServerSocket 等待连接 } else { // 本机是客户端,主动连接 goAddr 的指定端口 } } });groupFormed 为 false 时一定不要去建 Socket,因为链路还没通。isGroupOwner 用 boolean 直接区分角色,不要靠 deviceAddress 去猜。拿到 goAddr 后,两端就可以按 TCP 客户端/服务端模型建立数据通道。
这里我吃过一次亏:两端都把自己当成“主动发送方”,同时往对方去 connect,结果谁也没监听端口,永远连不上。正确做法是 Group Owner 端先 ServerSocket accept,客户端端再去 connect goAddr,角色分明。
5. 避坑指南:兼容性、掉线与线程三座山
5.1 扫描结果永远为空:先从系统位置服务查起
现象:discoverPeers 的 onSuccess 正常触发,但 onPeersAvailable 拿到的列表是空的,反复点搜索都一样。
原因:Android 10 以上系统把 Wi-Fi 扫描结果视为位置信息。只授予 ACCESS_FINE_LOCATION 权限还不够,系统级“位置服务”开关必须处于打开状态。不少定制 ROM 甚至会联动权限管理器拦截扫描结果。
解决:动态申请 ACCESS_FINE_LOCATION 只是第一步,第二步要跳转系统设置,引导用户打开位置服务。为了方便联调,我还会在调试版本里把 targetSdk 临时降到 28 做对照,确认是不是系统位置开关导致的。正式版本不要试图绕过这个限制,按系统要求来是最省事的。
5.2 connect 报 BUSY:状态机没复位
现象:点搜索后立刻点设备,connect 的 onFailure 返回 BUSY;或者 UI 上按钮连点,第二次操作直接失败。
原因:WifiP2pManager 同一时间只接受一个 P2P 操作。discoverPeers 还没执行完,connect 就排队进来,被系统判定为 BUSY 丢弃。
解决:所有 P2P 操作按钮在发起后立即置灰,在 ActionListener 的 onSuccess / onFailure 回调里再恢复可点。同时不要连续点搜索,至少等上一轮 PEERS_CHANGED 处理完再发起下一轮。
5.3 连接后几十秒断开:锁屏、休眠与省电策略
现象:两端都显示已连接,传输还没开始就断了;或者传输过程中一方锁屏,链路立刻中断。
原因:锁屏后 Wi-Fi 模块进入省电模式,P2P 组被系统回收;部分设备把 Group Owner 当热点对待,熄屏一段时间后自动关闭。
解决:在连接期间保持屏幕常亮,通过 Window 的 FLAG_KEEP_SCREEN_ON 实现。如果要做长时间的传输任务,把整体流程放到前台服务里,并申请一次 PARTIAL_WAKE_LOCK。两端都要做,只保一端不够。
5.4 回调里操作 UI 偶发崩溃:Looper 没过脑子
现象:onConnectionInfoAvailable 回调里直接弹出对话框或刷新列表,偶尔会崩溃,报错指向主线程更新 UI。
原因:initialize 时如果没有指定主线程 Looper,或者系统 binder 回调在某些设备上跨线程分发,回调线程就不是 UI 线程。虽然多数设备上表现正常,但偶发的那一次最难排查。
解决:initialize 第二参数固定传 Looper.getMainLooper();回调体里依然用 runOnUiThread 包一遍,形成习惯性写法。这一个动作成本很低,能挡掉很多“偶然”的崩溃。
5.5 个别设备功能被阉割:先兜底再升级
现象:同一份代码,在设备 A 上一切正常,在设备 B 上 manager.isAvailable() 直接返回 false,或者每次调用都抛 UnsupportedOperationException。
原因:部分定制 ROM 把 Wi-Fi P2P 能力做了裁剪,或者把 P2P 入口合并到了热点功能里,应用层拿不到完整服务。
解决:应用启动时先检查 WifiP2pManager 是否为 null,再看 isAvailable()。两者任一不满足,给用户显示明确提示并跳转到系统 Wi-Fi 设置页,而不是直接让应用崩溃。这块兜底逻辑建议写成独立工具方法,所有调用 P2P 的页面统一走它。
6. 把官方 Demo 改造成自己的传输骨架:三个小改动
拿到官方 Demo 只是拿到了“能连上”的骨架,真正要变成能传数据的工具,至少还要做三个改动。
第一个改动是保活。Android 8 以后,App 切到后台会被限制网络活动,P2P 连接很容易在一分钟内被系统回收。把连接管理和传输逻辑放到前台服务里,加一个常驻通知,系统就不会轻易杀进程。记得在 Manifest 里声明 FOREGROUND_SERVICE 权限,并在服务启动后 startForeground 传入通知。
第二个改动是封装数据通道。官方 Demo 没有 Socket 层,需要自己定义一个接口把收发动作抽象出来。我一般这样设计:
public interface DataChannel { void send(byte[] data); void setListener(Listener listener); interface Listener { void onData(byte[] data); void onDisconnect(); } }Group Owner 端实现这个接口时,内部开一个 ServerSocket 等待客户端接入;客户端端实现时,主动连接 goAddr。业务层只依赖 DataChannel 接口,不关心对方是服务端还是客户端,传输协议将来怎么换都不影响上层逻辑。
第三个改动是心跳保活。P2P 链路在长时间没有数据交互时,可能被系统或者对端设备的省电策略回收。最简单的方式是每隔 10 秒发一个一字节的控制帧:
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { channel.send(new byte[]{0x01}); } }, 10000, 10000);这个心跳间隔在功耗和保活效果之间比较平衡,再短的话会让设备长时间无法休眠。
以前我拿到这套官方 Demo 时,第一件事也是去界面里找文件传输按钮,翻了半天,最后才意识到它只负责把路修好,货要自己搬。从那以后,我每次做直连需求都先强制走一遍“发现—连接—拿到 GROUP_OWNER 地址”的闭环,确认两端都能看到连接信息,再写业务 Socket,遇到问题就对照广播时序排查。这套流程帮我挡掉了起码一半的“连不上”玄学。希望帮到你。
本文还有配套的精品资源,点击获取