Android Studio 是如何和我们的手机共享剪贴板的

news2024/11/19 10:42:46

背景

近期完成了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内部剪贴板变更的监听

@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外的剪贴板操作.

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) {
    //文本长度是否超过最大同步剪贴板长度默认为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 是如何给设备发送消息的

//创建基于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).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的加载和相关参数的传递

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();
  }
  //运行Controller
  controller_->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和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方法

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方法

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_默认为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_);
  }
}
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

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 {
  @Override
  public 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_是否为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侧

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连接等等技术,虽然整体原理比较简单,但是各种细节确实不少,其中有不少技术因为本人能力有限无法全面能力分析,如有遗漏错误欢迎斧正.

流程图

在这里插入图片描述

参考资料

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.coloradmin.cn/o/1066073.html

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

相关文章

静态住宅代理的优缺点以及使用方法

住宅代理ip分为静态住宅代理和动态住宅代理&#xff0c;住宅代理就是代理ip是真人网络中出发&#xff0c;以代理地址来对目标网站进行访问&#xff0c;具有高匿名度和安全性&#xff0c;在跨境业务中经常使用。而静态住宅代理的特征就是ip地址长时间固定不变&#xff0c;具有唯…

DevEco Studio下载/安装与配置开发环境

一、下载与安装DevEco Studio 在HarmonyOS应用开发学习之前&#xff0c;需要进行一些准备工作&#xff0c;首先需要完成开发工具DevEco Studio的下载与安装以及环境配置。 1.进入DevEco Studio下载官网 单击“立即下载”进入下载页面。 DevEco Studio提供了Windows版本和Mac…

html2Canvas截图中特殊字体出不来原因

项目中有正常字体和特殊字体,但是导出成图片时特殊字体无法正常显示 下图为导出的截图: 原因: 特殊字体的名称定义 因为之前特殊字体的font-family,是以数字开头,放在html中被转义了,所以一直出不来,前面改成英文后可以正常显示

让丢失成为过去,尽在我们的智能防丢器

我们都知道&#xff0c;生活中总会遇到一些小烦恼&#xff0c;比如钥匙不见了&#xff0c;钱包忘在哪里&#xff0c;甚至手机掉在了不知名的地方&#xff0c;这些看似小事&#xff0c;却足以打乱我们的日程。那么&#xff0c;有没有一种方法&#xff0c;可以让这些烦恼一扫而空…

Ubuntu 2204 搭建 nextcloud 个人网盘

Nextcloud是一套用于创建网络硬盘/云盘以存放文件的客户端-服务器软件&#xff0c;Nextcloud 完全开源并且免费。 一、搭建 ubuntu apache2 mysql php &#xff08;lamp&#xff09;环境 因为 nextcloud 服务是使用 php 语言和 mysql 数据库的web服务&#xff0c;因此需要…

【一周安全资讯1007】多项信息安全国家标准10月1日起实施;GitLab发布紧急安全补丁修复高危漏洞

要闻速览 1.以下信息安全国家标准10月1日起实施 2.GitLab发布紧急安全补丁修复高危漏洞 3.主流显卡全中招&#xff01;GPU.zip侧信道攻击可泄漏敏感数据 4.MOVEit漏洞导致美国900所院校学生信息发生大规模泄露 5.法国太空和国防供应商Exail遭黑客攻击&#xff0c;泄露大量敏感…

Safran与是德科技合作为蔚来提供电动汽车中的5G和C-V2X连接测试

概述 虹科Safran GNSS模拟器助力是德科技&#xff08;Keysight&#xff09;为中国顶级电动汽车制造商之一——蔚来汽车&#xff08;NIO&#xff09;提供了业界领先的UXM 5G测试解决方案以验证5G和C-V2X的连接性&#xff0c;能够根据3GPP和其他标准组织定义的最新5G新无线电&am…

理解ES的refresh、flush、merge

一、refresh 对于任何数据库的写入来讲fsync刷盘虽然保证的数据的安全但是如果每次操作都必须fsync一次&#xff0c;那fsync操作将是一个巨大的操作代价&#xff0c;在衡量对数据安全与操作代价下&#xff0c;ES引入了一个较轻量的操作refresh操作来避免频繁的fsync操作。 1.1…

如何优雅构建自定义 Spring Boot 验证器,让你的代码更加丝滑!

作为一名开发人员&#xff0c;你应该知道确保应用程序中流动的数据的准确性和完整性是多么重要。Spring Boot提供了强大的验证功能&#xff0c;但有时我们需要额外的验证&#xff0c;创建适合特定需求的自定义验证器。 接下来&#xff0c;我们来介绍下如何完整的创建一个自定义…

九、分枝切割算法

文章目录 1、Gomory切割的算法原理2、分枝切割算法THE END 1、Gomory切割的算法原理 \qquad 考虑有一个等式的形式如下所示&#xff1a; I L F f ILFf ILFf \qquad 其中各项满足以下性质&#xff1a; I L IL IL是一个整数值的表达式 F F F是一个严格正分数的和 f < 1 f&…

SpringBoot项目默认使用HikariDataSource数据库连接池修改使用Druid连接池

1.启动项目&#xff0c;查看正在使用的链接池。 2.在pom.xml文件中引入驱动 <dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version></dependency> 3.在ap…

【广州华锐互动】VR虚拟现实编辑软件的独特优势

随着科技的不断发展&#xff0c;虚拟现实(Virtual Reality&#xff0c;简称VR)技术已经成为了一种新兴的交互方式。由广州华锐互动研发的VR虚拟现实编辑软件&#xff0c;是一种用于创建虚拟现实内容的工具&#xff0c;它可以让用户在虚拟环境中自由地创作和体验各种内容。 VR虚…

JDBC-day01(JDBC概述,获取数据库连接)

一&#xff1a;JDBC概述 1.数据持久化 持久化(persistence)&#xff1a;把数据保存到可掉电式存储设备中以供之后使用。大多数情况下&#xff0c;数据持久化意味着将内存中的数据保存到硬盘上加以”固化”&#xff0c;而持久化的实现过程大多通过各种关系数据库来完成。简单来…

现货白银图表分析的依据

现货白银的行情图表分析其实与股票的差不多&#xff0c;投资者可以结合均线、k线的变化&#xff0c;来分析实时的行情走势。当走势图的均线呈多头排列&#xff0c;即短期、中期、长期均线依次从上到下排列并向右上方运行&#xff0c;且白银价格沿各均线向右上方拉升&#xff0c…

小匠物联获评2023年度浙江省省工业设计企业

小匠物联省工业设计企业 2023年9月25日&#xff0c;浙江省经济和信息化厅公布了2023年度省级工业设计中心拟认定名单。 喜报传来&#xff0c;小匠物联成功通过认定&#xff0c;荣获2023年度浙江省省工业设计企业称号。 小匠物联获权威肯定 浙江省人民政府此次开展省级工业设计中…

【赠书活动】Excel透视表的简单应用

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

2023年中国铁路安全行车系统市场规模现状及行业细分市场分析[图]

铁路行车安全系统是铁路运输组织信息系统的组成部分&#xff0c;涉及铁路通信系统、铁路信号系统、行车监测系统、信号联锁系统、车号识别及其他等。 在我国铁路营业里程不断增长、行车速度逐步提高、列车运行密度不断加大的背景下&#xff0c;运输安全控制需求日益提升。我国铁…

【Sentinel】Sentinel原码分析

本文内容来自【黑马】Sentinel从使用到源码解读笔记&#xff0c;做了部分修改和补充 目录 Sentinel 基本概念 基本流程 Node Entry 定义资源的两种方式 使用try-catch定义资源 使用注解标记资源 基于注解标记资源的实现原理 Context 什么是Context Context的初始化 …

andriod studio运行失败设置

(1) 重新设置maven maven("https://maven.aliyun.com/nexus/content/groups/public/")(2) 删除dependencies中的maven依赖 (3)

和力链携手纷享销客推动CRM业财一体化,引领大健康产业数智化发展

两化深度融合正在加速产业转型升级、重塑产业结构&#xff0c;为传统行业注入发展新活力&#xff0c;江西和力物联实业有限公司&#xff08;以下简称“和力链”&#xff09;正是这样一家推动医药大健康产业数智化发展的高新技术企业。 和力链是国内首家大健康供应链产能数字化…