☰
Qt读Android GPS:UDP实现定位数据实时采集与显示
2026/10/15 14:34:00 网站建设 项目流程

简介:面向需要在Qt环境中读取Android手机GPS信息的开发者,这份小巧的工程示例提供了完整可运行代码。工程包含5个文件,其中两个cpp源文件分别负责主窗口逻辑与GPS数据解析处理,两个h头文件声明相关接口与数据结构,一个pro文件配置Qt工程构建环境,整个压缩包仅4KB,便于快速查看和移植。通过该工程,可以学习如何获取经度、纬度、速度、定位精度等GPS字段,并利用UDP协议将采集到的定位信息发送到指定网络目标,适用于Android车载应用、定位调试工具或物联网设备间位置共享等场景。代码结构简洁,注释清晰,适合直接对照移植和二次开发。目前已有2058人浏览学习,对于刚接触Qt移动开发或需要快速实现GPS网络传输的读者,这份示例项目能提供直接的代码参考和实现思路。

1. 用QT读Android GPS:手机定位源加UDP管道

QT读取Android GPS信息,是车载调试和户外采集里很实用的一套组合:PC没有定位模块,Android手机自带GPS,一套链路下来就能把定位数据实时搬到QT上位机上。这条链路由三部分组成:手机端用LocationManager拿经纬度和速度,DatagramSocket走UDP局域网发送,PC端用QUdpSocket绑定端口接收解析,最后显示到界面上。它能直接解决车载轨迹记录、无人机地面站、移动端采集监控这些场景的上位机定位问题。适合手里有Android手机和Qt环境、想快速搭定位采集终端的开发者;新手照步骤能跑通,熟手可以直接拿走数据格式约定和几个定位坑。

2. Android端GPS采集:LocationManager选型、权限申请与参数调优

2.1 选型:为什么用LocationManager而不是FusedLocationProvider

第一次做这个项目时,我直接选了FusedLocationProvider,因为网上教程清一色推荐它。真机跑起来才发现问题:某品牌平板没有内置Play服务,应用启动就崩;另一台手机能起来,但定位回调十几秒才一次,完全没法用于采集。后来换成Android框架自带的LocationManager,这些问题彻底消失。

LocationManager是Android系统framework层的位置管理器,不依赖任何三方服务,直接对接底层GNSS芯片。GPS_PROVIDER模式下拿到的就是纯卫星定位结果,数据口径干净;NETWORK_PROVIDER模式走基站和Wi-Fi辅助,室内也能给出结果,但精度在几十米量级,不能当真实GPS用。FusedLocationProvider的卖点是多源融合、更省电、定位更快,但底层依赖Google Play services,在国产ROM上这个依赖不可控,出问题就抓瞎。

从测试角度还有一个关键区别。做车载定位调试,很多时候要验证信号遮挡对定位的影响,比如过隧道、走高架下。用GPS_PROVIDER能明确知道数据来自纯卫星定位,可以和已知路径做对比;如果用FusedLocation,融合算法会把卫星、基站、Wi-Fi混在一起,数据来源像一个黑匣子,测试结论就不干净。所以这类上位机调试场景,选LocationManager不是情怀,是可控性和可测试性决定的。

2.2 权限声明与运行时授权:两个权限,两个坑

AndroidManifest.xml里加三个权限声明:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" />

权限说明:ACCESS_FINE_LOCATION是精确位置权限,GPS定位必须持有;ACCESS_COARSE_LOCATION是粗略位置权限,作为网络定位兜底;INTERNET是UDP发送数据包的前置条件。三个权限声明少了任何一个,系统不会报错,但定位或网络功能会在运行时静默失效——进程正常启动、界面正常显示,就是不出数据,这种无声故障排起来最费时间。

Android 6.0之后定位权限属于危险权限,必须动态申请:

private static final int REQUEST_LOCATION = 1001; private void checkAndRequestPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_LOCATION); } }

