开发一个 Android VNC 客户端
码不停提
从零搭建一个纯 Kotlin 实现的 Android VNC 客户端。采用 MVVM 架构 + Koin DI + Kotlin Coroutines,自实现 RFB 协议(不依赖 C/JNI 库),使用 SurfaceView 渲染远程桌面。
码不停提
从零搭建一个纯 Kotlin 实现的 Android VNC 客户端。采用 MVVM 架构 + Koin DI + Kotlin Coroutines,自实现 RFB 协议(不依赖 C/JNI 库),使用 SurfaceView 渲染远程桌面。
TL;DR:从零搭建一个纯 Kotlin 实现的 Android VNC 客户端。采用 MVVM 架构 + Koin DI + Kotlin Coroutines,自实现 RFB 协议(不依赖 C/JNI 库),使用 SurfaceView 渲染远程桌面。开发分 14 个步骤,预计 1 人约 10 个工作日完成核心功能。关键技术决策:纯 Kotlin 实现 RFB 协议(参考 bVNC/TightVNC Java Viewer)、编码按 Raw → CopyRect → ZRLE → Tight 渐进实现、32bpp ARGB 像素格式直接映射 Android Bitmap、DataStore 存储设置。
Steps
1. 初始化 Android Gradle 项目
:app)gradlew、gradlew.bat、gradle-wrapper.jar)namespace = "com.vnclient.app",minSdk = 24,compileSdk = 35,targetSdk = 35viewBinding = trueINTERNET 权限,注册三个 Activity2. 启动页 + 主页 UI
VncActivityInetAddress 解析)data class ConnectionInfo(val host: String, val port: Int, val password: String?)3. TCP Socket 连接 + RFB 握手
suspend fun connect(host, port) — 在 Dispatchers.IO 上建立 TCP 连接DataInputStream / DataOutputStream 的读写方法InetSocketAddress 自动解析)"RFB 003.008\n"),回复支持的版本Connecting | Authenticating | Connected | Disconnected(reason) | Error(exception)4. VNC 密码认证
javax.crypto.Cipher + DESKeySpecDesEncryptor → 发送 16 字节 response → 读取 SecurityResult5. 帧缓冲接收 + Raw 编码渲染
sendSetPixelFormat():请求 32bpp true-color ARGB(与 Bitmap.Config.ARGB_8888 直接兼容)sendSetEncodings():声明支持的编码列表sendFramebufferUpdateRequest(incremental, x, y, w, h):请求帧更新sendKeyEvent(downFlag, keySym)sendPointerEvent(buttonMask, x, y)fun decode(input, bitmap, x, y, w, h)w × h × 4 字节像素数据写入 BitmapBitmap(Bitmap.createBitmap(serverWidth, serverHeight, ARGB_8888))lockPixels() / unlockPixels() 线程安全访问suspend fun start(connectionInfo) → connect → handshake → auth → setPixelFormat → setEncodings → 进入消息循环StateFlow<ConnectionState> 对外暴露连接状态SharedFlow 或回调通知帧已更新6. 远程桌面界面 + SurfaceView 渲染
SurfaceView,实现 SurfaceHolder.CallbacklockCanvas() → drawBitmap() with Matrix(缩放远程桌面适配屏幕)→ unlockCanvasAndPost()Matrix 用于坐标变换(屏幕坐标 ↔ 远程桌面坐标)AndroidManifest.xml 中声明 configChanges="orientation|screenSize|keyboardHidden" 防止横竖屏重建 Activity 导致连接断开VncViewModel.connectionState 更新 UI 状态栏颜色(绿/红)viewModel.disconnect() → finish()RfbClient 实例viewModelScope 内启动连接协程connectionState: StateFlow 和 framebuffer: Bitmap 给 UI 层7. 触摸 → 鼠标事件 + 虚拟键盘
GestureDetectorCompat + ScaleGestureDetectorPointerEvent(buttonMask=0, x, y)(移动鼠标)PointerEvent(buttonMask=1) + PointerEvent(buttonMask=0)(左键按下/释放)PointerEvent(buttonMask=4)(右键)PointerEvent(buttonMask=8/16)(滚轮上/下)Matrix.invert() 从屏幕坐标映射回远程桌面坐标KeyCode / Unicode 字符 → X11 KeySym 映射表VncSurfaceView 重写 onCreateInputConnection() 返回自定义 BaseInputConnectioncommitText() 中逐字符转 KeySym 发送 KeyEvent8. CopyRect 编码
blit 矩形区域(极轻量,处理窗口拖动场景)9. 增量帧更新 + 帧率控制
RfbClient 消息循环:每次 FramebufferUpdate 处理完毕后才发送下一次 FramebufferUpdateRequest(incremental=true),形成自然背压FramebufferManager 只标记脏矩形,渲染线程只重绘变化区域10. ZRLE 编码
java.util.zip.Inflater,无需外部库)11. Tight 编码(JPEG)
BitmapFactory.decodeByteArray() 解码 → 写入帧缓冲12. 断线重连 + 连接状态管理
RfbClient 中捕获 IOException → 更新状态为 DisconnectedVncActivity 观察到断连后弹出重连对话框(带倒计时自动重连)13. DataStore 设置 + 连接历史
DataStore<Preferences>MainActivity 启动时自动填充上次连接信息14. 横竖屏 + 缩放手势
VncSurfaceView 中使用 ScaleGestureDetector 实现双指缩放Matrix(scale + translate),限制缩放范围 0.5x - 3.0x15. 一键打包脚本 + 文档
#!/bin/bash
cd "$(dirname "$0")"
./gradlew assembleDebug
echo "APK: app/build/outputs/apk/debug/app-debug.apk"
完整文件清单(需新建 ~45 个文件)
| 类别 | 数量 | 关键文件 |
|---|---|---|
| Gradle 构建 | 6 | build.gradle.kts(root+app)、settings.gradle.kts、gradle.properties、wrapper |
| Android 配置 | 1 | AndroidManifest.xml |
| UI 资源 | ~10 | layouts(3)、values(4)、drawables(2)、mipmap(5) |
| Kotlin 源码 | ~25 | Application(1)、DI(1)、Activity+ViewModel(5)、RFB 协议(9)、编码(5)、输入(3)、数据(2)、工具(3) |
| 测试 | ~6 | 握手/认证/消息解析/KeySym/IP验证/DES 单元测试 |
| 脚本+文档 | 3 | build_debug.sh、2 个 docs |
Verification
./gradlew assembleDebug 编译通过,App 安装后能看到启动页 → 主页./build_debug.sh 一键生成 APK,在手机上安装后能完成完整的远程桌面操作Decisions
Bitmap.Config.ARGB_8888 零转换开销configChanges 而非 Activity 重建 — 防止 Socket 连接断开.kts) — 类型安全,IDE 自动补全暂无目录