当前位置: 首页 > news >正文

Android 中 TCP 协议的实战运用

在 Android 开发中,网络通信是核心功能之一。TCP 作为可靠的传输层协议,被广泛应用于需要稳定数据传输的场景 —— 如即时通讯、文件上传、智能家居控制等。与 HTTP 的 “请求 - 响应” 模式不同,TCP 的 “长连接” 特性使其能实现实时双向通信,但也带来了连接管理、断线重连、数据粘包等特有挑战。

本文将从 Android 开发视角,系统讲解 TCP 协议的核心原理、在 Android 中的实现方式,以及实战中的关键问题(如主线程规避、断线重连、数据解析),并提供可直接复用的代码框架。

一、TCP 协议基础:为什么需要它?

在深入代码之前,先明确 TCP 协议的核心价值 —— 理解其设计原理,才能在开发中合理运用。

1.1 TCP 与 HTTP 的本质区别

很多开发者混淆 TCP 和 HTTP 的关系:HTTP 是 “应用层协议”,而 TCP 是 “传输层协议”——HTTP 建立在 TCP 之上,相当于 “TCP 的一种使用方式”。

两者的核心差异体现在通信模式:

特性

TCP 协议

HTTP 协议(基于 TCP)

适用场景

连接方式

长连接(建立后保持连接)

短连接(请求完成后关闭连接)

TCP:即时通讯、实时控制;HTTP:接口请求

通信方向

双向通信(客户端与服务器互发)

单向请求(客户端发,服务器回)

TCP:聊天消息;HTTP:获取商品列表

数据格式

无固定格式(需自定义)

有固定格式(请求头、响应体)

TCP:灵活传输二进制 / 文本;HTTP:结构化数据

可靠性保障

自带重传、排序机制

依赖 TCP 的可靠性

两者均适合需要可靠传输的场景

例如:微信聊天用 TCP(需实时双向收发消息),而获取朋友圈用 HTTP(主动请求后等待响应)。

1.2 TCP 的 “三次握手” 与 “四次挥手”

TCP 的 “可靠性” 源于其连接建立和关闭的严谨流程:

  • 三次握手(建立连接)
  1. 客户端发送 “连接请求”(SYN);
  2. 服务器回复 “同意连接”(SYN+ACK);
  3. 客户端确认 “收到回复”(ACK)。

作用:确保客户端和服务器的发送、接收能力均正常。

  • 四次挥手(关闭连接)
  1. 客户端发送 “关闭请求”(FIN);
  2. 服务器回复 “收到请求”(ACK);
  3. 服务器发送 “准备关闭”(FIN);
  4. 客户端回复 “确认关闭”(ACK)。

作用:确保双方都已完成数据传输,避免数据丢失。

在 Android 开发中,这些流程由系统底层实现,开发者无需手动处理,但需理解:TCP 连接建立有延迟(约 100-300ms),频繁建立 / 关闭连接会影响性能。

1.3 Android 中 TCP 的核心类

Android 通过 Java 的java.net包提供 TCP 支持,核心类包括:

类名

作用

关键方法

Socket

客户端套接字(与服务器建立连接)

getInputStream() getOutputStream()

ServerSocket

服务器端套接字(监听客户端连接)

accept()(阻塞等待连接)

InputStream

从 Socket 读取数据

read(byte[] buffer)

OutputStream

向 Socket 写入数据

write(byte[] buffer)

注意:Android 中ServerSocket多用于本地服务(如 APP 内的进程间通信),实际开发中客户端通常连接远程服务器(用Socket即可)。

二、Android 中 TCP 客户端实现:从连接到通信

实现 TCP 客户端的核心步骤是 “建立连接→读写数据→关闭连接”,但需注意 Android 的主线程限制(网络操作不能在主线程执行)。

2.1 基本 TCP 客户端框架

以下是一个可复用的 TCP 客户端基类,包含连接、发送、接收功能:

