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

Android Studio 是如何和我们的手机共享剪贴板的

背景

近期完成了target33的项目适配升级,随着AGP和gradle的版本升级,万年老版本Android Studio(后文简称AS)也顺便升级到了最新版Android Studio Giraffe | 2022.3.1,除了新UI外,最让我好奇的是这次的Running Devices功能(官方也称为Device mirroring)可以控制真机了.

按照操作提示完成开启后就能在AS看到看到类似scrcpy和Vysor的手机控制界面.其中最让我惊喜的是剪贴板共享功能.这对于我这种需要在PC和手机频繁拷贝测试数据的人来说无疑降低了很多开发成本.

在这里插入图片描述

在这里插入图片描述

疑问

目前业内大部分剪贴板同步工具是基于局域网实现的,Android Studio(后续用AS替代)是如何做到PC和手机不在同一局域网的情况下实现剪贴板同步的呢?

实现

太长不看版

AS运行时会通过adb给设备推送一个agent的jar包和so文件.之后通过adb启动这个agent,并与这个agent建立了一个socket通信. AS和agent分别监听PC和设备的剪贴板变更,再通过socket进行数据传递同步

在这里插入图片描述

从网上没有搜索出太多资料,只能去看看从JetBrains开源的相关代码(https://github.com/JetBrains/android/tree/master)中一探究竟了

从代码的提交记录中可以发现监听版相关的逻辑主要集中在DeviceClipboardSynchronizer.kt中,简单分析一下它的核心成员变量和方法

成员变量功能
deviceClient用于与设备通信
copyPasteManager用于获取和设置主机上的剪贴板内容
deviceController用于向设备发送控制消息
focusOwnerListener用于侦听主机上焦点所有者的更改。
lastClipboardText与设备同步的最后一个剪贴板文本的字符串
方法功能
setDeviceClipboard设置设备剪贴板与主机剪贴板内容相同
getClipboardText从主机剪贴板获取文本
contentChanged当主机剪贴板内容更改时回调
onDeviceClipboardChanged设备剪贴板内容更改时回调

整体作用还是比较清晰的,那我们就以DeviceClipboardSynchronizer.kt为核心,仔细梳理一下AS是如何获取PC的剪贴板数据、将剪贴板数据发送给手机、手机如何更新剪贴板数据并监听设备剪贴板回传给AS的

问题1.AS如何获取PC的剪贴板数据

DeviceClipboardSynchronizer中获取PC剪贴板的场景有两种:

1、PC剪贴板内容变更的通知-用于在AS内部剪贴板变更的监听

@AnyThreadoverride fun contentChanged(oldTransferable: Transferable?, newTransferable: Transferable?) {UIUtil.invokeLaterIfNeeded { // This is safe because this code doesn't touch PSI or VFS.newTransferable?.getText()?.let { setDeviceClipboard(it, forceSend = false) }}}

2、AS初始化、获取焦点时-用于弥补在AS外的剪贴板操作.

private val focusOwnerListener = PropertyChangeListener { event ->// CopyPasteManager.ContentChangedListener doesn't receive notifications for all clipboard// changes that happen outside Studio. To compensate for that we also set the device clipboard// when Studio gains focus.if (event.newValue != null && event.oldValue == null) {// Studio gained focus.setDeviceClipboard(forceSend = false)}}

其中场景1通过CopyPasteManager.ContentChangedListener回调监听

public interface ContentChangedListener extends EventListener {void contentChanged(final @Nullable Transferable oldTransferable, final Transferable newTransferable);}

场景2通过copyPasteManager.getContents(DataFlavor.stringFlavor)获取

fun setDeviceClipboard(forceSend: Boolean) {val text = getClipboardText()setDeviceClipboard(text, forceSend = forceSend)}private fun getClipboardText(): String {return if (copyPasteManager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) {copyPasteManager.getContents(DataFlavor.stringFlavor) ?: ""}else {""}}

从这里可以看到AS侧获取PC剪贴板相关内容是通过com.intellij.openapi.ide.CopyPasteManager组件实现的,它是IntelliJ IDEA提供的一个用于负责复制和粘贴的接口组件用来抹平不同运行环境的差异,这里我们不细究CopyPasteManager的具体实现,如果各位感兴趣可以查看IDEA相关源码

总结:AS在获取焦点或者在AS内监听到剪贴板变化时会调用IDEA的CopyPasteManager获取PC的剪贴板内容.

问题2.AS如何将剪贴板数据发送给手机的

从之前的代码中可以看到AS获取到剪贴板数据后会调用setDeviceClipboard方法

private fun setDeviceClipboard(text: String, forceSend: Boolean) {//文本长度是否超过最大同步剪贴板长度默认为5000val maxSyncedClipboardLength = DeviceMirroringSettings.getInstance().maxSyncedClipboardLength//如果forceSend为true,或者text非空且与lastClipboardText不同.则走发送流程if (forceSend || (text.isNotEmpty() && text != lastClipboardText)) {val adjustedText = when {text.length <= maxSyncedClipboardLength -> textforceSend -> ""else -> return}//创建StartClipboardSyncMessage实例val message = StartClipboardSyncMessage(maxSyncedClipboardLength, adjustedText)//deviceController的sendControlMessage方法,将StartClipboardSyncMessage实例发送给设备控制器deviceController?.sendControlMessage(message)lastClipboardText = adjustedText}}

这个方法的整理流程还是比较清晰的:

  1. DeviceMirroringSettings实例中获取剪贴板同步的最大文本长度,默认为5000。
  2. 检查是否需要发送剪贴板内容。如果forceSendtrue,或者text非空且与lastClipboardText不同,那么就需要发送。
  3. 如果需要发送,根据text的长度和maxSyncedClipboardLength来调整要发送的文本内容。如果text的长度小于或等于maxSyncedClipboardLength,那么就发送text。如果forceSendtrue,那么发送空字符串。否则,函数直接返回,不做任何操作。
  4. 创建一个StartClipboardSyncMessage实例,这个实例包含了maxSyncedClipboardLength和调整后的文本内容。
  5. 调用deviceControllersendControlMessage方法,将StartClipboardSyncMessage实例发送给设备控制器。
  6. lastClipboardText设置为调整后的文本内容。

这里涉及到两个对象StartClipboardSyncMessagedeviceController ,其中StartClipboardSyncMessage 是一个传输数据的封装类,继承自ControlMessage,用于标识剪贴板消息类型及序列化和反序列化的实现.而deviceController 主要功能是通过发送控制消息来控制设备.

下面我们看来看看deviceController.sendControlMessage 是如何给设备发送消息的

//创建基于Base128编码的输出流
private val outputStream = Base128OutputStream(newOutputStream(controlChannel, CONTROL_MSG_BUFFER_SIZE))
...
fun sendControlMessage(message: ControlMessage) {if (!executor.isShutdown) {executor.submit {send(message)}}}private fun send(message:ControlMessage) {message.serialize(outputStream)outputStream.flush()}
...

我们可以看到在类的初始化阶段创建了一个基于Base128编码的输出流,剪贴板数据被序列化到输出流中,之后刷新了输出流完成数据发送.根据newOutputStream的相关注释说明,它会由传入的channel生成一个新的输出流.

而controlChannel是在DeviceController 初始化时传入的,层层回溯,最终在DeviceClient中创建的

DeviceClient主要功能是负责实现AS的设备的屏幕镜像功能,会通过和设备建立代理连接完成控制通道和视频通道的建立,而我们关注的controlChannel就是在该功能与设备建立代理连接时创建的

private suspend fun startAgentAndConnect(maxVideoSize: Dimension, initialDisplayOrientation: Int, startVideoStream: Boolean) {...//1.在协程中异步推送代理到设备。val agentPushed = coroutineScope {async {pushAgent(deviceSelector, adb)}}//2.创建一个异步服务器socket通道并绑定到一个随机端口。@Suppress("BlockingMethodInNonBlockingContext")val asyncChannel = AsynchronousServerSocketChannel.open().bind(InetSocketAddress(0))val port = (asyncChannel.localAddress as InetSocketAddress).portlogger.debug("Using port $port")SuspendingServerSocketChannel(asyncChannel).use { serverSocketChannel ->val socketName = "screen-sharing-agent-$port"//3.创建设备反向代理,它将设备上的一个设备上的抽象套接字转发到电脑上的一个TCP端口。ClosableReverseForwarding(deviceSelector, SocketSpec.LocalAbstract(socketName), SocketSpec.Tcp(port), adb).use {it.startForwarding()agentPushed.await()//4.启动代理对象startAgent(deviceSelector, adb, socketName, maxVideoSize, initialDisplayOrientation, startVideoStream)//5.建立代理连接connectChannels(serverSocketChannel)// Port forwarding can be removed since the already established connections will continue to work without it.}}try {//6.创建DeviceController来控制设备deviceController = DeviceController(this, controlChannel)}catch (e: IncorrectOperationException) {return // Already disposed.}...}

整体流程如下:

  1. 在协程中异步推送代理到设备.
  2. 创建一个异步服务器套接字通道并绑定到一个随机端口.
  3. 创建设备反向代理,它将设备上的一个socket转发到电脑上的一个TCP端口。
  4. 启动代理对象
  5. 建立代理连接
  6. 创建DeviceController控制设备

看到这里这里,疑问点就更多了,这里的代理是指什么,代理对象是如何启动的,连接又是怎么建立的,controlChannel是哪来的

问题2.1 代理是什么

这里的代理指的是两个文件:screen-sharing-agent.jar和libscreen-sharing-agent.so.这里我们可以简单了解一下他们的作用

  1. screen-sharing-agent.jar: 主要负责启动libscreen-sharing-agent.so ,处理NDK无法支持的MediaCodecList、MediaCodecInfo的编码视频流以及剪贴板监听同步等功能。
  2. libscreen-sharing-agent.so: 主要负责命令解析,设备视频解码、渲染等等功能.

篇幅有限,这里就不再展开了,有兴趣的可以查看相关源码

问题2.2 代理是如何启动的

第一步中会通过pushAgent将screen-sharing-agent.jar和libscreen-sharing-agent.so推送到设备的/data/local/tmp/.studio目录中,并设置好权限

之后调用startAgent()启动代理对象,startAgent()通过adb命令启动了代理中的com.android.tools.screensharing.Main方法,最终完成libscreen-sharing-agent.so的加载和相关参数的传递

private suspend fun startAgent(deviceSelector: DeviceSelector,adb: AdbDeviceServices,socketName: String,maxVideoSize: Dimension,initialDisplayOrientation: Int,startVideoStream: Boolean) {...//并设置代理程序的类路径,然后使用app_process命令启动代理程序的主类,并传入了根据入参构建一系列的命令行参数。val command = "CLASSPATH=$DEVICE_PATH_BASE/$SCREEN_SHARING_AGENT_JAR_NAME app_process $DEVICE_PATH_BASE" +" com.android.tools.screensharing.Main" +" --socket=$socketName" +maxSizeArg +orientationArg +flagsArg +maxBitRateArg +logLevelArg +" --codec=${StudioFlags.DEVICE_MIRRORING_VIDEO_CODEC.get()}"    //在一个新的协程作用域中执行这个命令,使用Dispatchers.Unconfined调度器确保能够正常终止CoroutineScope(Dispatchers.Unconfined).launch {val log = Logger.getInstance("ScreenSharingAgent $deviceName")val agentStartTime = System.currentTimeMillis()val errors = OutputAccumulator(MAX_TOTAL_AGENT_MESSAGE_LENGTH, MAX_ERROR_MESSAGE_AGE_MILLIS)try {adb.shellAsLines(deviceSelector, command).collect {//日志收集处理...}}...}
//com.android.tools.screensharing.Main
public class Main {@SuppressLint("UnsafeDynamicallyLoadedCode")public static void main(String[] args) {try {System.load("/data/local/tmp/.studio/libscreen-sharing-agent.so");}catch (Throwable e) {Log.e("ScreenSharing", "Unable to load libscreen-sharing-agent.so - " + e.getMessage());}nativeMain(args);}private static native void nativeMain(String[] args);
}

问题2.3 代理连接是怎么建立的

在问题2.2 代理是如何启动的中我们发现startAgent最终会调用到代理libscreen-sharing-agent.so的nativeMain()方法

Java_com_android_tools_screensharing_Main_nativeMain(JNIEnv* jni_env, jclass thisClass, jobjectArray argArray) {...//创建agent对象,并启动Agent agent(args);agent.Run();Log::I("Screen sharing agent stopped");// Exit explicitly to bypass the final JVM cleanup that for some unclear reason sometimes crashes with SIGSEGV.exit(EXIT_SUCCESS);
}
void Agent::Run() {...//创建DisplayStreamer对象处理视频流display_streamer_ = new DisplayStreamer(display_id_, codec_name_, max_video_resolution_, initial_video_orientation_, max_bit_rate_, CreateAndConnectSocket(socket_name_));//创建Controller对象处理控制命令,调用CreateAndConnectSocket创建Socket用于初始化controller_ = new Controller(CreateAndConnectSocket(socket_name_));Log::D("Created video and control sockets");if ((flags_ & START_VIDEO_STREAM) != 0) {StartVideoStream();}//运行Controllercontroller_->Run();Shutdown();
}

我们可以发现启动代理时,最终会在代理的cpp中创建了一个DisplayStreamer对象和一个Controller对象,并根据条件允许,因为本文目的是弄懂as是如何处理剪贴板数据的,我们重点关注Controller的相关逻辑.

首先Controller对象创建时,会先调用CreateAndConnectSocket创建Socket用于初始化,该方法会使用DeviceClient传入的socketname作为名称创建一个UNIX域Socket并进行连接.之后将该socket的描述符返回传入Controller构造函数

Controller::Controller(int socket_fd): socket_fd_(socket_fd),input_stream_(socket_fd, BUFFER_SIZE),output_stream_(socket_fd, BUFFER_SIZE),pointer_helper_(),motion_event_start_time_(0),key_character_map_(),clipboard_listener_(this),max_synced_clipboard_length_(0),clipboard_changed_() {assert(socket_fd > 0);char channel_marker = 'C';//写入一个字符`C`到之前创建的socket中,用于发送一个标记write(socket_fd_, &channel_marker, sizeof(channel_marker));  // Control channel marker.
}

我们发现在Controller的构建函数中,会通过Socket写入一个标记”C”,(DisplayStreamer中会写入标记“V”).在上文的DeviceClient的startAgentAndConnect方法中,我们知道在调用了startAgent()方法启动代理对象后,会调用connectChannels(serverSocketChannel)完成连接建立

private suspend fun connectChannels(serverSocketChannel: SuspendingServerSocketChannel) {//接受两个链接channel1和channel2val channel1 = serverSocketChannel.acceptAndEnsureClosing(this)val channel2 = serverSocketChannel.acceptAndEnsureClosing(this)// The channels are distinguished by single-byte markers, 'V' for video and 'C' for control.// Read the markers to assign the channels appropriately.coroutineScope {//接收标记val marker1 = async { readChannelMarker(channel1) }val marker2 = async { readChannelMarker(channel2) }val m1 = marker1.await()val m2 = marker2.await()//根据"C"和"V"分别确定视频流和控制流if (m1 == VIDEO_CHANNEL_MARKER && m2 == CONTROL_CHANNEL_MARKER) {videoChannel = channel1controlChannel = channel2}else if (m1 == CONTROL_CHANNEL_MARKER && m2 == VIDEO_CHANNEL_MARKER) {videoChannel = channel2controlChannel = channel1}else {throw RuntimeException("Unexpected channel markers: $m1, $m2")}}channelConnectedTime = System.currentTimeMillis()controlChannel.setOption(StandardSocketOptions.TCP_NODELAY, true)}private suspend fun readChannelMarker(channel: SuspendingSocketChannel): Byte {val buf = ByteBuffer.allocate(1)channel.read(buf, 5, TimeUnit.SECONDS)buf.flip()return buf.get()}

至此我们就通过代理完成了videoChannel和controlChannel的连接

总结:AS的DeviceClient会在与设备建立连接时会通过startAgentAndConnect方法:

  1. 将代理对象通过adb 命令发送到设备中
  2. 创建一个socket对象绑定随机端口,通过adb命令将设备socket与此端口建立反向代理
  3. 启动代理DeviceClient后通过此socket获取控制连接和视频连接.
  4. 将控制连接用于创建DeviceController

AS的DeviceClipboardSynchronizer通过DeviceClient.deviceController传递剪贴板数据完成数据通信

问题3.手机如何更新剪贴板数据并监听设备剪贴板回传给AS的

在了解了AS是如何给手机发送剪贴板数据后,那还剩下两个问题,AS发送的剪贴板数据是如何更新的以及如何获取设备剪贴板数据回传给AS的了.

问题3.1 AS发送的剪贴板数据是如何更新的

在问题2.3的最后,我们知道代理中的Controller会在启动时运行run方法

void Controller::Run() {Log::D("Controller::Run");Initialize();try {//无限循环中接收和处理控制消息for (;;) {if (max_synced_clipboard_length_ != 0) {//clipboard_changed_是否为trueif (clipboard_changed_.exchange(false)) {//处理剪贴板变化ProcessClipboardChange();}// Set a receive timeout to check for clipboard changes frequently.SetReceiveTimeoutMillis(SOCKET_RECEIVE_TIMEOUT_MILLIS, socket_fd_);}int32_t message_type;try {//从输入流中读取一个整数message_type = input_stream_.ReadInt32();} catch (IoTimeout& e) {continue;}SetReceiveTimeoutMillis(0, socket_fd_);  // Remove receive timeout for reading the rest of the message.//根据消息类型,从输入流中反序列化出一个控制消息。unique_ptr<ControlMessage> message = ControlMessage::Deserialize(message_type, input_stream_);//调用ProcessMessage()处理控制消息ProcessMessage(*message);}} catch (EndOfFile& e) {Log::D("Controller::Run: End of command stream");} catch (IoException& e) {Log::Fatal("%s", e.GetMessage().c_str());}
}void Controller::ProcessMessage(const ControlMessage& message) {switch (message.type()) {//处理各种类型消息...case StartClipboardSyncMessage::TYPE:StartClipboardSync((const StartClipboardSyncMessage&) message);break;...

代理中的Controller会启动一个无限循环不断处理各类消息,完成消息解析后会调用ProcessMessage进行处理,这里AS发送的type类型是StartClipboardSyncMessage,最终会调用到StartClipboardSync方法

void Controller::StartClipboardSync(const StartClipboardSyncMessage& message) {ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);//判断当前剪贴板数据和last_clipboard_text_是否一致if (message.text() != last_clipboard_text_) {last_clipboard_text_ = message.text();//调用clipboard_manager的SetText方法clipboard_manager->SetText(last_clipboard_text_);}bool was_stopped = max_synced_clipboard_length_ == 0;//更新文本最大长度max_synced_clipboard_length_ = message.max_synced_length();if (was_stopped) {clipboard_manager->AddClipboardListener(&clipboard_listener_);}
}void ClipboardManager::SetText(const string& text) const {JString jtext = JString(jni_, text.c_str());//调用到JAVA层ClipboardAdapter的setText方法clipboard_adapter_class_.CallStaticVoidMethod(jni_, set_text_method_, jtext.ref(), jtext.ref());
}

这里的流程比较简单,处理收到相关参数数据后最终会通过JNI回调到screen-sharing-agent.jar中ClipboardAdapter的setText方法

static {//获取剪贴板服务的接口clipboard = ServiceManager.getServiceAsInterface("clipboard", "android/content/IClipboard", true);try {if (clipboard != null) {//反射找到剪贴板服务的一些方法Class<?> clipboardClass = clipboard.getClass();Method[] methods = clipboardClass.getDeclaredMethods();getPrimaryClipMethod = findMethodAndMakeAccessible(methods, "getPrimaryClip");setPrimaryClipMethod = findMethodAndMakeAccessible(methods, "setPrimaryClip");addPrimaryClipChangedListenerMethod = findMethodAndMakeAccessible(methods, "addPrimaryClipChangedListener");removePrimaryClipChangedListenerMethod = findMethodAndMakeAccessible(methods, "removePrimaryClipChangedListener");numberOfExtraParameters = getPrimaryClipMethod.getParameterCount() - 1;if (numberOfExtraParameters <= 3) {clipboardListener = new ClipboardListener();//在Android 13及以上版本中创建一个PersistableBundle对象,用于禁止剪贴板更改的UI提示if (SDK_INT >= 33) {overlaySuppressor = new PersistableBundle(1);overlaySuppressor.putBoolean("com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY", true);}}else {Log.e("ScreenSharing", "Unexpected number of getPrimaryClip parameters: " + (numberOfExtraParameters + 1));}}}catch (NoSuchMethodException e) {Log.e("ScreenSharing", e.getMessage());clipboard = null;}}public static void setText(String text) throws InvocationTargetException, IllegalAccessException {if (clipboard == null) {return;}ClipData clipData = ClipData.newPlainText(text, text);if (SDK_INT >= 33) {// Suppress clipboard change UI overlay on Android 13+.clipData.getDescription().setExtras(overlaySuppressor);}if (numberOfExtraParameters == 0) {setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME);}else if (numberOfExtraParameters == 1) {setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, USER_ID);}else if (numberOfExtraParameters == 2) {setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID);}else if (numberOfExtraParameters == 3) {setPrimaryClipMethod.invoke(clipboard, clipData, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID, DEVICE_ID_DEFAULT);}}

可以看见在ClipboardAdapter的初始化时会通过反射的方式获取剪贴板相关的调用方法,最终在setText时会调用对于的剪贴板设置方法

总结:代理的Controller会在启动时会通过run方法启动一个无限循环不断处理各类消息,当收到AS侧发送的剪贴板同步的消息时最终会通过JNI调用到代理中ClipboardAdapter的setText方法最终通过反射调用剪贴板服务.

问题3.2 如何获取设备剪贴板数据回传给AS

在问题3.1中收到AS剪贴板消息时Controller::StartClipboardSync会调用 clipboard_manager->AddClipboardListener方法

void Controller::StartClipboardSync(const StartClipboardSyncMessage& message) {ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);...//通过max_synced_clipboard_length_大小判断之前是否停止了剪贴板,max_synced_clipboard_length_默认为0bool was_stopped = max_synced_clipboard_length_ == 0;//更新同步文本最大长度max_synced_clipboard_length_ = message.max_synced_length();if (was_stopped) {clipboard_manager->AddClipboardListener(&clipboard_listener_);}
}
void ClipboardManager::AddClipboardListener(ClipboardListener* listener) {for (;;) {auto old_listeners = clipboard_listeners_.load();//创建一个新的剪贴板监听器列表,这个新列表是当前列表的副本,并将新的监听器添加到新列表中auto new_listeners = new vector<ClipboardListener*>(*old_listeners);new_listeners->push_back(listener);//使用compare_exchange_strong方法尝试更新剪贴板监听器列表,没有被其他线程修改则为trueif (clipboard_listeners_.compare_exchange_strong(old_listeners, new_listeners)) {if (old_listeners->empty()) {//那么检查旧的监听器列表为空,那么调用ClipboardAdapter的enablePrimaryClipChangedListenerclipboard_adapter_class_.CallStaticVoidMethod(jni_, enable_primary_clip_changed_listener_method_);}delete old_listeners;return;}//compare_exchange_strong方法失败,那么删除新的监听器列表delete new_listeners;}
}

在clipboard_manager的AddClipboardListener方法中通过无锁编程的方式通过compare_exchange_strong线程安全的添加剪贴板监听器,并在监听器列表为空时通过JNI调用ClipboardAdapter的enablePrimaryClipChangedListener

public static void enablePrimaryClipChangedListener() throws InvocationTargetException, IllegalAccessException {if (clipboard == null) {return;}if (numberOfExtraParameters == 0) {addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME);}else if (numberOfExtraParameters == 1) {addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, USER_ID);}else if (numberOfExtraParameters == 2) {addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID);}else if (numberOfExtraParameters == 3) {addPrimaryClipChangedListenerMethod.invoke(clipboard, clipboardListener, PACKAGE_NAME, ATTRIBUTION_TAG, USER_ID, DEVICE_ID_DEFAULT);}}
public class ClipboardListener extends IOnPrimaryClipChangedListener.Stub {@Overridepublic native void dispatchPrimaryClipChanged();
}

最终通过在问题3.1中提到的反射方式,调用剪贴板服务中的addPrimaryClipChangedListener方法,这样当剪贴板数据变化时最终会调用到Java_com_android_tools_screensharing_ClipboardListener_dispatchPrimaryClipChanged

extern "C"
JNIEXPORT void JNICALL
Java_com_android_tools_screensharing_ClipboardListener_dispatchPrimaryClipChanged(JNIEnv* env, jobject thiz) {ClipboardManager* clipboard_manager = clipboard_manager_instance;if (clipboard_manager != nullptr) {clipboard_manager->OnPrimaryClipChanged();}
}...
void Controller::OnPrimaryClipChanged() {Log::D("Controller::OnPrimaryClipChanged");clipboard_changed_ = true;
}

经过层层传递最终会调用到Controller的OnPrimaryClipChanged方法中,这里的逻辑很简单指设置了clipboard_changed_为true.此时在之前的问题3.1 中提到的Controller::Run()方法,有一个无限循环一直在检测clipboard_changed_是否为true

void Controller::Run() {Log::D("Controller::Run");Initialize();try {//无限循环中接收和处理控制消息for (;;) {if (max_synced_clipboard_length_ != 0) {//clipboard_changed_是否为trueif (clipboard_changed_.exchange(false)) {//处理剪贴板变化ProcessClipboardChange();}// Set a receive timeout to check for clipboard changes frequently.SetReceiveTimeoutMillis(SOCKET_RECEIVE_TIMEOUT_MILLIS, socket_fd_);}....}
}void Controller::ProcessClipboardChange() {Log::D("Controller::ProcessClipboardChange");ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);Log::V("%s:%d", __FILE__, __LINE__);string text = clipboard_manager->GetText();Log::V("%s:%d", __FILE__, __LINE__);//检测剪贴板文本是否为空,或者与last_clipboard_text_相同if (text.empty() || text == last_clipboard_text_) {return;}Log::V("%s:%d", __FILE__, __LINE__);//检查剪贴板文本的长度是否超过了允许的最大长度max_lengthint max_length = max_synced_clipboard_length_;if (text.size() > max_length * UTF8_MAX_BYTES_PER_CHARACTER || Utf8CharacterCount(text) > max_length) {return;}last_clipboard_text_ = text;//创建一个ClipboardChangedNotification消息ClipboardChangedNotification message(std::move(text));Log::V("%s:%d", __FILE__, __LINE__);try {//尝试将消息序列化到output_stream_,然后刷新output_stream_message.Serialize(output_stream_);output_stream_.Flush();} catch (EndOfFile& e) {// The socket has been closed - ignore.}Log::V("%s:%d", __FILE__, __LINE__);
}

当检测到clipboard_changed_为true时会调用Controller::ProcessClipboardChange方法,经过检测后最终通过socket回传到AS侧

private fun startReceivingMessages() {receiverScope.launch {while (true) {try {if (inputStream.available() == 0) {suspendingInputStream.waitForData(1)}when (val message = ControlMessage.deserialize(inputStream)) {is ClipboardChangedNotification -> onDeviceClipboardChanged(message)else -> thisLogger().error("Unexpected type of a received message: ${message.type}")}}catch (_: EOFException) {break}catch (e: IOException) {if (e.message?.startsWith("Connection reset") == true) {break}throw e}}}}@AnyThreadoverride fun onDeviceClipboardChanged(text: String) {UIUtil.invokeLaterIfNeeded { // This is safe because this code doesn't touch PSI or VFS.if (text != lastClipboardText) {lastClipboardText = textcopyPasteManager.setContents(StringSelection(text))}}}

最终AS侧在收到socket回传消息后最终将其传递给copyPasteManager完整PC端的剪贴板同步

总结:在代理首次收到AS侧发送的剪贴板数据后会通过反射方法启动剪贴板变化的监听,当发现剪贴板变更时,会获取当前剪贴板数据通过socket回传给AS端,最终AS端通过copyPasteManager完成剪贴板数据的同步

总结

至此我们已经完整分析了Android Studio 是如何实现和我们的手机共享剪贴板的,其中涉及到ADB命令、代理、反射调用、socket连接等等技术,虽然整体原理比较简单,但是各种细节确实不少,其中有不少技术因为本人能力有限无法全面能力分析,如有遗漏错误欢迎斧正.

流程图

在这里插入图片描述

参考资料

https://github.com/JetBrains/android/tree/master

https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:adblib/src/com/android/adblib/?hl=zh-cn

相关文章:

Android Studio 是如何和我们的手机共享剪贴板的

背景 近期完成了target33的项目适配升级,随着AGP和gradle的版本升级,万年老版本Android Studio(后文简称AS)也顺便升级到了最新版Android Studio Giraffe | 2022.3.1,除了新UI外,最让我好奇的是这次的Running Devices功能(官方也称为Device mirroring)可以控制真机了. 按照操…...

大数据面试题:Spark和MapReduce之间的区别?各自优缺点?

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 可回答&#xff1a; 1&#xff09;spark和maprecude的对比&#xff1b;2&#xff09;mapreduce与spark优劣好处 问过的一些公司&#xff1a;阿里云…...

【开发篇】十八、SpringBoot整合ActiveMQ

文章目录 1、安装ActiveMQ2、整合3、发送消息到队列4、使用消息监听器对消息队列监听5、流程性业务消息消费完转入下一个消息队列6、发布订阅模型 1、安装ActiveMQ docker安装 docker pull webcenter/activemqdocker run -d --name activemq -p 61616:61616 -p 8161:8161 webce…...

QTcpSocket 接收数据实时性问题

一、开发背景 使用 Qt 的 QTcpSocket 接收数据的时候发现数据接收出现粘包的现象&#xff0c;并且实时性很差&#xff0c;通过日志的时间戳发现数据接收的误差在 100ms 以内。 二、开发环境 Qt5.12.2 QtCreator4.8.2 三、实现步骤 在 socket 连接的槽函数设置接收延时时间&…...

前端el-select 单选和多选

el-select单选 <el-form-item label"部门名称" prop"departId"><el-select v-model"dataForm.departId" placeholder"请选择" clearable:style{ "width": "100%" } :multiple"false" filtera…...

【MySQL】Linux 中 MySQL 环境的安装与卸载

文章目录 Linux 中 MySQL 环境的卸载Linux 中 MySQL 环境的安装 Linux 中 MySQL 环境的卸载 在安装 MySQL 前&#xff0c;我们需要先将系统中以前的环境给卸载掉。 1、查看以前系统中安装的 MySQL rpm -qa | grep mysql2、卸载这些 MySQL rpm -qa | grep mysql | args yum …...

机器学习算法分类

学习视频黑马程序员 监督学习 无监督学习 半监督学习 强化学习...

Mysql bin-log日志恢复数据与物理备份-xtrabackup

主打一个数据备份与恢复 binlog与xtarbackup bin-log日志恢复开启bin-log配置bin-log日志恢复 物理备份-xtrabackup三种备份方式安装xtrabackup备份全量备份增量备份差异备份 bin-log日志恢复 bin-log 日志&#xff0c;就记录对数据库进行的操作&#xff0c;什么增删改的操作全…...

JAVA 学习笔记 2年经验

文章目录 基础String、StringBuffer、StringBuilder的区别jvm堆和栈的区别垃圾回收标记阶段清除阶段 异常类型双亲委派机制hashmap和hashtable concurrentHashMap 1.7和1.8的区别java的数据结构排序算法&#xff0c;查找算法堆排序 ThreadLocal单例模式常量池synchronizedsynch…...

网络安全--安全认证、IPSEC技术

目录 1. 什么是数据认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段&#xff1f; 2. 什么是身份认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段&#xff1f; 3. 什么是VPN技术&#xff1f; 4. VPN技术有哪些分类&#xff1f; 5. IPSEC技术能够…...

Mysql——创建数据库,对表的创建及字段定义、数据录入、字段增加及删除、重命名表。

一.创建数据库 create database db_classics default charsetutf8mb4;//创建数据库 use db_classics;//使用该数据库二.对表的创建及字段定义 create table if not exists t_hero ( id int primary key auto_increment, Name varchar(100) not null unique, Nickname varchar(1…...

第1篇 目标检测概述 —(4)目标检测评价指标

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。目标检测评价指标是用来衡量目标检测算法性能的指标&#xff0c;可以分为两类&#xff0c;包括框级别评价指标和像素级别评价指标。本节课就给大家重点介绍下目标检测中的相关评价指标及其含义&#xff0c;希望大家学习之后…...

前端和后端是Web开发中的两个不同的领域,你更倾向于哪一种?

前端和后端是Web开发中的两个不同的领域&#xff0c;你更倾向于哪一种&#xff1f; 你可以从以下几个维度谈谈你对前端开发和后端开发的看法。此为内容创作模板&#xff0c;在发布之前请将不必要的内容删除 一、引言 提示&#xff1a;可对前端开发和后端开发进行简要介绍并提出…...

SpringBoot集成MyBatis-Plus实现增删改查

背景 因为学习工具的时候经常需要用到jar包&#xff0c;需要增删查改接口&#xff0c;所以参考文章实现了基于mybatis-plus的增删查改接口。 参考文章&#xff1a;第二十二节:SpringBoot集成MyBatis-Plus实现增删改查 原文中的git地址不存在&#xff0c;本文内容是原文代码修…...

基于STM32设计的智能水产养殖系统(华为云IOT)

一、设计简述 基于STM32设计的智能水产养殖监测系统 1.1 项目背景 随着经济的发展和人口的增长,对水产养殖的需求不断增加。然而,传统的水产养殖方式存在一系列问题,如水质污染、鱼病爆发等。因此,智能化水产养殖技术成为当前热门研究领域。其中,基于物联网技术的智能水产…...

运行软件找不到mfc140u.dll怎么解决,mfc140u.dll是什么文件

"找不到 mfc140u.dll"是一条错误信息&#xff0c;表示您的计算机上缺少一个名为 mfc140u.dll 的动态链接库&#xff08;DLL&#xff09;文件。这个文件通常与 Microsoft Visual C Redistributable 相关。Mfc140u.dll 是 Microsoft 基础类库&#xff08;MFC&#xff0…...

数据结构(2-5~2-8)

2-5编写算法&#xff0c;在单链表中查找第一值为x的结点&#xff0c;并输出其前驱和后继的存储位置 #include<stdio.h> #include<stdlib.h>typedef int DataType; struct Node {DataType data; struct Node* next; }; typedef struct Node *PNode; …...

浅谈智能安全配电装置在老年人建筑中的应用

摘要&#xff1a;我国每年因触电伤亡人数非常多&#xff0c;大多数事故是发生在用电设备和配电装置。在电气事故中&#xff0c;无法预料和不可抗拒的事故是比较少的&#xff0c;大量用电事故可采取切实可行措施来预防。本文通过结合老年人建筑的特点和智能安全配电装置的功能&a…...

【ES】笔记-ES6模块化

暴露数据引入模块语法 规范基本语法分别暴露 (按需暴露)统一暴露 export {暴露内容1&#xff0c;暴露内容2}默认暴露 (适合只暴露一个数据) 只能暴露一次同时使用在app.js中引入 规范 每个文件都是一个模块要借助Babel和Browserify依次编译代码&#xff0c;才能在浏览器端运行…...

阿里云/腾讯云国际站代理:腾讯云国际站开户购买EdgeOne发布,安全加速一体化方案获业内认可

作为下一代CDN产品面世的腾讯云EdgeOne&#xff0c;历时一年服务&#xff0c;腾讯云国际站凭借安全加速一体化的解决方案&#xff0c;用All in One 架构构筑边缘应用无限想象。 近年来&#xff0c;随着5G网络、物联网、边缘计算的快速发展&#xff0c;爆炸式增长的数据量和市场…...

AIGC AI绘画 Midjourney 的详细使用手册

Midjourney参数提示与用法。 常见的命令有: --seed:种子值 --q:品质 --c:混乱 --no:负面提示 --iw:权重(0.5-2) ::(多重提示) -- repeat(重复) --stop(停止) --title(无缝贴图:适用于模型版本 1、2、3、5) --video(过程动画,适用于模型版本 1、2…...

Lua系列文章(1)---Lua5.4参考手册学习总结

windows系统上安装lua,下载地址&#xff1a; Github 下载地址&#xff1a;https://github.com/rjpcomputing/luaforwindows/releases 可以有一个叫SciTE的IDE环境执行lua程序 1 – 简介 Lua 是一种强大、高效、轻量级、可嵌入的脚本语言。 它支持过程编程&#xff0c; 面向对…...

Leetcode.121 买卖股票的最佳时机

题目链接 Leetcode.121 买卖股票的最佳时机 esay 题目描述 给定一个数组 p r i c e s prices prices &#xff0c;它的第 i i i 个元素 p r i c e s [ i ] prices[i] prices[i] 表示一支给定股票第 i i i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在…...

IDE相关设置和插件

https://www.jetbrains.com/zh-cn/webstorm/ 一、插件 Chinese ​(Simplified)​ Language Pack&#xff1a;中文语言包Translation&#xff1a;翻译插件&#xff0c;需要申请国内翻译软件应用&#xff0c;可以搜索translations配置快捷键any-rule&#xff1a;正侧插件&#…...

nodejs之jsdom插件,运行浏览器环境

https://www.npmjs.com/package/jsdom 一、安装依赖 npm install jsdom二、用法 var jsdom require(jsdom) var { JSDOM } jsdom; var dom new JSDOM(<!DOCTYPE html><html lang"cn"><head></head><body></body></htm…...

运行vite项目报错:await import(‘source-map-support‘).then((r) => r.default.install())

项目场景&#xff1a; Electron vue3 vite项目实现屏幕截图功能 问题描述 运行 npm run dev 启动项目报错 await import(source-map-support).then((r) > r.default.install()) PS D:\study\electron\electronDemo> npm run dev> electronDemo0.0.1 dev D:\study…...

【GIT版本控制】--安装GIT

一、在不同操作系统上安装GIT 在不同操作系统上安装GIT非常容易&#xff0c;以下是针对不同操作系统的安装步骤&#xff1a; 在Windows上安装GIT&#xff1a; 访问 Git官方网站。下载适合您Windows版本的GIT安装程序&#xff08;32位或64位&#xff09;。运行下载的安装程序。…...

java 常见api Arrays类

int类型数组 package daysreplace;import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arrays{38,24,42,56,22,44};//直接输出数组名称就是内存地址System.out.println(arrays);//Arrays.toString()会将数组内容转成字符串形式System…...

Java常见设计模式

单例模式&#xff1a;程序自始至终只创建一个对象。 应用场景&#xff1a;1.整个程序运行中只允许一个类的实例时 2.需要频繁实例化然后销毁的对象 3.创建对象时耗时过多但又经常用到的对象 4.方便资源相互通信的环境 懒汉式线程不安全问题解决方案&#xff1a; 双重检查加锁机…...

Hive 【Hive(七)窗口函数练习】

窗口函数案例 数据准备 1&#xff09;建表语句 create table order_info (order_id string, --订单iduser_id string, -- 用户iduser_name string, -- 用户姓名order_date string, -- 下单日期order_amount int -- 订单金额 ); 2&#xff09;装载语句 i…...

网站建设经费预算/推广链接点击器app

一个层接收触摸消息需要如下步骤&#xff1a; 1&#xff0c;在初始化阶段(init方法)将此层的属性设置为接收触摸消息 setTouchEnabled(true);//开启屏幕触摸 2&#xff0c;重载函数virtual void registerWithTouchDispatcher(void);因为默认的方式为Standard Touch Delegate&a…...

看设计作品的网站软件/滨州seo招聘

俗话说工欲善其事&#xff0c;必先利其器做Python开发同样也是这样一个道理&#xff0c;那么Python用什么开发工具呢?其实Python开发工具能用的有很多的&#xff0c;今天小编就为大家推荐几款&#xff0c;这些里面总有一款适合你。NO1. PycharmPycharm可以应用于写相应脚本和程…...

桃城网站建设价格/百度推广一个点击多少钱

通过前面的学习我们已经能够通过遍历所有已知的函数及其指令来达到一种基本的搜索效果,这当然很有用,不过有时候我们需要搜索一些特定的字节,比如0x48 0xff 0xc2,这3个字节代表的汇编代码为inc rdx 我们可以选中该指令后右键-》synchronized with->hex view 1,可以看到…...

wordpress授权系统/百度链接提交

题目描述&#xff1a; 已知一个列表myList1&#xff0c;请编写一个函数&#xff0c;实现下列功能&#xff1a;该函数的输入是一个仅包含整数的myList1,输出一个新的myList2&#xff0c;其中myList2中的每一个元素满足以下条件&#xff1a; 1.该元素是偶数 2.该元素在原myLis…...

网站挂黑链赚钱/企业管理培训机构

Namedtuples在Python中用于命名小型数据集合.以这个命名元组为例&#xff1a;import collectionssesameEpisodeNTC collections.namedtuple(sesameEpisodeNTC,lead_character, has_elmo)se0 sesameEpisodeNTC(lead_characterbigbird, has_elmoFalse)可以将类定义(‘sesameEpi…...

wordpress简体切换/网站推广软文

展开全部大四学生32313133353236313431303231363533e58685e5aeb931333433653933&#xff0c;应该先考研还是工作后再考研&#xff1f;这两种选择&#xff0c;哪一个更实惠&#xff1f;作出选择之前&#xff0c;这3个问题一定要考虑清楚。考虑复习效果一般情况下&#xff0c;多数…...