代码说明:checkSelfPermission先检查是否已授权,未授权就调requestPermissions拉起系统授权弹窗。两个危险权限一起传入,Android会合并成一个授权弹窗,用户点一次允许全部通过。REQUEST_LOCATION是请求码常量,在回调里用它区分是哪一次权限请求。

授权回调里最容易被忽略的是"拒绝并不再询问"分支。用户拒绝两次后,系统不再自动弹窗,requestPermissions直接返回拒绝结果。这时必须跳转应用设置页让用户手动开启,代码这样处理:

@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode != REQUEST_LOCATION) return; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); return; } } }

参数说明:grantResults数组与permissions数组一一对应,循环里只要有一个拒绝就进入跳转。ACTION_APPLICATION_DETAILS_SETTINGS是Android标准的应用详情页动作,会打开当前应用的权限管理界面。实测在多数ROM上都能正常跳转,少数定制系统路由不同,但最终都能进到应用详情页。

这里还要提醒一个Android 10之后的新变化:系统把定位权限拆成了前台定位和后台定位两级,日常使用中的前台定位权限就够。如果应用要在后台持续采集GPS,比如锁屏后继续记录轨迹,需要在权限页额外打开"始终允许"选项,也就是后台位置权限。这个权限只能由用户在设置页手动打开,代码无法弹窗申请,交互流程要在应用里做好引导,否则系统版本升级后定位在后台会频繁被打断。

2.3 GPS数据采集核心代码与参数调优

GpsCollector封装类的核心逻辑:

public class GpsCollector { private LocationManager locationManager; private LocationListener locationListener; private volatile double latitude = 0.0; private volatile double longitude = 0.0; private volatile float speed = 0.0f; private volatile long lastFixTime = 0L; public void start(Context context) { locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } locationListener = new LocationListener() { @Override public void onLocationChanged(Location loc) { latitude = loc.getLatitude(); longitude = loc.getLongitude(); speed = loc.getSpeed(); lastFixTime = System.currentTimeMillis(); } }; locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 1000, 1, locationListener); } public void stop() { if (locationManager != null && locationListener != null) { locationManager.removeUpdates(locationListener); } } public double getLatitude() { return latitude; } public double getLongitude() { return longitude; } public float getSpeed() { return speed; } public long getLastFixTime() { return lastFixTime; } }

代码说明:requestLocationUpdates四个参数依次是provider、最小回调间隔毫秒、最小移动距离米、监听器。1000毫秒表示每秒至多上报一次;1米表示位置变化超过1米才触发,静止时不出数据,省电且能过滤抖动。经纬度字段用volatile修饰,因为定位回调运行在Binder线程,而发送线程读取在主线程或独立线程,volatile保证写入立即可见,不加的话可能读到缓存旧值。

参数调节看场景。车载测试车速快,1000毫秒间隔合适;徒步采集想要密集点位,调到500毫秒,但耗电明显上涨;只要路线走向不关心细节,拉到2000毫秒也可以。minDistance不建议设0,设0任何微小抖动都会上报,坐标漂移会非常明显;1米是精度和稳定性的平衡点。

实际采集时,还要关注卫星数和定位精度。卫星数可以通过GpsStatus.Listener获取,精度则直接用Location的hasAccuracy判断。一个有参考价值的处理是:当定位精度低于30米时,认为这次定位不可靠,直接丢弃这一帧。

if (loc.hasAccuracy() && loc.getAccuracy() > 30.0f) return; if (loc.getLatitude() == 0.0 && loc.getLongitude() == 0.0) return;

这两行拦截代码加在onLocationChanged最前面,绝大部分无效数据都会被挡掉。第一行过滤精度差的数据,第二行过滤冷启动阶段默认输出的(0,0)坐标。有条件的话还要采集UTC时间戳,后续CSV分析时按时间排序能对应当地时间,这一步看起来不起眼,记录多了就知道时间戳有多重要。

2.4 后台采集与锁屏保活:GPS线程卡住的常见原因