public class TcpClient {private static final String TAG = "TcpClient";private Socket mSocket; // TCP连接对象private InputStream mInputStream; // 输入流(读数据)private OutputStream mOutputStream; // 输出流(写数据)private boolean isConnected; // 连接状态标记private String mHost; // 服务器IPprivate int mPort; // 服务器端口private OnDataReceivedListener mDataListener; // 数据接收回调// 回调接口(数据接收、连接状态变化)public interface OnDataReceivedListener {void onDataReceived(byte[] data); // 收到数据void onConnectSuccess(); // 连接成功void onConnectFailed(Throwable e); // 连接失败void onDisconnect(); // 断开连接}public TcpClient(String host, int port, OnDataReceivedListener listener) {mHost = host;mPort = port;mDataListener = listener;}// 建立连接(必须在子线程调用)public void connect() {new Thread(() -> {try {// 关闭已有连接(避免重复连接)if (mSocket != null && mSocket.isConnected()) {mSocket.close();}// 创建Socket,连接服务器(超时时间5秒)mSocket = new Socket();mSocket.connect(new InetSocketAddress(mHost, mPort), 5000);// 获取输入输出流mInputStream = mSocket.getInputStream();mOutputStream = mSocket.getOutputStream();// 更新连接状态isConnected = true;// 通知UI连接成功(切换到主线程)MainLooper.runOnUiThread(mDataListener::onConnectSuccess);// 启动接收数据的线程startReceiveThread();} catch (Exception e) {// 连接失败isConnected = false;MainLooper.runOnUiThread(() -> mDataListener.onConnectFailed(e));e.printStackTrace();}}).start();}// 接收数据的线程(循环读取)private void startReceiveThread() {new Thread(() -> {byte[] buffer = new byte[1024]; // 缓冲区(根据需求调整大小)int length;try {// 循环读取,直到连接断开while (isConnected && (length = mInputStream.read(buffer)) != -1) {// 复制有效数据(避免缓冲区多余内容)byte[] data = new byte[length];System.arraycopy(buffer, 0, data, 0, length);// 回调通知收到数据MainLooper.runOnUiThread(() -> mDataListener.onDataReceived(data));}} catch (Exception e) {// 读取失败(通常是连接已断开)e.printStackTrace();}// 跳出循环表示连接已断开disconnect();}).start();}// 发送数据(必须在子线程调用)public void sendData(byte[] data) {if (!isConnected || mOutputStream == null) {MainLooper.runOnUiThread(() -> Toast.makeText(App.getContext(), "未连接服务器", Toast.LENGTH_SHORT).show());return;}new Thread(() -> {try {mOutputStream.write(data);mOutputStream.flush(); // 立即发送(避免缓冲)} catch (Exception e) {e.printStackTrace();// 发送失败,触发断开连接disconnect();}}).start();}// 断开连接public void disconnect() {if (!isConnected) return;try {isConnected = false;if (mInputStream != null) mInputStream.close();if (mOutputStream != null) mOutputStream.close();if (mSocket != null) mSocket.close();} catch (Exception e) {e.printStackTrace();} finally {// 通知UI断开连接MainLooper.runOnUiThread(mDataListener::onDisconnect);}}// 判断是否连接public boolean isConnected() {return isConnected;}
}

2.2 客户端使用示例

在 Activity 中初始化并使用 TcpClient:

public class TcpClientActivity extends AppCompatActivity implements TcpClient.OnDataReceivedListener {private TcpClient mTcpClient;private TextView mStatusTv;private EditText mInputEt;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_tcp_client);mStatusTv = findViewById(R.id.tv_status);mInputEt = findViewById(R.id.et_input);// 初始化TCP客户端(替换为实际服务器IP和端口)mTcpClient = new TcpClient("192.168.1.100", 8080, this);// 连接按钮findViewById(R.id.btn_connect).setOnClickListener(v -> {if (!mTcpClient.isConnected()) {mTcpClient.connect();mStatusTv.setText("连接中...");}});// 发送按钮findViewById(R.id.btn_send).setOnClickListener(v -> {String content = mInputEt.getText().toString();if (!TextUtils.isEmpty(content)) {// 发送字符串(转为字节数组)mTcpClient.sendData(content.getBytes(StandardCharsets.UTF_8));mInputEt.setText("");}});// 断开按钮findViewById(R.id.btn_disconnect).setOnClickListener(v -> {if (mTcpClient.isConnected()) {mTcpClient.disconnect();}});}// 收到数据回调@Overridepublic void onDataReceived(byte[] data) {String message = new String(data, StandardCharsets.UTF_8);mStatusTv.append("\n收到服务器:" + message);}// 连接成功回调@Overridepublic void onConnectSuccess() {mStatusTv.setText("已连接");Toast.makeText(this, "连接成功", Toast.LENGTH_SHORT).show();}// 连接失败回调@Overridepublic void onConnectFailed(Throwable e) {mStatusTv.setText("连接失败:" + e.getMessage());}// 断开连接回调@Overridepublic void onDisconnect() {mStatusTv.setText("已断开");Toast.makeText(this, "连接已断开", Toast.LENGTH_SHORT).show();}// 页面销毁时断开连接@Overrideprotected void onDestroy() {super.onDestroy();if (mTcpClient != null) {mTcpClient.disconnect();}}
}

