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开源的相关代码(github.com/JetBrains/a...)中一探究竟了

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

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

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

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

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

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

kotlin 复制代码
@AnyThread
  override 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外的剪贴板操作.

kotlin 复制代码
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回调监听

kotlin 复制代码
public interface ContentChangedListener extends EventListener {
    void contentChanged(final @Nullable Transferable oldTransferable, final Transferable newTransferable);
  }

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

kotlin 复制代码
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方法

kotlin 复制代码
private fun setDeviceClipboard(text: String, forceSend: Boolean) {
    //文本长度是否超过最大同步剪贴板长度默认为5000
    val maxSyncedClipboardLength = DeviceMirroringSettings.getInstance().maxSyncedClipboardLength
    //如果forceSend为true,或者text非空且与lastClipboardText不同.则走发送流程
    if (forceSend || (text.isNotEmpty() && text != lastClipboardText)) {
      val adjustedText = when {
        text.length <= maxSyncedClipboardLength -> text
        forceSend -> ""
        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 是如何给设备发送消息的

kotlin 复制代码
//创建基于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就是在该功能与设备建立代理连接时创建的

kotlin 复制代码
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).port
    logger.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的加载和相关参数的传递

kotlin 复制代码
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 {
          //日志收集处理
          ...
        }
      }
      ...
  }
kotlin 复制代码
//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()方法

cpp 复制代码
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);
}
cpp 复制代码
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();
  }
  //运行Controller
  controller_->Run();
  Shutdown();
}

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

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

cpp 复制代码
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)完成连接建立

kotlin 复制代码
private suspend fun connectChannels(serverSocketChannel: SuspendingServerSocketChannel) {
    //接受两个链接channel1和channel2
    val 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 = channel1
        controlChannel = channel2
      }
      else if (m1 == CONTROL_CHANNEL_MARKER && m2 == VIDEO_CHANNEL_MARKER) {
        videoChannel = channel2
        controlChannel = 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方法

cpp 复制代码
void Controller::Run() {
  Log::D("Controller::Run");
  Initialize();

  try {
    //无限循环中接收和处理控制消息
    for (;;) {
      if (max_synced_clipboard_length_ != 0) {
        //clipboard_changed_是否为true
        if (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方法

cpp 复制代码
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方法

java 复制代码
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方法

cpp 复制代码
void Controller::StartClipboardSync(const StartClipboardSyncMessage& message) {
  ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);
  ...
  //通过max_synced_clipboard_length_大小判断之前是否停止了剪贴板,max_synced_clipboard_length_默认为0
  bool was_stopped = max_synced_clipboard_length_ == 0;
  //更新同步文本最大长度
  max_synced_clipboard_length_ = message.max_synced_length();
  if (was_stopped) {
    clipboard_manager->AddClipboardListener(&clipboard_listener_);
  }
}
cpp 复制代码
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方法尝试更新剪贴板监听器列表,没有被其他线程修改则为true
    if (clipboard_listeners_.compare_exchange_strong(old_listeners, new_listeners)) {
      if (old_listeners->empty()) {
        //那么检查旧的监听器列表为空,那么调用ClipboardAdapter的enablePrimaryClipChangedListener
        clipboard_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

java 复制代码
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);
    }
  }
java 复制代码
public class ClipboardListener extends IOnPrimaryClipChangedListener.Stub {
  @Override
  public native void dispatchPrimaryClipChanged();
}

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

cpp 复制代码
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

cpp 复制代码
void Controller::Run() {
  Log::D("Controller::Run");
  Initialize();

  try {
    //无限循环中接收和处理控制消息
    for (;;) {
      if (max_synced_clipboard_length_ != 0) {
        //clipboard_changed_是否为true
        if (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_length
  int 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侧

kotlin 复制代码
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
        }
      }
    }
  }

@AnyThread
  override fun onDeviceClipboardChanged(text: String) {
    UIUtil.invokeLaterIfNeeded { // This is safe because this code doesn't touch PSI or VFS.
      if (text != lastClipboardText) {
        lastClipboardText = text
        copyPasteManager.setContents(StringSelection(text))
      }
    }
  }

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

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

总结

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

流程图

最后再通过流程图回顾一下整体流程

参考资料

github.com/JetBrains/a...

cs.android.com/android-stu...

相关推荐
氤氲息41 分钟前
Android 底部tab,使用recycleview实现
android
Clockwiseee1 小时前
PHP之伪协议
android·开发语言·php
小林爱1 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
小何开发2 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟3 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_438150993 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu4 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜4 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp
键盘侠0074 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp
江上清风山间明月5 小时前
flutter bottomSheet 控件详解
android·flutter·底部导航·bottomsheet