把GpsCollector放进Service做长时间采集时,经常遇到锁屏后数据中断的问题。原因是屏幕关闭后CPU进入休眠,Binder定位线程和UDP发送线程一起停摆。常见做法是拿一个PARTIAL_WAKE_LOCK唤醒锁,锁屏后CPU继续运行,GPS和UDP都能正常工作。

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "gps:wakelock"); wakeLock.acquire();

代码说明:PARTIAL_WAKE_LOCK只保持CPU运行、不点亮屏幕,是定位采集场景的标准选择。注意这个锁同一时间只能持有一个,多线程重复获取会出问题,用完必须release。另一个保活手段是前台服务:Android 8.0之后后台Service熄屏会被系统限制,startForegroundService是标配,配合常驻通知栏的"GPS运行中"通知,既保活也向用户透明说明耗电原因。Service方案的完整代码比较多,这里先把锁和前台服务的思路讲清楚,实现时照这个方向走就行。

3. UDP传输链路:JSON格式、发送代码与通路自检

3.1 为什么选UDP而不是TCP

这个场景选UDP有三个理由。第一是实时性:GPS数据是周期性上报,每秒一次,UDP无重传、无拥塞控制,发出即走,内网延迟通常低于1毫秒;TCP如果遇丢包会暂停发送等待重传,表现是数据突然中断再突增,定位轨迹出现锯齿。第二是简单性:UDP无连接,手机端不需要握手、断线重连和心跳保活,几十行代码就能完成;TCP要维护连接状态,手机切换网络或系统休眠都会导致连接断开,重连逻辑会明显增加代码量。第三是丢包容忍:GPS数据是连续流,一次丢包下个周期马上补偿,界面基本无感,不需要重传机制。

边界也要讲清楚。如果传输的是控制类指令,比如无人机的返航命令、设备的参数配置,可靠投递是硬需求,这时必须用TCP或带ACK确认的应用层协议。但上位机读GPS属于单向数据流,UDP的弱可靠性反而带来低延迟和简单性。

3.2 数据格式约定:JSON字段、精度与兼容性

自己定数据格式时,一定要先定JSON结构,别一开始就上二进制。我常用的字段结构:

{ "id": "phone-gps", "type": "gps", "lat": 30.1234567, "lon": 120.7654321, "speed": 1.25, "bearing": 89.6, "satellites": 8, "time": 1719567890123 }

字段说明:lat和lon是WGS84坐标系下的经纬度,浮点保留7位小数,对应约1厘米的定位精度;speed是速度,单位m/s;bearing是航向角,范围0到360度,正北为0;satellites是当前参与定位的卫星数;time是UTC毫秒时间戳,用于数据对齐和轨迹回放排序。

为什么用JSON?后续需求几乎一定会变,比如加海拔、加定位精度、加provider类型,JSON加字段不破坏已有解析逻辑,PC端只需多取一个key。二进制结构体添加字段会导致两端字节流对齐全乱,排查成本极高。GPS数据每秒一次,JSON比二进制多出的几十字节完全可以忽略。字段名要固定成英文字符串,禁止用中文或数字开头,这样PC端解析时少踩编码坑。

3.3 手机端UDP发送:DatagramSocket的用法与参数

发送端用GpsSender封装类:

public class GpsSender { private DatagramSocket socket; private InetAddress pcAddress; private int pcPort; public GpsSender(String pcIp, int port) throws IOException { this.pcAddress = InetAddress.getByName(pcIp); this.pcPort = port; this.socket = new DatagramSocket(); } public void send(String json) throws IOException { byte[] data = json.getBytes("UTF-8"); DatagramPacket packet = new DatagramPacket(data, data.length, pcAddress, pcPort); socket.send(packet); } public void close() { if (socket != null) socket.close(); } }

代码说明:DatagramSocket创建时不指定本机端口,系统自动分配,发送不需要固定源端口。pcAddress是PC端内网IP,getByName只做字符串到IP的解析;pcPort是PC端QT绑定的接收端口。send方法把JSON字符串按UTF-8编码,包进DatagramPacket后调用socket.send完成发送。