2.3 核心代码解析

上述框架已处理 Android 开发中的关键问题:

  1. 主线程规避
  • 连接、发送、接收操作均在子线程执行;
  • 回调到 UI 层时用MainLooper.runOnUiThread切换主线程。
  1. 资源管理
  • 连接前关闭已有连接,避免资源泄漏;
  • 断开连接时关闭所有流和 Socket;
  • Activity 销毁时主动断开连接。
  1. 状态管理
  • isConnected标记连接状态,避免重复操作;
  • 所有操作前检查连接状态,防止空指针。

三、TCP 服务器搭建:本地测试必备

开发阶段需本地服务器调试,以下是用 Java 实现的简易 TCP 服务器(可运行在 PC 或 Android 设备):

public class TcpServer {private ServerSocket mServerSocket;private boolean isRunning;private List<Socket> mClientSockets = new ArrayList<>(); // 存储客户端连接public void start(int port) {isRunning = true;new Thread(() -> {try {// 启动服务器,监听指定端口mServerSocket = new ServerSocket(port);System.out.println("服务器已启动,端口:" + port);// 循环接收客户端连接while (isRunning) {Socket clientSocket = mServerSocket.accept(); // 阻塞等待连接synchronized (mClientSockets) {mClientSockets.add(clientSocket);}System.out.println("新客户端连接,当前数量:" + mClientSockets.size());// 启动线程处理该客户端handleClient(clientSocket);}} catch (Exception e) {e.printStackTrace();}}).start();}// 处理客户端通信private void handleClient(Socket clientSocket) {new Thread(() -> {try {InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream();byte[] buffer = new byte[1024];int length;// 接收客户端数据while ((length = inputStream.read(buffer)) != -1) {byte[] data = new byte[length];System.arraycopy(buffer, 0, data, 0, length);String message = new String(data, StandardCharsets.UTF_8);System.out.println("收到客户端:" + message);// 回复客户端String response = "服务器已收到:" + message;outputStream.write(response.getBytes(StandardCharsets.UTF_8));outputStream.flush();}} catch (Exception e) {e.printStackTrace();} finally {// 客户端断开连接try {clientSocket.close();} catch (Exception e) {e.printStackTrace();}synchronized (mClientSockets) {mClientSockets.remove(clientSocket);System.out.println("客户端断开,当前数量:" + mClientSockets.size());}}}).start();}// 停止服务器public void stop() {isRunning = false;try {if (mServerSocket != null) {mServerSocket.close();}synchronized (mClientSockets) {for (Socket socket : mClientSockets) {socket.close();}mClientSockets.clear();}} catch (Exception e) {e.printStackTrace();}System.out.println("服务器已停止");}public static void main(String[] args) {TcpServer server = new TcpServer();server.start(8080); // 启动服务器,端口8080}
}

使用方法:

1.在 PC 上运行main方法启动服务器;

2.Android 客户端连接 PC 的 IP(如192.168.1.101)和端口8080;

3.客户端发送消息,服务器会自动回复。

四、实战关键问题:从 “能跑” 到 “稳定”

基础框架能实现通信,但实际场景中需解决以下问题:

4.1 断线重连机制

网络波动会导致 TCP 连接断开,需自动重连:

