背景
近期完成了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