Android Wi-Fi P2P官方Demo深度解析:从连接到自建传输通道

📅 发布时间:2026/10/15 19:39:29
Android Wi-Fi P2P官方Demo深度解析:从连接到自建传输通道
简介安卓官方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 Mbps10 米内配对流程低小数据同步、音频BLE数百 Kbps20 米左右广播扫描极低传感器、配网指令传统热点几十 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:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.CHANGE_NETWORK_STATE / uses-feature android:nameandroid.hardware.wifi.direct android:requiredtrue /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_ACTIONP2P 功能开关变化刷新 UI提示当前开关状态WIFI_P2P_PEERS_CHANGED_ACTION附近设备列表发生变化调 requestPeers 拿最新列表WIFI_P2P_CONNECTION_CHANGED_ACTION连接建立或断开调 requestConnectionInfo 拿 IPWIFI_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通常用主线程 LooperWifiP2pManager 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) { ListWifiP2pDevice devices new ArrayList(peers.getDeviceList()); // 过滤掉状态不是 AVAILABLE / INVITED 的设备 ListWifiP2pDevice 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-1515 表示本机尽量成为 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 和 groupOwnerAddressmanager.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遇到问题就对照广播时序排查。这套流程帮我挡掉了起码一半的“连不上”玄学。希望帮到你。本文还有配套的精品资源点击获取