这里有一个关键参数:pcIp必须和PC处于同一网段并指向PC的实际IP。手机和PC连同一个Wi-Fi时,PC的IP从网络设置里查,常见的是192.168.1.x;如果手机开热点给PC用,PC的IP会落在手机分配的网段,可能是192.168.43.x。最省事的做法是手机端界面留一个IP输入框和端口输入框,启动时读取配置,免去每次改代码重编。实测中大部分通路问题都出在IP不在同一个网段,而不是代码本身。

发送线程也要注意:DatagramSocket要复用,不要每发一包就new一个,否则频繁创建销毁socket会引入不必要的性能损耗。建议单独起一个发送线程,从共享队列取JSON串再调用send,定位回调只负责往队列里塞数据。这样可以避免定位回调阻塞,也不会因为网络抖动拖慢GPS采集。

3.4 先用现成工具验证通路:把网络问题和技术问题分开

拿到这套代码后,我建议先做一次通路验证再联调。手机端装一个网络调试工具类APP,向PC的监听端口发一串"hello";PC端监听该端口,能收到则证明Wi-Fi和端口都通。通路没问题再换手机端代码和QT程序联调,出问题就一定是应用层代码的问题。

这里给一个PC端Python监听示例,适合临时验证:

import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("0.0.0.0", 9000)) while True: data, addr = sock.recvfrom(1024) print(data.decode("utf-8"), addr)

运行这个脚本,手机工具往PC的9000端口发包,脚本能打印出来就说明网络通。这一步的收益是隔离问题域。手机端和PC端代码各自能跑但组合起来没有数据时,排查网络配置最没有头绪。先工具后代码,九成的收不到数据案例可以定位为端口不一致或防火墙拦截,而不是协议解析问题。

4. QT端接收与显示:QUdpSocket绑定、解析、坐标转换与界面刷新

4.1 QT工程配置与QUdpSocket初始化

新建Qt Widgets应用后,在.pro文件里加network模块:

QT += core gui network

network这个模块必须加,不加编译时找不到QUdpSocket的头文件。加了模块后,重新执行qmake并构建。工程文件里顺手设置C++标准为C++11,后面的connect新语法和lambda依赖它。然后是初始化代码:

QUdpSocket *udpSocket = new QUdpSocket(this); bool ok = udpSocket->bind(QHostAddress::Any, 9000); if (!ok) { qDebug() << "bind failed:" << udpSocket->errorString(); } connect(udpSocket, &QUdpSocket::readyRead, this, &MainWindow::onReadyRead);

参数说明:bind第一参数QHostAddress::Any监听所有网卡,这样无论手机从Wi-Fi还是USB网络共享进来都能收到;第二个参数9000是接收端口,必须和手机端GpsSender里的pcPort一致。readyRead信号在UDP包到达时触发,同一时刻可能有多个包在缓冲区排队,接收函数必须用while循环把数据全部读完。

界面布局上,用四个QLabel分别显示纬度、经度、速度、卫星数,再加一个按钮控制启动和停止。布局简单,但要注意QLabel刷新频率不要超过UI线程的处理能力,GPS每秒才一两个包,直接在主线程更新没有压力,完全不需要引入定时器刷新这种复杂方案。

4.2 接收解析JSON并刷新界面

void MainWindow::onReadyRead() { while (udpSocket->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(udpSocket->pendingDatagramSize()); udpSocket->readDatagram(datagram.data(), datagram.size()); QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(datagram, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) { qDebug() << "json error:" << err.errorString(); continue; } QJsonObject obj = doc.object(); double lat = obj.value("lat").toDouble(); double lon = obj.value("lon").toDouble(); double speed = obj.value("speed").toDouble(); int sat = obj.value("satellites").toInt(); ui->labelLat->setText(QString::number(lat, 'f', 7)); ui->labelLon->setText(QString::number(lon, 'f', 7)); ui->labelSpeed->setText(QString::number(speed, 'f', 2) + " m/s"); ui->labelSat->setText(QString::number(sat)); updateTrail(lat, lon); } }