// 在TcpClient中添加重连逻辑
private int mReconnectCount = 0; // 重连次数
private static final int MAX_RECONNECT = 5; // 最大重连次数// 修改disconnect方法,触发重连
private void disconnect() {if (!isConnected) return;// ... 原有关闭资源代码 ...// 判断是否需要重连(未主动断开且未超过最大次数)if (isNeedReconnect && mReconnectCount < MAX_RECONNECT) {mReconnectCount++;MainLooper.runOnUiThread(() -> mStatusTv.setText("断开连接,第" + mReconnectCount + "次重连..."));// 延迟1秒重连(避免频繁尝试)new Handler(Looper.getMainLooper()).postDelayed(this::connect, 1000);} else {mReconnectCount = 0; // 重置计数MainLooper.runOnUiThread(mDataListener::onDisconnect);}
}// 添加主动断开标记(避免主动断开后重连)
private boolean isNeedReconnect = true;public void disconnect(boolean initiative) {isNeedReconnect = !initiative; // 主动断开则不重连disconnect();
}

使用时:

  • 网络异常断开:自动重连(最多 5 次);
  • 用户主动点击断开:调用disconnect(true),不重连。

4.2 数据粘包与拆包问题

TCP 是 “流式传输”,多次发送的小数据可能被合并(粘包),大数据可能被拆分(拆包)。例如:

  • 客户端连续发送 “Hello” 和 “World”,服务器可能收到 “HelloWorld”(粘包);
  • 客户端发送 1000 字节数据,服务器可能分两次收到 500 字节(拆包)。

解决方案:自定义数据协议,添加 “数据长度” 头:

// 发送时添加长度前缀(4字节表示长度)
public void sendDataWithHeader(byte[] data) {if (data == null) return;// 总长度 = 4(长度) + 数据长度byte[] totalData = new byte[4 + data.length];// 转换长度为4字节(大端模式)byte[] lengthBytes = ByteBuffer.allocate(4).putInt(data.length).array();// 拼接长度和数据System.arraycopy(lengthBytes, 0, totalData, 0, 4);System.arraycopy(data, 0, totalData, 4, data.length);// 发送带头部的数据sendData(totalData);
}// 接收时按长度解析
private byte[] mReceiveBuffer = new byte[0]; // 缓存未解析数据private void handleReceivedData(byte[] newData) {// 合并缓存和新数据byte[] allData = new byte[mReceiveBuffer.length + newData.length];System.arraycopy(mReceiveBuffer, 0, allData, 0, mReceiveBuffer.length);System.arraycopy(newData, 0, allData, mReceiveBuffer.length, newData.length);int index = 0;// 循环解析完整数据包while (index + 4 <= allData.length) {// 读取长度(4字节)int dataLength = ByteBuffer.wrap(allData, index, 4).getInt();// 检查是否有完整数据包if (index + 4 + dataLength > allData.length) {break; // 数据不完整,退出循环}// 提取有效数据byte[] data = new byte[dataLength];System.arraycopy(allData, index + 4, data, 0, dataLength);// 回调通知MainLooper.runOnUiThread(() -> mDataListener.onDataReceived(data));// 移动索引index += 4 + dataLength;}// 保存未解析的数据到缓存if (index < allData.length) {mReceiveBuffer = new byte[allData.length - index];System.arraycopy(allData, index, mReceiveBuffer, 0, mReceiveBuffer.length);} else {mReceiveBuffer = new byte[0]; // 清空缓存}
}

使用时:

  • 发送:调用sendDataWithHeader("消息内容".getBytes());
  • 接收:在onDataReceived中调用handleReceivedData(data)解析。

4.3 心跳检测机制

长时间无数据传输时,TCP 连接可能被路由器 / 服务器关闭(默认超时约 30-120 秒),需定期发送心跳包维持连接:

