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}}
这个方法的整理流程还是比较清晰的:
- 从
DeviceMirroringSettings
实例中获取剪贴板同步的最大文本长度,默认为5000。 - 检查是否需要发送剪贴板内容。如果
forceSend
为true
,或者text
非空且与lastClipboardText
不同,那么就需要发送。 - 如果需要发送,根据
text
的长度和maxSyncedClipboardLength
来调整要发送的文本内容。如果text
的长度小于或等于maxSyncedClipboardLength
,那么就发送text
。如果forceSend
为true
,那么发送空字符串。否则,函数直接返回,不做任何操作。 - 创建一个
StartClipboardSyncMessage
实例,这个实例包含了maxSyncedClipboardLength
和调整后的文本内容。 - 调用
deviceController
的sendControlMessage
方法,将StartClipboardSyncMessage
实例发送给设备控制器。 - 将
lastClipboardText
设置为调整后的文本内容。
这里涉及到两个对象StartClipboardSyncMessage
和deviceController
,其中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.}...}
整体流程如下:
- 在协程中异步推送代理到设备.
- 创建一个异步服务器套接字通道并绑定到一个随机端口.
- 创建设备反向代理,它将设备上的一个socket转发到电脑上的一个TCP端口。
- 启动代理对象
- 建立代理连接
- 创建DeviceController控制设备
看到这里这里,疑问点就更多了,这里的代理是指什么,代理对象是如何启动的,连接又是怎么建立的,controlChannel是哪来的
问题2.1 代理是什么
这里的代理指的是两个文件:screen-sharing-agent.jar和libscreen-sharing-agent.so.这里我们可以简单了解一下他们的作用
screen-sharing-agent.jar
: 主要负责启动libscreen-sharing-agent.so
,处理NDK无法支持的MediaCodecList、MediaCodecInfo的编码视频流以及剪贴板监听同步等功能。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方法:
- 将代理对象通过adb 命令发送到设备中
- 创建一个socket对象绑定随机端口,通过adb命令将设备socket与此端口建立反向代理
- 启动代理DeviceClient后通过此socket获取控制连接和视频连接.
- 将控制连接用于创建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之间的区别?各自优缺点?
面试题来源: 《大数据面试题 V4.0》 大数据面试题V3.0,523道题,679页,46w字 可回答: 1)spark和maprecude的对比;2)mapreduce与spark优劣好处 问过的一些公司:阿里云…...

【开发篇】十八、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 接收数据的时候发现数据接收出现粘包的现象,并且实时性很差,通过日志的时间戳发现数据接收的误差在 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 前,我们需要先将系统中以前的环境给卸载掉。 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 日志,就记录对数据库进行的操作,什么增删改的操作全…...

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

网络安全--安全认证、IPSEC技术
目录 1. 什么是数据认证,有什么作用,有哪些实现的技术手段? 2. 什么是身份认证,有什么作用,有哪些实现的技术手段? 3. 什么是VPN技术? 4. VPN技术有哪些分类? 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)目标检测评价指标
前言:Hello大家好,我是小哥谈。目标检测评价指标是用来衡量目标检测算法性能的指标,可以分为两类,包括框级别评价指标和像素级别评价指标。本节课就给大家重点介绍下目标检测中的相关评价指标及其含义,希望大家学习之后…...
前端和后端是Web开发中的两个不同的领域,你更倾向于哪一种?
前端和后端是Web开发中的两个不同的领域,你更倾向于哪一种? 你可以从以下几个维度谈谈你对前端开发和后端开发的看法。此为内容创作模板,在发布之前请将不必要的内容删除 一、引言 提示:可对前端开发和后端开发进行简要介绍并提出…...

SpringBoot集成MyBatis-Plus实现增删改查
背景 因为学习工具的时候经常需要用到jar包,需要增删查改接口,所以参考文章实现了基于mybatis-plus的增删查改接口。 参考文章:第二十二节:SpringBoot集成MyBatis-Plus实现增删改查 原文中的git地址不存在,本文内容是原文代码修…...
基于STM32设计的智能水产养殖系统(华为云IOT)
一、设计简述 基于STM32设计的智能水产养殖监测系统 1.1 项目背景 随着经济的发展和人口的增长,对水产养殖的需求不断增加。然而,传统的水产养殖方式存在一系列问题,如水质污染、鱼病爆发等。因此,智能化水产养殖技术成为当前热门研究领域。其中,基于物联网技术的智能水产…...

运行软件找不到mfc140u.dll怎么解决,mfc140u.dll是什么文件
"找不到 mfc140u.dll"是一条错误信息,表示您的计算机上缺少一个名为 mfc140u.dll 的动态链接库(DLL)文件。这个文件通常与 Microsoft Visual C Redistributable 相关。Mfc140u.dll 是 Microsoft 基础类库(MFC࿰…...

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

浅谈智能安全配电装置在老年人建筑中的应用
摘要:我国每年因触电伤亡人数非常多,大多数事故是发生在用电设备和配电装置。在电气事故中,无法预料和不可抗拒的事故是比较少的,大量用电事故可采取切实可行措施来预防。本文通过结合老年人建筑的特点和智能安全配电装置的功能&a…...
【ES】笔记-ES6模块化
暴露数据引入模块语法 规范基本语法分别暴露 (按需暴露)统一暴露 export {暴露内容1,暴露内容2}默认暴露 (适合只暴露一个数据) 只能暴露一次同时使用在app.js中引入 规范 每个文件都是一个模块要借助Babel和Browserify依次编译代码,才能在浏览器端运行…...
阿里云/腾讯云国际站代理:腾讯云国际站开户购买EdgeOne发布,安全加速一体化方案获业内认可
作为下一代CDN产品面世的腾讯云EdgeOne,历时一年服务,腾讯云国际站凭借安全加速一体化的解决方案,用All in One 架构构筑边缘应用无限想象。 近年来,随着5G网络、物联网、边缘计算的快速发展,爆炸式增长的数据量和市场…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...