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.rhkb.cn/news/150317.html

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

相关文章

东哥录了一些课程,你能想到应该都有了

哈喽&#xff0c;大家好&#xff0c;我是hahaCoderX。 我在B站录制了《快速入门C语言程序设计》、《Python3网络爬虫开发实战》、《机器学习实战》以及我的个人图书案例讲解指南等系列课程&#xff0c;目前正在陆续上传开放中&#xff0c;欢迎大家看我的视频&#xff0c;一块学…

第十课 贪心

文章目录 第十课 贪心lc 322.零钱兑换--中等题目描述代码展示 lc860.柠檬水找零--简单题目描述代码展示 lc455.分发饼干--简单题目描述代码展示 lc122.买卖股票的最佳时机II--中等题目描述代码展示 lc45.跳跃游戏II--中等题目描述代码展示 lc1665.完成所有任务的最少初始能量--…

基于SSM的商品营销系统计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

ARM汇编学习录 1 -基础概念

指令集概述 现阶段有四个不同的指令集 名称概述ARM3232位指令集Thumb16位指令集,ARM32子集,提供高密度低功耗Thumb232位指令集,ARMv6T2 引入.是thumb超集ARM6464位指令集 note&#xff1a; ARM某一个时刻只能运行单独ARM指令集或者Thumb指令,通过CPSR的T标志位决定. 如何当前…

熔断、限流、降级 —— SpringCloud Alibaba Sentinel

Sentinel 简介 Sentinel 是阿里中间件团队开源的&#xff0c;面向分布式服务架构的高可用流量防护组件&#xff0c;主要以流量为切入点&#xff0c;从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性 Sentinel 提供了两个服务组件…

AAU-net: 用于超声图像中乳腺病变分割的自适应注意力U-Net

AAU-net 期刊分析摘要贡献方法整体框架1.Hybrid Adaptive Attention Module2.Channel Self-Attention Block3.Spatial Self-Attention Block![在这里插入图片描述](https://img-blog.csdnimg.cn/629948402dc647d2b61817db3cd203f1.png) 实验1.消融实验1.1 Architecture Ablatio…

《protobuf》基础语法3

文章目录 默认值更新规则保留字段未知字段 默认值 在反序列化时&#xff0c;若被反序列化的二进制序列中不包含某个字段&#xff0c;则在反序列化时&#xff0c;就会设置对应默认值。不同的类型默认值不同&#xff1a; 类型默认值字符串“”布尔型false数值类型0枚举型0设置了…

基于风驱动优化的BP神经网络(分类应用) - 附代码

基于风驱动优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于风驱动优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.风驱动优化BP神经网络3.1 BP神经网络参数设置3.2 风驱动算法应用 4.测试结果&#x…

Zookeeper经典应用场景实战(一)

文章目录 1、Zookeeper Java客户端实战1.1、 Zookeeper 原生Java客户端使用1.2、 Curator开源客户端使用 2、 Zookeeper在分布式命名服务中的实战2.1、 分布式API目录2.2、 分布式节点的命名2.3、 分布式的ID生成器 3、Zookeeper实现分布式队列3.1、 设计思路3.2、 使用Apache …

Springboot学生成绩管理系统idea开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 springboot 学生成绩管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统 具有完整的源代码和数据库&…

Android:实现手机前后摄像头预览同开

效果展示 一.概述 本博文讲解如何实现手机前后两颗摄像头同时预览并显示 我之前博文《OpenGLES&#xff1a;GLSurfaceView实现Android Camera预览》对单颗摄像头预览做过详细讲解&#xff0c;而前后双摄实现原理其实也并不复杂&#xff0c;粗糙点说就是把单摄像头预览流程写两…

TikTok环保运动:短视频平台上的可持续发展

在当今社交媒体的繁荣时代&#xff0c;TikTok已经成为全球范围内年轻一代最喜爱的短视频分享平台之一。 数以亿计的用户在这里分享他们的创造力、生活片段和喜好。然而&#xff0c;随着全球环保意识的不断增强&#xff0c;TikTok也成为了一个独特的环境&#xff0c;倡导可持续…

Node-RED系列教程-25node-red获取天气

安装节点:node-red-contrib-weather 节点图标如下: 使用说明:node-red-contrib-weather (node) - Node-RED 流程图中填写经度和纬度即可。 演示: json内容: {

【重磅】这就是元宇宙碰撞的后果

筹备了一年多——朋友们&#xff0c;它终于来了&#xff01; 我们刚刚宣布官方 Aavegotchi x Sandbox 在 X 上共享元宇宙体验。 10 月 25 日在 The Sandbox 上线&#xff0c;有两份可领取的空投。 Gotchi 游戏即将爆发。你们兴奋吗&#xff1f;

氟化钡镜片

氟化钡晶体具有良好的光学透过性能&#xff0c;在0.15μm-14.5μm的光谱范围内&#xff0c;可以用作紫外和红外光学窗口。同时&#xff0c;又具有优良的闪烁性能&#xff0c;成为高能物理与核物理、核医学等领域中重要的晶体材料。 特此记录 anlog 2023年10月7日

Linux 逻辑卷

目录 一、认识 1、概念 2、术语&#xff1a; 1&#xff09;物理存储设备 2&#xff09;物理卷 3&#xff09;卷组 4&#xff09;PE物理区域 5&#xff09;逻辑卷 6&#xff09;LE逻辑区域 7&#xff09;VGDA卷组描述符区域 二、部署逻辑卷 1、物理卷管理 2、卷组…

防御安全第五次作业

1. 什么是数据认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段&#xff1f; 数据认证是指保证数据的真实性、完整性和可信度&#xff0c;以确保数据不被篡改或伪造。其作用包括但不限于&#xff1a; 保护关键数据不被恶意篡改或损坏 提供数据来源的可靠性和安全性…

E: Unable to locate package XXX

问题描述&#xff1a; 当使用 apt-get install XXX 安装包时&#xff0c;出现错误 E: Unable to locate package XXX 解决方法&#xff1a; apt-get update apt-get install XXX

为什么append到父节点后的子节点发生修改,父节点打印出来的也会变化

今天走查前端代码&#xff0c;发现历史代码写出来的不规范&#xff0c;但是他还是在生产运行了很久的代码&#xff0c;仔细思量后发现&#xff0c;其实原理是对的&#xff0c;只是看起来不美观&#xff0c;不易读而已。 废话不说&#xff0c;先上demo代码 <!DOCTYPE html&g…

【Spring Boot】创建一个 Spring Boot 项目

创建一个 Spring Boot 项目 1. 安装插件2. 创建 Spring Boot 项目3. 项目目录介绍和运行注意事项 1. 安装插件 IDEA 中安装 Spring Boot Helper / Spring Assistant / Spring Initializr and Assistant插件才能创建 Spring Boot 项⽬ &#xff08;有时候不用安装&#xff0c;直…