逻辑说明:hasPendingDatagrams判断缓冲区里还有没有数据包,有就读一个,读出来交给QJsonDocument解析。fromJson的返回值要先检查错误——手机端发来的数据编码不对或字段缺失时,解析失败就跳过这个包,不更新界面。每个包对应一次GPS定位,QJsonObject按字段名取值,取值失败返回类型默认值,比如lat取不到返回0.0,界面会闪一下0,这个现象能帮助及时发现字段名不一致的问题。

字段名一致性是折腾最多的地方。手机端定义的是lat,PC端写成latitude,解析不报错但数据全为0。建议调试时把收到的原始报文用qDebug()打印出来,手工比对字段名;更稳妥的做法是把字段名常量统一放在一个头文件里管理,手机端和PC端各自引用同一套定义,从源头消灭不一致。

4.3 WGS84转Web墨卡托:把定位点画到平面坐标系

界面要画轨迹而不是只显示数字时,需要把经纬度投影到平面坐标系。常用的是Web墨卡托投影:

QPointF wgs84ToWebMercator(double lat, double lon) { double x = lon * 20037508.34 / 180.0; double y = std::log(std::tan((90.0 + lat) * M_PI / 360.0)) / (M_PI / 180.0); y = y * 20037508.34 / 180.0; return QPointF(x, y); }

代码说明:墨卡托投影把球面经纬度展开成平面坐标,x方向是经度线性映射,y方向做对数变换,保证方向角不变。20037508.34是Web墨卡托在赤道处的半周长,把坐标归一化到标准范围。lat超出-85到85度投影会发散,GPS数据一般不会触及,但程序里最好加个范围判断防呆。

投影之后,选定一个显示区域做缩放平移,再连点画线,轨迹就出来了。如果用的是在线地图瓦片,还要注意坐标系差异:高德、腾讯地图使用的坐标与标准WGS84不同,直接把WGS84经纬度接到在线地图上会有几十到几百米的偏移;离线自行画轨迹不受影响。这一点在集成地图SDK时容易忽略,先明确自己用的是哪套坐标系,再决定是否做转换。

5. 避坑指南:GPS定位与UDP传输的五个典型现场

5.1 权限已授予但GPS一直无回调

现象:应用设置页显示定位权限已开启,在室外开阔地等五分钟,onLocationChanged一次都没执行,也没有任何报错。

原因:最常见的是代码里传入的是NETWORK_PROVIDER而不是GPS_PROVIDER,网络定位结果不稳定但兜底逻辑不报错;另一种情况是requestLocationUpdates根本没被调用,权限检查的分支提前return,把后续注册流程截断了。

解决:先打印当前注册的provider字符串,与GPS_PROVIDER的常量值比对;再在start方法入口和requestLocationUpdates之后各加一条日志,判断是没注册还是注册了没回调。实际排查时我靠这两条日志把问题分开,很快定位到是权限分支return导致整个注册流程被跳过。修复方式是调整代码结构,权限通过后显式走到requestLocationUpdates调用。

5.2 Android 12及以上GPS_PROVIDER静默失效

现象:同一套代码在Android 11设备上定位正常,Android 12设备上完全没有GPS回调,日志无异常。

原因:Android 12系统对targetSdk 31以上的应用,强制要求使用LocationRequest新机制来请求定位更新,旧的requestLocationUpdates(String, long, float, LocationListener)重载在部分设备上被静默丢弃,不报错也不再回调。

解决:targetSdk 31以上改用LocationRequest配合requestLocationUpdates(LocationRequest, Executor, LocationListener)的新接口调用,setIntervalMillis和setMinUpdateDistanceM对应旧接口的最小时间和最小距离两个参数。切换新接口后Android 12恢复正常。如果不改代码,把targetSdk保持30以下也能跑,但应用市场的政策一直在收紧,建议直接升级到新接口,别拖。

5.3 手机发的UDP包PC收不到

现象:手机端socket.send执行正常无异常,PC端QT的readyRead始终不触发,日志没有任何内容。