// 在TcpClient中添加心跳机制
private Handler mHeartbeatHandler = new Handler(Looper.getMainLooper());
private static final long HEARTBEAT_INTERVAL = 30 * 1000; // 30秒一次// 连接成功后启动心跳
private void startHeartbeat() {mHeartbeatHandler.postDelayed(mHeartbeatRunnable, HEARTBEAT_INTERVAL);
}private Runnable mHeartbeatRunnable = new Runnable() {@Overridepublic void run() {if (isConnected) {// 发送心跳包(自定义格式,如0x01)sendData(new byte[]{0x01});// 继续定时mHeartbeatHandler.postDelayed(this, HEARTBEAT_INTERVAL);}}
};// 断开连接时停止心跳
private void stopHeartbeat() {mHeartbeatHandler.removeCallbacks(mHeartbeatRunnable);
}

服务器收到心跳包后需回复确认,客户端未收到回复则判断连接异常。

4.4 数据加密传输

TCP 传输内容明文可见,敏感数据(如密码、支付信息)需加密。推荐使用 AES 对称加密:

// AES加密工具类
public class AesUtils {private static final String KEY = "1234567890abcdef"; // 16位密钥(实际需安全存储)// 加密public static byte[] encrypt(byte[] data) throws Exception {SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.ENCRYPT_MODE, keySpec);return cipher.doFinal(data);}// 解密public static byte[] decrypt(byte[] encryptedData) throws Exception {SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.DECRYPT_MODE, keySpec);return cipher.doFinal(encryptedData);}
}// 使用加密发送
public void sendEncryptedData(String content) {try {byte[] data = content.getBytes(StandardCharsets.UTF_8);byte[] encrypted = AesUtils.encrypt(data);sendDataWithHeader(encrypted);} catch (Exception e) {e.printStackTrace();}
}// 接收解密
@Override
public void onDataReceived(byte[] data) {try {byte[] decrypted = AesUtils.decrypt(data);String message = new String(decrypted, StandardCharsets.UTF_8);// 处理解密后的消息} catch (Exception e) {e.printStackTrace();}
}

注意:密钥需通过安全方式传输(如首次连接用 RSA 加密密钥),避免硬编码泄露。

五、Android 特有限制与适配

Android 系统对网络和后台运行有特殊限制,需针对性处理。

5.1 网络权限与明文传输

  • 添加权限:在AndroidManifest.xml中声明网络权限:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 检测网络状态 -->

  • Android 9 + 明文传输限制

Android 9(API 28+)默认禁止 HTTP/TCP 明文传输,需在AndroidManifest.xml中添加配置:

<applicationandroid:usesCleartextTraffic="true"> <!-- 允许明文传输 -->
</application>

生产环境建议使用 SSL/TLS 加密(如SSLSocket替代Socket)。

5.2 后台保活与电量优化

TCP 长连接会消耗电量,需平衡实时性和功耗:

  • 屏幕关闭时降低心跳频率
    // 监听屏幕状态
    private BroadcastReceiver mScreenReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {// 屏幕关闭,心跳改为60秒一次stopHeartbeat();mHeartbeatHandler.postDelayed(mHeartbeatRunnable, 60 * 1000);} else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {// 屏幕开启,恢复30秒心跳stopHeartbeat();mHeartbeatHandler.postDelayed(mHeartbeatRunnable, 30 * 1000);}}
    };

  • 网络类型适配
    // 检测网络类型(WIFI/移动网络)
    private boolean isWifiConnected() {ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo info = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);return info != null && info.isConnected();
    }// WIFI下30秒心跳,移动网络下60秒
    long interval = isWifiConnected() ? 30000 : 60000;

5.3 进程保活(可选)

对于核心功能(如即时通讯),需避免进程被杀死导致连接断开:

  • 使用前台服务
    public class TcpService extends Service {private TcpClient mTcpClient;@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {// 启动前台服务(显示通知,降低被杀死概率)Notification notification = createNotification();startForeground(1, notification);// 初始化TCP连接mTcpClient = new TcpClient("host", 8080, listener);mTcpClient.connect();return START_STICKY; // 被杀死后尝试重启}// 创建前台通知private Notification createNotification() {NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "tcp_channel");// 设置通知内容(省略)return builder.build();}
    }

  • 在 Manifest 中声明服务
    <serviceandroid:name=".TcpService"android:foregroundServiceType="dataSync" /> <!-- Android 10+需指定类型 -->

六、TCP 在 Android 中的典型应用场景

掌握 TCP 的适用场景,才能在开发中做出合理选择:

6.1 即时通讯(如聊天 APP)

核心需求:实时双向收发消息,支持文字、图片、语音。

实现要点:

  • 用 TCP 长连接维持在线状态;
  • 自定义协议区分消息类型(文字 0x01、图片 0x02);
  • 消息加解密保护隐私;
  • 断线重连确保消息不丢失。

6.2 文件上传下载

核心需求:稳定传输大文件(如视频、安装包)。

实现要点:

  • 分块传输(每次发送 4KB,避免内存溢出);
  • 带校验(每块添加 MD5,确保完整性);
  • 断点续传(记录已传输位置,支持暂停后继续);
  • 进度反馈(通过 TCP 发送已传输百分比)。

6.3 智能家居控制

核心需求:手机实时控制设备(如灯光、空调)。

实现要点:

  • 短指令传输(如 “开灯” 对应 0x01 指令);
  • 快速响应(心跳间隔 5-10 秒);
  • 状态同步(设备状态变化主动通知手机);
  • 重连优先级高(确保控制指令能送达)。

七、总结:TCP 开发的核心原则

在 Android 中使用 TCP 协议,需牢记以下原则:

1.连接管理是核心

  • 建立连接:处理超时、网络异常;
  • 维持连接:心跳检测、断线重连;
  • 关闭连接:释放资源、避免泄漏。

2.数据传输需严谨

  • 解决粘包拆包:定义协议头;
  • 确保数据完整:校验和、重传机制;
  • 保护数据安全:加密传输。

3.适配 Android 特性

  • 避免主线程:所有网络操作放子线程;
  • 平衡功耗:根据网络 / 屏幕状态调整心跳;
  • 遵守系统限制:权限、后台运行规则。

TCP 协议的灵活性使其能适应多种场景,但也要求开发者处理更多底层细节。掌握本文的框架和问题解决方案,可大幅降低开发难度,实现稳定可靠的 TCP 通信功能。

最后提醒:TCP 并非万能 —— 简单的接口请求优先用 HTTP(Retrofit 可直接实现),只有需要实时双向通信时,才选择 TCP。

http://www.dtcms.com/a/302003.html

相关文章:

  • 【JAVA安全-Fastjson系列】Fastjson 1.2.24 反序列化漏洞分析及测试环境构建【复习回顾】
  • 安宝特案例丨户外通信机房施工革新:AR+作业流技术破解行业难题
  • 安宝特案例丨AR+AI赋能轨道交通制造:破解人工装配难题的创新实践
  • AR技术赋能工业设备维护:效率与智能的飞跃
  • keeplived实例
  • 基于Verilog的神经网络加速器设计
  • 微信小程序点击输入框时,顶部导航栏被遮挡问题如何解决?
  • 数值计算 | 图解基于龙格库塔法的微分方程计算与连续系统离散化(附Python实现)
  • 软件测试开发转型经验分享与职业发展指南
  • 基于FPGA和DDS原理的任意波形发生器(含仿真)
  • 可配置的PWM外设模块
  • Java Collections工具类
  • RocketMQ入门实战详解
  • 【MySQL学习|黑马笔记|Day1】数据库概述,SQL|通用语法、SQL分类、DDL
  • 【数据标注】详解使用 Labelimg 进行数据标注的 Conda 环境搭建与操作流程
  • 【unitrix】 6.20 非零整数特质(non_zero.rs)
  • 做了一款小而美的本地校验器
  • 【保姆级喂饭教程】Python依赖管理工具大全:Virtualenv、venv、Pipenv、Poetry、pdm、Rye、UV、Conda、Pixi等
  • 【el-table滚动事件】el-table表格滚动时,获取可视窗口内的行数据
  • 电磁兼容五:仿真技术
  • 数智驱动的「库存管理」:从风险系数、ABC分类到OMS和ERP系统的协同优化策略
  • 前端静态资源优化
  • WD5030A芯片24降12V,15A以内,应用于路由器、交换机和网络服务器,成本低大电流
  • 枚举策略模式实战:优雅消除支付场景的if-else
  • 6种将iPhone照片传输到Windows 10电脑的方法
  • Vue 正在热映模块
  • 安宝特案例丨AR+AI+SOP?3大技术融合革新军工航天领域
  • 组件化(一):重新思考“组件”:状态、视图和逻辑的“最佳”分离实践
  • 中兴云电脑W101D2-晶晨S905L3A-2G+8G-安卓9-线刷固件包
  • react前端样式如何给元素设置高度自适应