Android实现两台手机屏幕共享和远程控制(附带源码)_android 屏幕共享
一、项目概述
在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 Android 手机之间的屏幕共享与远程控制,其核心功能包括:
-
主控端(Controller):捕获自身屏幕并将实时画面编码后通过网络发送;同时监听用户在主控端的触摸、滑动和按键等输入操作,并将操作事件发送至受控端。
-
受控端(Receiver):接收屏幕画面数据并实时解码、渲染到本地界面;接收并解析主控端的输入操作事件,通过系统接口模拟触摸和按键,实现被控设备的操作。
通过这一方案,用户可以实时“看到”受控端的屏幕,并在主控端进行点触、滑动等交互,达到“远程操控”他机的效果。本项目的核心难点在于如何保证图像数据的实时性与清晰度,以及如何准确、及时地模拟输入事件。
二、相关知识
2.1 MediaProjection API
-
概述:Android 5.0(API 21)引入的屏幕录制和投影接口。通过
MediaProjectionManager
获取用户授权后,可创建VirtualDisplay
,将屏幕内容输送至Surface
或ImageReader
。 -
关键类:
-
MediaProjectionManager
:请求屏幕捕获权限 -
MediaProjection
:执行屏幕捕获 -
VirtualDisplay
:虚拟显示、输出到Surface
-
ImageReader
:以Image
帧的方式获取屏幕图像
-
2.2 Socket 网络通信
-
概述:基于 TCP 协议的双向流式通信,适合大块数据的稳定传输。
-
关键类:
-
ServerSocket
/Socket
:服务端监听与客户端连接 -
InputStream
/OutputStream
:数据读写
-
-
注意:需要设计简单高效的协议,在发送每帧图像前加上帧头(如长度信息),以便接收端正确分包、组帧。
2.3 输入事件模拟
-
概述:在非系统应用中无法直接使用
InputManager
注入事件,需要借助无障碍服务(AccessibilityService)或系统签名权限。 -
关键技术:
-
无障碍服务(AccessibilityService)注入触摸事件
-
使用
GestureDescription
构造手势并通过dispatchGesture
触发
-
2.4 数据压缩与传输优化
-
图像编码:将
Image
帧转为 JPEG 或 H.264,以减小带宽占用。 -
数据分片:对大帧进行分片发送,防止单次写入阻塞或触发
OutOfMemoryError
。 -
网络缓冲与重传:TCP 本身提供重传,但需控制合适的发送速率,防止拥塞。
2.5 多线程与异步处理
-
概述:屏幕捕获与网络传输耗时,需放在独立线程或
HandlerThread
中,否则 UI 会卡顿。 -
框架:
-
ThreadPoolExecutor
管理捕获、编码、发送任务 -
HandlerThread
配合Handler
处理 IO 回调
-
三、实现思路
3.1 架构设计
+--------------+ +--------------+| |--(请求授权)------------------->| || MainActivity | | RemoteActivity|| | MediaProjection -> ImageReader | 接收画面 -> 解码 -> SurfaceView | 编码(JPEG/H.264) | | 发送 -> Socket OutputStream | | | 接收事件 -> 无障碍 Service -> dispatchGesture | AccessibilityService |+------+-------+ +------+-------+| ScreenShare | | RemoteControl|| Service | | Service |+--------------+ +--------------+
3.2 协议与数据格式
-
帧头结构(12 字节)
-
4 字节:帧类型(0x01 表示图像,0x02 表示触摸事件)
-
4 字节:数据长度 N(网络字节序)
-
4 字节:时间戳(毫秒)
-
-
图像帧数据:
[帧头][JPEG 数据]
-
触摸事件数据:
-
1 字节:事件类型(0:DOWN,1:MOVE,2:UP)
-
4 字节:X 坐标(float)
-
4 字节:Y 坐标(float)
-
8 字节:时间戳
-
3.3 屏幕捕获与编码
-
主控端调用
MediaProjectionManager.createScreenCaptureIntent()
,请求授权。 -
授权通过后,获取
MediaProjection
,创建VirtualDisplay
并绑定ImageReader.getSurface()
。 -
在独立线程中,通过
ImageReader.acquireLatestImage()
不断获取原始Image
。 -
将
Image
转为Bitmap
,然后使用Bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream)
编码。 -
将 JPEG 字节根据协议拼接帧头,发送至受控端。
3.4 网络传输与解码
主控端
-
使用单例
SocketClient
管理连接。 -
将编码后的帧数据写入
BufferedOutputStream
,并在必要时调用flush()
。
受控端
-
启动
ScreenReceiverService
,监听端口,接受连接。 -
使用
BufferedInputStream
,先读取 12 字节帧头,再根据长度读完数据。 -
将 JPEG 数据用
BitmapFactory.decodeByteArray()
解码,更新到SurfaceView
。
3.5 输入事件捕获与模拟
主控端
-
在
MainActivity
上监听触摸事件onTouchEvent(MotionEvent)
,提取事件类型与坐标。 -
按协议封装成事件帧,发送至受控端。
受控端
-
RemoteControlService
接收事件帧后,通过无障碍接口构造GestureDescription
:
Path path = new Path();path.moveTo(x, y);GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription(path, 0, 1);
-
调用
dispatchGesture(stroke, callback, handler)
注入触摸。
四、完整代码
/************************** MainActivity.java **************************/package com.example.screencast;import android.app.Activity;import android.app.AlertDialog;import android.content.Context;import android.content.Intent;import android.graphics.PixelFormat;import android.media.Image;import android.media.ImageReader;import android.media.projection.MediaProjection;import android.media.projection.MediaProjectionManager;import android.os.Bundle;import android.util.DisplayMetrics;import android.view.MotionEvent;import android.view.SurfaceView;import android.view.View;import android.widget.Button;import java.io.BufferedOutputStream;import java.io.ByteArrayOutputStream;import java.io.OutputStream;import java.net.Socket;/* * MainActivity:负责 * 1. 请求屏幕捕获权限 * 2. 启动 ScreenShareService * 3. 捕获触摸事件并发送 */public class MainActivity extends Activity { private static final int REQUEST_CODE_CAPTURE = 100; private MediaProjectionManager mProjectionManager; private MediaProjection mMediaProjection; private ImageReader mImageReader; private VirtualDisplay mVirtualDisplay; private ScreenShareService mShareService; private Button mStartBtn, mStopBtn; private Socket mSocket; private BufferedOutputStream mOut; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mStartBtn = findViewById(R.id.btn_start); mStopBtn = findViewById(R.id.btn_stop); // 点击开始:请求授权并启动服务 mStartBtn.setOnClickListener(v -> startCapture()); // 点击停止:停止服务并断开连接 mStopBtn.setOnClickListener(v -> { mShareService.stop(); }); } /** 请求屏幕捕获授权 */ private void startCapture() { mProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); startActivityForResult(mProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_CAPTURE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_CAPTURE && resultCode == RESULT_OK) { mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data); // 初始化 ImageReader 和 VirtualDisplay setupVirtualDisplay(); // 启动服务 mShareService = new ScreenShareService(mMediaProjection, mImageReader); mShareService.start(); } } /** 初始化虚拟显示器用于屏幕捕获 */ private void setupVirtualDisplay() { DisplayMetrics metrics = getResources().getDisplayMetrics(); mImageReader = ImageReader.newInstance(metrics.widthPixels, metrics.heightPixels, PixelFormat.RGBA_8888, 2); mVirtualDisplay = mMediaProjection.createVirtualDisplay(\"ScreenCast\", metrics.widthPixels, metrics.heightPixels, metrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null); } /** 捕获触摸事件并发送至受控端 */ @Override public boolean onTouchEvent(MotionEvent event) { if (mShareService != null && mShareService.isRunning()) { mShareService.sendTouchEvent(event); } return super.onTouchEvent(event); }}/************************** ScreenShareService.java **************************/package com.example.screencast;import android.graphics.Bitmap;import android.graphics.ImageFormat;import android.media.Image;import android.media.ImageReader;import android.media.projection.MediaProjection;import android.os.Handler;import android.os.HandlerThread;import android.util.Log;import java.io.BufferedOutputStream;import java.io.ByteArrayOutputStream;import java.net.Socket;/* * ScreenShareService:负责 * 1. 建立 Socket 连接 * 2. 从 ImageReader 获取屏幕帧 * 3. 编码后发送 * 4. 接收触摸事件发送 */public class ScreenShareService { private MediaProjection mProjection; private ImageReader mImageReader; private Socket mSocket; private BufferedOutputStream mOut; private volatile boolean mRunning; private HandlerThread mEncodeThread; private Handler mEncodeHandler; public ScreenShareService(MediaProjection projection, ImageReader reader) { mProjection = projection; mImageReader = reader; // 创建后台线程处理编码与网络 mEncodeThread = new HandlerThread(\"EncodeThread\"); mEncodeThread.start(); mEncodeHandler = new Handler(mEncodeThread.getLooper()); } /** 启动服务:连接服务器并开始捕获发送 */ public void start() { mRunning = true; mEncodeHandler.post(this::connectAndShare); } /** 停止服务 */ public void stop() { mRunning = false; try { if (mSocket != null) mSocket.close(); mEncodeThread.quitSafely(); } catch (Exception ignored) {} } /** 建立 Socket 连接并循环捕获发送 */ private void connectAndShare() { try { mSocket = new Socket(\"192.168.1.100\", 8888); mOut = new BufferedOutputStream(mSocket.getOutputStream()); while (mRunning) { Image image = mImageReader.acquireLatestImage(); if (image != null) { sendImageFrame(image); image.close(); } } } catch (Exception e) { Log.e(\"ScreenShare\", \"连接或发送失败\", e); } } /** 发送图像帧 */ private void sendImageFrame(Image image) throws Exception { // 将 Image 转 Bitmap、压缩为 JPEG Image.Plane plane = image.getPlanes()[0]; ByteBuffer buffer = plane.getBuffer(); int width = image.getWidth(), height = image.getHeight(); Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bmp.copyPixelsFromBuffer(buffer); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.JPEG, 40, baos); byte[] jpegData = baos.toByteArray(); // 写帧头:类型=1, 长度, 时间戳 mOut.write(intToBytes(1)); mOut.write(intToBytes(jpegData.length)); mOut.write(longToBytes(System.currentTimeMillis())); // 写图像数据 mOut.write(jpegData); mOut.flush(); } /** 发送触摸事件 */ public void sendTouchEvent(MotionEvent ev) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) ev.getAction()); baos.write(floatToBytes(ev.getX())); baos.write(floatToBytes(ev.getY())); baos.write(longToBytes(ev.getEventTime())); byte[] data = baos.toByteArray(); mOut.write(intToBytes(2)); mOut.write(intToBytes(data.length)); mOut.write(longToBytes(System.currentTimeMillis())); mOut.write(data); mOut.flush(); } catch (Exception ignored) {} } // …(byte/int/long/float 与 bytes 相互转换方法,略)}/************************** RemoteControlService.java **************************/package com.example.screencast;import android.accessibilityservice.AccessibilityService;import android.graphics.Path;import android.view.accessibility.GestureDescription;import java.io.BufferedInputStream;import java.io.InputStream;import java.net.ServerSocket;import java.net.Socket;/* * RemoteControlService(继承 AccessibilityService) * 1. 启动 ServerSocket,接收主控端连接 * 2. 循环读取帧头与数据 * 3. 区分图像帧与事件帧并处理 */public class RemoteControlService extends AccessibilityService { private ServerSocket mServerSocket; private Socket mClient; private BufferedInputStream mIn; private volatile boolean mRunning; @Override public void onServiceConnected() { super.onServiceConnected(); new Thread(this::startServer).start(); } /** 启动服务端 socket */ private void startServer() { try { mServerSocket = new ServerSocket(8888); mClient = mServerSocket.accept(); mIn = new BufferedInputStream(mClient.getInputStream()); mRunning = true; while (mRunning) { handleFrame(); } } catch (Exception e) { e.printStackTrace(); } } /** 处理每个数据帧 */ private void handleFrame() throws Exception { byte[] header = new byte[12]; mIn.read(header); int type = bytesToInt(header, 0); int len = bytesToInt(header, 4); // long ts = bytesToLong(header, 8); byte[] payload = new byte[len]; int read = 0; while (read < len) { read += mIn.read(payload, read, len - read); } if (type == 1) { // 图像帧:解码并渲染到 SurfaceView handleImageFrame(payload); } else if (type == 2) { // 触摸事件:模拟 handleTouchEvent(payload); } } /** 解码 JPEG 并更新 UI(通过 Broadcast 或 Handler 通信) */ private void handleImageFrame(byte[] data) { // …(略,解码 Bitmap 并 post 到 SurfaceView) } /** 根据协议解析并 dispatchGesture */ private void handleTouchEvent(byte[] data) { int action = data[0]; float x = bytesToFloat(data, 1); float y = bytesToFloat(data, 5); // long t = bytesToLong(data, 9); Path path = new Path(); path.moveTo(x, y); GestureDescription.StrokeDescription sd = new GestureDescription.StrokeDescription(path, 0, 1); dispatchGesture(new GestureDescription.Builder().addStroke(sd).build(), null, null); } @Override public void onInterrupt() {}}
五、代码解读
-
MainActivity
-
请求并处理用户授权,创建并绑定
VirtualDisplay
; -
启动
ScreenShareService
负责捕获与发送; -
重写
onTouchEvent
,将触摸事件传给服务。
-
-
ScreenShareService
-
在后台线程中建立 TCP 连接;
-
循环从
ImageReader
获取帧,将其转为Bitmap
并压缩后通过 Socket 发送; -
监听主控端触摸事件,封装并发送事件帧。
-
-
RemoteControlService
-
作为无障碍服务启动,监听端口接收数据;
-
读取帧头与载荷,根据类型分发到图像处理或触摸处理;
-
触摸处理时使用
dispatchGesture
注入轨迹,实现远程控制。
-
-
布局与权限
-
在
AndroidManifest.xml
中声明必要权限与无障碍服务; -
activity_main.xml
简单布局包含按钮与SurfaceView
用于渲染。
-
六、项目总结
通过本项目,我们完整地实现了 Android 平台上两台设备的屏幕共享与远程控制功能,掌握并综合运用了以下关键技术:
-
MediaProjection API:原生屏幕捕获与虚拟显示创建;
-
Socket 编程:设计帧协议,实现高效、可靠的图像与事件双向传输;
-
图像编码/解码:将屏幕帧压缩为 JPEG,平衡清晰度与带宽;
-
无障碍服务:通过
dispatchGesture
注入触摸事件,完成远程控制; -
多线程处理:使用
HandlerThread
保证捕获、编码、传输等实时性,避免 UI 阻塞。
这套方案具备以下扩展方向:
-
音频同步:在屏幕共享同时传输麦克风或系统音频。
-
视频编解码优化:引入硬件 H.264 编码,以更低延迟和更高压缩率。
-
跨平台支持:在 iOS、Windows 等平台实现对应客户端。
-
安全性增强:加入 TLS/SSL 加密,防止中间人攻击;验证设备身份。