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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/99255.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

异步爬虫实战:实际应用asyncio和aiohttp库构建异步爬虫

在网络爬虫的开发中&#xff0c;异步爬虫已经成为一种非常流行的技术。它能够充分利用计算机的资源&#xff0c;提高爬虫效率&#xff0c;并且能够处理大量的运算请求。Python中的asyncio和aiohttp库提供了强大的异步爬虫支持&#xff0c;使得开发者能够轻松构建高效的异步爬虫…

在pycharm中出现下载软件包失败的解决方法

一. 一般情况下我们会选择在设置中下载软件包,过程如下. 1. 直接点击左上角的文件, 再点击设置, 再点击项目, 在右边选择python解释器,点击号,输入要下载的软件包, 在下面的一系列的包中选择相对应的包,点击安装就可以了,有的时候我们下载的是最新的版本,如果要下载固定的版本…

算法框架-LLM-1-Prompt设计(一)

原文&#xff1a;算法框架-LLM-1-Prompt设计&#xff08;一&#xff09; - 知乎 目录 收起 1 prompt-engineering-for-developers 1.1 Prompt Engineering 1.1.1 提示原则 1. openai的环境 2. 两个基本原则 3. 示例 eg.1 eg.2 结构化输出 eg.3 模型检验 eg.4 提供示…

【计算机基础】Git系列3:常用操作

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

如何使用CDN加载静态资源

文章目录 前言工具场景七牛云创建空间存储 控制台查看后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误…

QFluentWidgets: 基于 C++ Qt 的 Fluent Design 组件库

简介 QFluentWidgets 是一个基于 Qt 的 Fluent Designer 组件库&#xff0c;内置超过 150 个开箱即用的 Fluent Designer 组件&#xff0c;支持亮暗主题无缝切换和自定义主题色。搭配所见即所得的 Fluent Designer 软件&#xff0c;只需拖拖拽拽&#xff0c;不用编写一行 QSS&…

弹性资源组件elastic-resource设计(四)-任务管理器和资源消费者规范

简介 弹性资源组件提供动态资源能力&#xff0c;是分布式系统关键基础设施&#xff0c;分布式datax&#xff0c;分布式索引&#xff0c;事件引擎都需要集群和资源的弹性资源能力&#xff0c;提高伸缩性和作业处理能力。 本文介绍弹性资源组件的设计&#xff0c;包括架构设计和详…

数据结构-图-最短路径问题

最短路径问题 单源最短路径Dijkstra算法原理代码实现 Bellman-Ford算法原理代码实现SPFA优化SPFA代码实现 多元最短路径Floyd-Warshall算法原理代码实现 单源最短路径 &#x1f680;最短路径&#xff1a;从图G的某个顶点出发到达另一个顶点的最短路径&#xff0c;其中最短是指…

线性代数小例子

这样做有什么问题呢&#xff1a; A 2 A > A ( A − E ) 0 > A E A 0 A^2 A > A(A - E) 0> A E \quad A 0 A2A>A(A−E)0>AEA0 上述做法是错误的&#xff0c;这是因为两个矩阵的乘积结果为0&#xff0c;并不能说明这两个矩阵就是0&#xff0c;即上述…

提高工作效率!本地部署Stackedit Markdown编辑器,并实现远程访问

文章目录 1. docker部署Stackedit2. 本地访问3. Linux 安装cpolar4. 配置Stackedit公网访问地址5. 公网远程访问Stackedit6. 固定Stackedit公网地址 StackEdit是一个受欢迎的Markdown编辑器&#xff0c;在GitHub上拥有20.7k Star&#xff01;&#xff0c;它支持将Markdown笔记保…

假期AI新闻热点:亚运会Al技术亮点;微软GPT-4V论文精读;Perplexity推出pplx-api;DALL-E 3多渠道测评 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 科技感拉满&#xff0c;第19届杭州亚运会中的Al技术亮点 八年筹备&#xff0c;杭州第19届亚运会开幕式于9月23日晚隆重举行&#xff0…

论文解析——异构多芯粒神经网络加速器

作者 朱郭益, 马胜&#xff0c;张春元, 王波&#xff08;国防科技大学计算机学院&#xff09; 摘要 随着神经网络技术的快速发展, 出于安全性等方面考虑, 大量边缘计算设备被应用于智能计算领域。首先&#xff0c;设计了可应用于边缘计算的异构多芯粒神经网络加速器其基本结构…

SSRF+redis未授权漏洞复现

1.SSRF漏洞简介 SSRF&#xff08;Server-Side Request Forgery&#xff09;即服务器端请求伪造&#xff0c;是一种由攻击者构造攻击链传给服务器&#xff0c;服务器执行并发起请求造成安全问题的漏洞&#xff0c;一般用来在外网探测或攻击内网服务。当网站需要调用指定URL地址…

vscode 无法打开源文件

以下是c/c插件的intelligense设置情况&#xff1a; 解决办法&#xff1a; 重新安装vsode无用&#xff1b;重新下载mingw64&#xff0c;管用了&#xff01;&#xff08;我猜可能是之前换电脑移植文件的时候导致了部分文件丢失&#xff09;

基于微信小程序的个人健康数据管理平台设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

你该了解的自动化测试工具:Selenium控制浏览器的常用方法!

Selenium怎么来的&#xff1f; Selenium这个词&#xff0c;是化学元素硒&#xff08;Se&#xff09;的意思。在软件测试领域&#xff0c;它是绝对的自动化测试开源项目的标杆。取这个名字也是有讲究的&#xff0c;在当时乃至如今的软件测试领域&#xff0c;QTP(UFT)占有率非常…

关于flink重新提交任务,重复消费kafka的坑

异常现象1 按照以下方式设置backend目录和checkpoint目录&#xff0c;fsbackend目录有数据&#xff0c;checkpoint目录没数据 env.getCheckpointConfig().setCheckpointStorage(PropUtils.getValueStr(Constant.ENV_FLINK_CHECKPOINT_PATH)); env.setStateBackend(new FsStat…

云原生监控系统Prometheus:基于Prometheus构建智能化监控告警系统

目录 一、理论 1.Promethues简介 2.监控告警系统设计思路 3.Prometheus监控体系 4.Prometheus时间序列数据 5.Prometheus的生态组件 6.Prometheus工作原理 7.Prometheus监控内容 8.部署Prometheus 9.部署Exporters 10.部署Grafana进行展示 二、实验 1.部署Prometh…

【C语言】字符函数和内存操作函数

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解字符函数和内存操作函数&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 一.字符函数1.1 字符分类函数1.2 字符转换函数 二.内存操作函数2.1 memcpy2.2…

鸿蒙手表开发之使用adb命令安装线上包

#国庆发生的那些事儿# 鸿蒙手表开发之使用adb命令安装线上包 前言&#xff1a; 由于之前的哥们匆忙离职了&#xff0c;所以鸿蒙手表项目的新版本我临时接过来打包发布&#xff0c;基本上之前没有啥鸿蒙经验&#xff0c;但是一直是做Android开发的&#xff0c;在工作人员的指…