原因:端口和IP都核对过以后,最常见的拦路虎是Windows防火墙拦截了QT程序的入站UDP流量。UDP无连接,防火墙拦截入站UDP包通常不弹窗提醒,直接丢包,表现就是静默无声。

解决:在Windows防火墙高级设置里为QT程序添加一条入站规则,协议选UDP,本地端口填QT绑定的那个端口,操作选允许连接。添加后重试。另一个隐蔽原因是手机和PC不在同一网段:看起来IP相近,但中间隔着路由器,包自然到不了PC。先把PC和手机确认放在同一个局域网里,再谈防火墙问题。

5.4 QT端口绑定失败与readyRead不触发

现象:程序启动正常,界面也能显示,但数据一直不来,控制台输出里有bind失败的错误日志。

原因:端口被其他程序占用,bind返回AddressInUseError。常见占用者是另一个QT实例、路由器管理页面的某些后台进程,或是之前的网络调试工具没有退出。

解决:bind失败时打印errorString,把端口换到高位段如54321再试。注意bind默认模式下共享地址端口可能不报错,但UDP收包行为会受干扰,多个程序监听同一个端口时数据到底被谁收走不确定。稳妥做法是换一个冷门端口,并在手机端配置里同步修改端口号。

5.5 GPS冷启动输出0,0坐标

现象:定位成功之前,界面偶尔显示0.0,0.0,一闪而过或持续几秒,看起来像是数据异常。

原因:GPS冷启动需要30到60秒搜星并计算星历,这段时间内LocationManager可能回调一个默认的Location对象,经纬度都是0,表示定位尚未完成。

解决:在onLocationChanged最前面加有效性判断,lat或lon为0时直接丢弃;同时检查loc.hasAccuracy(),没有精度信息的定位也不可信。等GPS完成冷启动、有效数据稳定输出后再上传,界面就不会闪0坐标。这套过滤逻辑在写入CSV时同样重要,能避免把大量无效数据污染日志文件。

6. 进阶:数据落CSV文件与轨迹平滑

联调通过之后,下一步通常是数据记录。CSV是最通用的格式,Excel和Python都能直接打开,日常调试够用。实现方式是在QT的onReadyRead里追加一个文件写入函数:

void MainWindow::appendCsvLine(const QJsonObject &obj) { QFile file("gps_log.csv"); if (!file.open(QIODevice::Append | QIODevice::Text)) return; QTextStream out(&file); out << obj.value("time").toVariant().toString() << "," << obj.value("lat").toDouble() << "," << obj.value("lon").toDouble() << "," << obj.value("speed").toDouble() << "\n"; file.close(); }

代码说明:QIODevice::Append是追加模式,每次打开数据都写在文件末尾;QTextStream负责把数字和文本混排写入。注意time字段若手机端传来的是毫秒时间戳,这里直接输出的是数字字符串,CSV打开后能辨认,但想转成人类可读时间还要在Excel里做一次格式化。文件句柄每次都打开关闭虽然省事,采集频率高时建议改为定时批量flush,减少磁盘IO。

轨迹平滑我常用滑动平均。原始GPS点通常有1到2米的随机抖动,画出来的轨迹边缘毛糙;用长度5的窗口做均值,能把毛边去掉又不明显损失转弯细节:

QPointF smoothPoint(const QPointF &raw) { static QList<QPointF> buffer; buffer.append(raw); if (buffer.size() > 5) buffer.removeFirst(); double sumX = 0, sumY = 0; for (const auto &p : buffer) sumX += p.x(), sumY += p.y(); return QPointF(sumX / buffer.size(), sumY / buffer.size()); }

这个函数放在墨卡托投影之后、画线之前调用,轨迹的毛刺就被磨平了。窗口长度5对步行和车速都不错,想要更平滑就加到10,但转弯处的曲线会变钝,参数按需调。

经历了这轮定位项目的打磨,我养成了一个习惯:每次联调新设备,先花两分钟用网络调试工具把通路测一遍,再动应用层代码;从那以后,GPS项目的翻车率明显降下来,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询