国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁(yè) > news >正文

手機(jī)網(wǎng)站開(kāi)發(fā)學(xué)習(xí)免費(fèi)單頁(yè)網(wǎng)站在線制作

手機(jī)網(wǎng)站開(kāi)發(fā)學(xué)習(xí),免費(fèi)單頁(yè)網(wǎng)站在線制作,在深圳注冊(cè)公司有什么好處,元氏網(wǎng)站建設(shè)背景 近期完成了target33的項(xiàng)目適配升級(jí),隨著AGP和gradle的版本升級(jí),萬(wàn)年老版本Android Studio(后文簡(jiǎn)稱AS)也順便升級(jí)到了最新版Android Studio Giraffe | 2022.3.1,除了新UI外,最讓我好奇的是這次的Running Devices功能(官方也稱為Device mirroring)可以控制真機(jī)了. 按照操…

背景

近期完成了target33的項(xiàng)目適配升級(jí),隨著AGP和gradle的版本升級(jí),萬(wàn)年老版本Android Studio(后文簡(jiǎn)稱AS)也順便升級(jí)到了最新版Android Studio Giraffe | 2022.3.1,除了新UI外,最讓我好奇的是這次的Running Devices功能(官方也稱為Device mirroring)可以控制真機(jī)了.

按照操作提示完成開(kāi)啟后就能在AS看到看到類(lèi)似scrcpy和Vysor的手機(jī)控制界面.其中最讓我驚喜的是剪貼板共享功能.這對(duì)于我這種需要在PC和手機(jī)頻繁拷貝測(cè)試數(shù)據(jù)的人來(lái)說(shuō)無(wú)疑降低了很多開(kāi)發(fā)成本.

在這里插入圖片描述

在這里插入圖片描述

疑問(wèn)

目前業(yè)內(nèi)大部分剪貼板同步工具是基于局域網(wǎng)實(shí)現(xiàn)的,Android Studio(后續(xù)用AS替代)是如何做到PC和手機(jī)不在同一局域網(wǎng)的情況下實(shí)現(xiàn)剪貼板同步的呢?

實(shí)現(xiàn)

太長(zhǎng)不看版

AS運(yùn)行時(shí)會(huì)通過(guò)adb給設(shè)備推送一個(gè)agent的jar包和so文件.之后通過(guò)adb啟動(dòng)這個(gè)agent,并與這個(gè)agent建立了一個(gè)socket通信. AS和agent分別監(jiān)聽(tīng)PC和設(shè)備的剪貼板變更,再通過(guò)socket進(jìn)行數(shù)據(jù)傳遞同步

在這里插入圖片描述

從網(wǎng)上沒(méi)有搜索出太多資料,只能去看看從JetBrains開(kāi)源的相關(guān)代碼(https://github.com/JetBrains/android/tree/master)中一探究竟了

從代碼的提交記錄中可以發(fā)現(xiàn)監(jiān)聽(tīng)版相關(guān)的邏輯主要集中在DeviceClipboardSynchronizer.kt中,簡(jiǎn)單分析一下它的核心成員變量和方法

成員變量功能
deviceClient用于與設(shè)備通信
copyPasteManager用于獲取和設(shè)置主機(jī)上的剪貼板內(nèi)容
deviceController用于向設(shè)備發(fā)送控制消息
focusOwnerListener用于偵聽(tīng)主機(jī)上焦點(diǎn)所有者的更改。
lastClipboardText與設(shè)備同步的最后一個(gè)剪貼板文本的字符串
方法功能
setDeviceClipboard設(shè)置設(shè)備剪貼板與主機(jī)剪貼板內(nèi)容相同
getClipboardText從主機(jī)剪貼板獲取文本
contentChanged當(dāng)主機(jī)剪貼板內(nèi)容更改時(shí)回調(diào)
onDeviceClipboardChanged設(shè)備剪貼板內(nèi)容更改時(shí)回調(diào)

整體作用還是比較清晰的,那我們就以DeviceClipboardSynchronizer.kt為核心,仔細(xì)梳理一下AS是如何獲取PC的剪貼板數(shù)據(jù)、將剪貼板數(shù)據(jù)發(fā)送給手機(jī)、手機(jī)如何更新剪貼板數(shù)據(jù)并監(jiān)聽(tīng)設(shè)備剪貼板回傳給AS的

問(wèn)題1.AS如何獲取PC的剪貼板數(shù)據(jù)

DeviceClipboardSynchronizer中獲取PC剪貼板的場(chǎng)景有兩種:

1、PC剪貼板內(nèi)容變更的通知-用于在AS內(nèi)部剪貼板變更的監(jiān)聽(tīng)

@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初始化、獲取焦點(diǎn)時(shí)-用于彌補(bǔ)在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)}}

其中場(chǎng)景1通過(guò)CopyPasteManager.ContentChangedListener回調(diào)監(jiān)聽(tīng)

public interface ContentChangedListener extends EventListener {void contentChanged(final @Nullable Transferable oldTransferable, final Transferable newTransferable);}

場(chǎng)景2通過(guò)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側(cè)獲取PC剪貼板相關(guān)內(nèi)容是通過(guò)com.intellij.openapi.ide.CopyPasteManager組件實(shí)現(xiàn)的,它是IntelliJ IDEA提供的一個(gè)用于負(fù)責(zé)復(fù)制和粘貼的接口組件用來(lái)抹平不同運(yùn)行環(huán)境的差異,這里我們不細(xì)究CopyPasteManager的具體實(shí)現(xiàn),如果各位感興趣可以查看IDEA相關(guān)源碼

總結(jié):AS在獲取焦點(diǎn)或者在AS內(nèi)監(jiān)聽(tīng)到剪貼板變化時(shí)會(huì)調(diào)用IDEA的CopyPasteManager獲取PC的剪貼板內(nèi)容.

問(wèn)題2.AS如何將剪貼板數(shù)據(jù)發(fā)送給手機(jī)的

從之前的代碼中可以看到AS獲取到剪貼板數(shù)據(jù)后會(huì)調(diào)用setDeviceClipboard方法

private fun setDeviceClipboard(text: String, forceSend: Boolean) {//文本長(zhǎng)度是否超過(guò)最大同步剪貼板長(zhǎng)度默認(rèn)為5000val maxSyncedClipboardLength = DeviceMirroringSettings.getInstance().maxSyncedClipboardLength//如果forceSend為true,或者text非空且與lastClipboardText不同.則走發(fā)送流程if (forceSend || (text.isNotEmpty() && text != lastClipboardText)) {val adjustedText = when {text.length <= maxSyncedClipboardLength -> textforceSend -> ""else -> return}//創(chuàng)建StartClipboardSyncMessage實(shí)例val message = StartClipboardSyncMessage(maxSyncedClipboardLength, adjustedText)//deviceController的sendControlMessage方法,將StartClipboardSyncMessage實(shí)例發(fā)送給設(shè)備控制器deviceController?.sendControlMessage(message)lastClipboardText = adjustedText}}

這個(gè)方法的整理流程還是比較清晰的:

  1. DeviceMirroringSettings實(shí)例中獲取剪貼板同步的最大文本長(zhǎng)度,默認(rèn)為5000。
  2. 檢查是否需要發(fā)送剪貼板內(nèi)容。如果forceSendtrue,或者text非空且與lastClipboardText不同,那么就需要發(fā)送。
  3. 如果需要發(fā)送,根據(jù)text的長(zhǎng)度和maxSyncedClipboardLength來(lái)調(diào)整要發(fā)送的文本內(nèi)容。如果text的長(zhǎng)度小于或等于maxSyncedClipboardLength,那么就發(fā)送text。如果forceSendtrue,那么發(fā)送空字符串。否則,函數(shù)直接返回,不做任何操作。
  4. 創(chuàng)建一個(gè)StartClipboardSyncMessage實(shí)例,這個(gè)實(shí)例包含了maxSyncedClipboardLength和調(diào)整后的文本內(nèi)容。
  5. 調(diào)用deviceControllersendControlMessage方法,將StartClipboardSyncMessage實(shí)例發(fā)送給設(shè)備控制器。
  6. lastClipboardText設(shè)置為調(diào)整后的文本內(nèi)容。

這里涉及到兩個(gè)對(duì)象StartClipboardSyncMessagedeviceController ,其中StartClipboardSyncMessage 是一個(gè)傳輸數(shù)據(jù)的封裝類(lèi),繼承自ControlMessage,用于標(biāo)識(shí)剪貼板消息類(lèi)型及序列化和反序列化的實(shí)現(xiàn).而deviceController 主要功能是通過(guò)發(fā)送控制消息來(lái)控制設(shè)備.

下面我們看來(lái)看看deviceController.sendControlMessage 是如何給設(shè)備發(fā)送消息的

//創(chuàng)建基于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()}
...

我們可以看到在類(lèi)的初始化階段創(chuàng)建了一個(gè)基于Base128編碼的輸出流,剪貼板數(shù)據(jù)被序列化到輸出流中,之后刷新了輸出流完成數(shù)據(jù)發(fā)送.根據(jù)newOutputStream的相關(guān)注釋說(shuō)明,它會(huì)由傳入的channel生成一個(gè)新的輸出流.

而controlChannel是在DeviceController 初始化時(shí)傳入的,層層回溯,最終在DeviceClient中創(chuàng)建的

DeviceClient主要功能是負(fù)責(zé)實(shí)現(xiàn)AS的設(shè)備的屏幕鏡像功能,會(huì)通過(guò)和設(shè)備建立代理連接完成控制通道和視頻通道的建立,而我們關(guān)注的controlChannel就是在該功能與設(shè)備建立代理連接時(shí)創(chuàng)建的

private suspend fun startAgentAndConnect(maxVideoSize: Dimension, initialDisplayOrientation: Int, startVideoStream: Boolean) {...//1.在協(xié)程中異步推送代理到設(shè)備。val agentPushed = coroutineScope {async {pushAgent(deviceSelector, adb)}}//2.創(chuàng)建一個(gè)異步服務(wù)器socket通道并綁定到一個(gè)隨機(jī)端口。@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.創(chuàng)建設(shè)備反向代理,它將設(shè)備上的一個(gè)設(shè)備上的抽象套接字轉(zhuǎn)發(fā)到電腦上的一個(gè)TCP端口。ClosableReverseForwarding(deviceSelector, SocketSpec.LocalAbstract(socketName), SocketSpec.Tcp(port), adb).use {it.startForwarding()agentPushed.await()//4.啟動(dòng)代理對(duì)象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.創(chuàng)建DeviceController來(lái)控制設(shè)備deviceController = DeviceController(this, controlChannel)}catch (e: IncorrectOperationException) {return // Already disposed.}...}

整體流程如下:

  1. 在協(xié)程中異步推送代理到設(shè)備.
  2. 創(chuàng)建一個(gè)異步服務(wù)器套接字通道并綁定到一個(gè)隨機(jī)端口.
  3. 創(chuàng)建設(shè)備反向代理,它將設(shè)備上的一個(gè)socket轉(zhuǎn)發(fā)到電腦上的一個(gè)TCP端口。
  4. 啟動(dòng)代理對(duì)象
  5. 建立代理連接
  6. 創(chuàng)建DeviceController控制設(shè)備

看到這里這里,疑問(wèn)點(diǎn)就更多了,這里的代理是指什么,代理對(duì)象是如何啟動(dòng)的,連接又是怎么建立的,controlChannel是哪來(lái)的

問(wèn)題2.1 代理是什么

這里的代理指的是兩個(gè)文件:screen-sharing-agent.jar和libscreen-sharing-agent.so.這里我們可以簡(jiǎn)單了解一下他們的作用

  1. screen-sharing-agent.jar: 主要負(fù)責(zé)啟動(dòng)libscreen-sharing-agent.so ,處理NDK無(wú)法支持的MediaCodecList、MediaCodecInfo的編碼視頻流以及剪貼板監(jiān)聽(tīng)同步等功能。
  2. libscreen-sharing-agent.so: 主要負(fù)責(zé)命令解析,設(shè)備視頻解碼、渲染等等功能.

篇幅有限,這里就不再展開(kāi)了,有興趣的可以查看相關(guān)源碼

問(wèn)題2.2 代理是如何啟動(dòng)的

第一步中會(huì)通過(guò)pushAgent將screen-sharing-agent.jar和libscreen-sharing-agent.so推送到設(shè)備的/data/local/tmp/.studio目錄中,并設(shè)置好權(quán)限

之后調(diào)用startAgent()啟動(dòng)代理對(duì)象,startAgent()通過(guò)adb命令啟動(dòng)了代理中的com.android.tools.screensharing.Main方法,最終完成libscreen-sharing-agent.so的加載和相關(guān)參數(shù)的傳遞

private suspend fun startAgent(deviceSelector: DeviceSelector,adb: AdbDeviceServices,socketName: String,maxVideoSize: Dimension,initialDisplayOrientation: Int,startVideoStream: Boolean) {...//并設(shè)置代理程序的類(lèi)路徑,然后使用app_process命令啟動(dòng)代理程序的主類(lèi),并傳入了根據(jù)入?yún)?gòu)建一系列的命令行參數(shù)。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()}"    //在一個(gè)新的協(xié)程作用域中執(zhí)行這個(gè)命令,使用Dispatchers.Unconfined調(diào)度器確保能夠正常終止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);
}

問(wèn)題2.3 代理連接是怎么建立的

在問(wèn)題2.2 代理是如何啟動(dòng)的中我們發(fā)現(xiàn)startAgent最終會(huì)調(diào)用到代理libscreen-sharing-agent.so的nativeMain()方法

Java_com_android_tools_screensharing_Main_nativeMain(JNIEnv* jni_env, jclass thisClass, jobjectArray argArray) {、...//創(chuàng)建agent對(duì)象,并啟動(dòng)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() {...//創(chuàng)建DisplayStreamer對(duì)象處理視頻流display_streamer_ = new DisplayStreamer(display_id_, codec_name_, max_video_resolution_, initial_video_orientation_, max_bit_rate_, CreateAndConnectSocket(socket_name_));//創(chuàng)建Controller對(duì)象處理控制命令,調(diào)用CreateAndConnectSocket創(chuàng)建Socket用于初始化controller_ = new Controller(CreateAndConnectSocket(socket_name_));Log::D("Created video and control sockets");if ((flags_ & START_VIDEO_STREAM) != 0) {StartVideoStream();}//運(yùn)行Controllercontroller_->Run();Shutdown();
}

我們可以發(fā)現(xiàn)啟動(dòng)代理時(shí),最終會(huì)在代理的cpp中創(chuàng)建了一個(gè)DisplayStreamer對(duì)象和一個(gè)Controller對(duì)象,并根據(jù)條件允許,因?yàn)楸疚哪康氖桥產(chǎn)s是如何處理剪貼板數(shù)據(jù)的,我們重點(diǎn)關(guān)注Controller的相關(guān)邏輯.

首先Controller對(duì)象創(chuàng)建時(shí),會(huì)先調(diào)用CreateAndConnectSocket創(chuàng)建Socket用于初始化,該方法會(huì)使用DeviceClient傳入的socketname作為名稱創(chuàng)建一個(gè)UNIX域Socket并進(jìn)行連接.之后將該socket的描述符返回傳入Controller構(gòu)造函數(shù)

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';//寫(xiě)入一個(gè)字符`C`到之前創(chuàng)建的socket中,用于發(fā)送一個(gè)標(biāo)記write(socket_fd_, &channel_marker, sizeof(channel_marker));  // Control channel marker.
}

我們發(fā)現(xiàn)在Controller的構(gòu)建函數(shù)中,會(huì)通過(guò)Socket寫(xiě)入一個(gè)標(biāo)記”C”,(DisplayStreamer中會(huì)寫(xiě)入標(biāo)記“V”).在上文的DeviceClient的startAgentAndConnect方法中,我們知道在調(diào)用了startAgent()方法啟動(dòng)代理對(duì)象后,會(huì)調(diào)用connectChannels(serverSocketChannel)完成連接建立

private suspend fun connectChannels(serverSocketChannel: SuspendingServerSocketChannel) {//接受兩個(gè)鏈接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 {//接收標(biāo)記val marker1 = async { readChannelMarker(channel1) }val marker2 = async { readChannelMarker(channel2) }val m1 = marker1.await()val m2 = marker2.await()//根據(jù)"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()}

至此我們就通過(guò)代理完成了videoChannel和controlChannel的連接

總結(jié):AS的DeviceClient會(huì)在與設(shè)備建立連接時(shí)會(huì)通過(guò)startAgentAndConnect方法:

  1. 將代理對(duì)象通過(guò)adb 命令發(fā)送到設(shè)備中
  2. 創(chuàng)建一個(gè)socket對(duì)象綁定隨機(jī)端口,通過(guò)adb命令將設(shè)備socket與此端口建立反向代理
  3. 啟動(dòng)代理DeviceClient后通過(guò)此socket獲取控制連接和視頻連接.
  4. 將控制連接用于創(chuàng)建DeviceController

AS的DeviceClipboardSynchronizer通過(guò)DeviceClient.deviceController傳遞剪貼板數(shù)據(jù)完成數(shù)據(jù)通信

問(wèn)題3.手機(jī)如何更新剪貼板數(shù)據(jù)并監(jiān)聽(tīng)設(shè)備剪貼板回傳給AS的

在了解了AS是如何給手機(jī)發(fā)送剪貼板數(shù)據(jù)后,那還剩下兩個(gè)問(wèn)題,AS發(fā)送的剪貼板數(shù)據(jù)是如何更新的以及如何獲取設(shè)備剪貼板數(shù)據(jù)回傳給AS的了.

問(wèn)題3.1 AS發(fā)送的剪貼板數(shù)據(jù)是如何更新的

在問(wèn)題2.3的最后,我們知道代理中的Controller會(huì)在啟動(dòng)時(shí)運(yùn)行run方法

void Controller::Run() {Log::D("Controller::Run");Initialize();try {//無(wú)限循環(huán)中接收和處理控制消息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 {//從輸入流中讀取一個(gè)整數(shù)message_type = input_stream_.ReadInt32();} catch (IoTimeout& e) {continue;}SetReceiveTimeoutMillis(0, socket_fd_);  // Remove receive timeout for reading the rest of the message.//根據(jù)消息類(lèi)型,從輸入流中反序列化出一個(gè)控制消息。unique_ptr<ControlMessage> message = ControlMessage::Deserialize(message_type, input_stream_);//調(diào)用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()) {//處理各種類(lèi)型消息...case StartClipboardSyncMessage::TYPE:StartClipboardSync((const StartClipboardSyncMessage&) message);break;...

代理中的Controller會(huì)啟動(dòng)一個(gè)無(wú)限循環(huán)不斷處理各類(lèi)消息,完成消息解析后會(huì)調(diào)用ProcessMessage進(jìn)行處理,這里AS發(fā)送的type類(lèi)型是StartClipboardSyncMessage,最終會(huì)調(diào)用到StartClipboardSync方法

void Controller::StartClipboardSync(const StartClipboardSyncMessage& message) {ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);//判斷當(dāng)前剪貼板數(shù)據(jù)和last_clipboard_text_是否一致if (message.text() != last_clipboard_text_) {last_clipboard_text_ = message.text();//調(diào)用clipboard_manager的SetText方法clipboard_manager->SetText(last_clipboard_text_);}bool was_stopped = max_synced_clipboard_length_ == 0;//更新文本最大長(zhǎng)度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());//調(diào)用到JAVA層ClipboardAdapter的setText方法clipboard_adapter_class_.CallStaticVoidMethod(jni_, set_text_method_, jtext.ref(), jtext.ref());
}

這里的流程比較簡(jiǎn)單,處理收到相關(guān)參數(shù)數(shù)據(jù)后最終會(huì)通過(guò)JNI回調(diào)到screen-sharing-agent.jar中ClipboardAdapter的setText方法

static {//獲取剪貼板服務(wù)的接口clipboard = ServiceManager.getServiceAsInterface("clipboard", "android/content/IClipboard", true);try {if (clipboard != null) {//反射找到剪貼板服務(wù)的一些方法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及以上版本中創(chuàng)建一個(gè)PersistableBundle對(duì)象,用于禁止剪貼板更改的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);}}

可以看見(jiàn)在ClipboardAdapter的初始化時(shí)會(huì)通過(guò)反射的方式獲取剪貼板相關(guān)的調(diào)用方法,最終在setText時(shí)會(huì)調(diào)用對(duì)于的剪貼板設(shè)置方法

總結(jié):代理的Controller會(huì)在啟動(dòng)時(shí)會(huì)通過(guò)run方法啟動(dòng)一個(gè)無(wú)限循環(huán)不斷處理各類(lèi)消息,當(dāng)收到AS側(cè)發(fā)送的剪貼板同步的消息時(shí)最終會(huì)通過(guò)JNI調(diào)用到代理中ClipboardAdapter的setText方法最終通過(guò)反射調(diào)用剪貼板服務(wù).

問(wèn)題3.2 如何獲取設(shè)備剪貼板數(shù)據(jù)回傳給AS

在問(wèn)題3.1中收到AS剪貼板消息時(shí)Controller::StartClipboardSync會(huì)調(diào)用 clipboard_manager->AddClipboardListener方法

void Controller::StartClipboardSync(const StartClipboardSyncMessage& message) {ClipboardManager* clipboard_manager = ClipboardManager::GetInstance(jni_);...//通過(guò)max_synced_clipboard_length_大小判斷之前是否停止了剪貼板,max_synced_clipboard_length_默認(rèn)為0bool was_stopped = max_synced_clipboard_length_ == 0;//更新同步文本最大長(zhǎng)度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();//創(chuàng)建一個(gè)新的剪貼板監(jiān)聽(tīng)器列表,這個(gè)新列表是當(dāng)前列表的副本,并將新的監(jiān)聽(tīng)器添加到新列表中auto new_listeners = new vector<ClipboardListener*>(*old_listeners);new_listeners->push_back(listener);//使用compare_exchange_strong方法嘗試更新剪貼板監(jiān)聽(tīng)器列表,沒(méi)有被其他線程修改則為trueif (clipboard_listeners_.compare_exchange_strong(old_listeners, new_listeners)) {if (old_listeners->empty()) {//那么檢查舊的監(jiān)聽(tīng)器列表為空,那么調(diào)用ClipboardAdapter的enablePrimaryClipChangedListenerclipboard_adapter_class_.CallStaticVoidMethod(jni_, enable_primary_clip_changed_listener_method_);}delete old_listeners;return;}//compare_exchange_strong方法失敗,那么刪除新的監(jiān)聽(tīng)器列表delete new_listeners;}
}

在clipboard_manager的AddClipboardListener方法中通過(guò)無(wú)鎖編程的方式通過(guò)compare_exchange_strong線程安全的添加剪貼板監(jiān)聽(tīng)器,并在監(jiān)聽(tīng)器列表為空時(shí)通過(guò)JNI調(diào)用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();
}

最終通過(guò)在問(wèn)題3.1中提到的反射方式,調(diào)用剪貼板服務(wù)中的addPrimaryClipChangedListener方法,這樣當(dāng)剪貼板數(shù)據(jù)變化時(shí)最終會(huì)調(diào)用到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;
}

經(jīng)過(guò)層層傳遞最終會(huì)調(diào)用到Controller的OnPrimaryClipChanged方法中,這里的邏輯很簡(jiǎn)單指設(shè)置了clipboard_changed_為true.此時(shí)在之前的問(wèn)題3.1 中提到的Controller::Run()方法,有一個(gè)無(wú)限循環(huán)一直在檢測(cè)clipboard_changed_是否為true

void Controller::Run() {Log::D("Controller::Run");Initialize();try {//無(wú)限循環(huán)中接收和處理控制消息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__);//檢測(cè)剪貼板文本是否為空,或者與last_clipboard_text_相同if (text.empty() || text == last_clipboard_text_) {return;}Log::V("%s:%d", __FILE__, __LINE__);//檢查剪貼板文本的長(zhǎng)度是否超過(guò)了允許的最大長(zhǎng)度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;//創(chuàng)建一個(gè)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__);
}

當(dāng)檢測(cè)到clipboard_changed_為true時(shí)會(huì)調(diào)用Controller::ProcessClipboardChange方法,經(jīng)過(guò)檢測(cè)后最終通過(guò)socket回傳到AS側(cè)

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側(cè)在收到socket回傳消息后最終將其傳遞給copyPasteManager完整PC端的剪貼板同步

總結(jié):在代理首次收到AS側(cè)發(fā)送的剪貼板數(shù)據(jù)后會(huì)通過(guò)反射方法啟動(dòng)剪貼板變化的監(jiān)聽(tīng),當(dāng)發(fā)現(xiàn)剪貼板變更時(shí),會(huì)獲取當(dāng)前剪貼板數(shù)據(jù)通過(guò)socket回傳給AS端,最終AS端通過(guò)copyPasteManager完成剪貼板數(shù)據(jù)的同步

總結(jié)

至此我們已經(jīng)完整分析了Android Studio 是如何實(shí)現(xiàn)和我們的手機(jī)共享剪貼板的,其中涉及到ADB命令、代理、反射調(diào)用、socket連接等等技術(shù),雖然整體原理比較簡(jiǎn)單,但是各種細(xì)節(jié)確實(shí)不少,其中有不少技術(shù)因?yàn)楸救四芰τ邢逕o(wú)法全面能力分析,如有遺漏錯(cuò)誤歡迎斧正.

流程圖

在這里插入圖片描述

參考資料

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://aloenet.com.cn/news/30658.html

相關(guān)文章:

  • 可以用足球做的游戲視頻網(wǎng)站好的競(jìng)價(jià)賬戶托管外包
  • 常規(guī)做網(wǎng)站要在工信部認(rèn)證嗎網(wǎng)站建立
  • 適合個(gè)人做的網(wǎng)站有哪些東西嗎百度權(quán)重批量查詢
  • 英語(yǔ)機(jī)構(gòu)網(wǎng)站建設(shè)方案足球進(jìn)球排行榜
  • 做網(wǎng)站的困難windows優(yōu)化大師會(huì)員兌換碼
  • 做網(wǎng)站學(xué)什么專業(yè)防惡意點(diǎn)擊軟件
  • 南寧網(wǎng)站建設(shè)設(shè)計(jì)制作今日最新抗疫數(shù)據(jù)
  • 做代購(gòu)有哪些網(wǎng)站有哪些seo優(yōu)化培訓(xùn)課程
  • 做聊天網(wǎng)站的視頻教程怎么進(jìn)行網(wǎng)站關(guān)鍵詞優(yōu)化
  • wordpress主題如何修改seo優(yōu)化設(shè)計(jì)
  • 做網(wǎng)站需要注意的點(diǎn)抖音seo供應(yīng)商
  • 山西成寧做的網(wǎng)站運(yùn)營(yíng)推廣渠道有哪些
  • wordpress下載站源碼營(yíng)銷(xiāo)推廣公司案例
  • 安徽建筑大學(xué)學(xué)工在線網(wǎng)站蘇州網(wǎng)站制作推廣
  • 廣西代理網(wǎng)站建設(shè)公司百度網(wǎng)址提交入口
  • 做網(wǎng)站建設(shè)站長(zhǎng)工具櫻花
  • 甘肅網(wǎng)站建設(shè)制作商競(jìng)價(jià)賬戶托管公司
  • 網(wǎng)站開(kāi)發(fā)集成環(huán)境seo優(yōu)化工作怎么樣
  • 網(wǎng)站建設(shè)招標(biāo)評(píng)分標(biāo)準(zhǔn)福州網(wǎng)站制作推廣
  • wordpress的數(shù)據(jù)庫(kù)在那里關(guān)鍵詞優(yōu)化建議
  • dw怎么做網(wǎng)站輪播圖企業(yè)宣傳方式有哪些
  • 網(wǎng)站備案 太煩個(gè)人免費(fèi)域名注冊(cè)網(wǎng)站
  • 企業(yè)網(wǎng)站建設(shè)與維護(hù)運(yùn)營(yíng)愛(ài)站網(wǎng)能不能挖掘關(guān)鍵詞
  • 徐州哪家做網(wǎng)站好網(wǎng)站的seo方案
  • 海城網(wǎng)站制作建設(shè)高效統(tǒng)籌疫情防控和經(jīng)濟(jì)社會(huì)發(fā)展
  • java做網(wǎng)站步驟網(wǎng)推
  • 做兼職的網(wǎng)站企業(yè)網(wǎng)站seo優(yōu)化公司
  • 整站策劃營(yíng)銷(xiāo)型網(wǎng)站建設(shè)網(wǎng)站優(yōu)化b2b網(wǎng)站有哪些平臺(tái)
  • dede自適應(yīng)網(wǎng)站注意事項(xiàng)營(yíng)銷(xiāo)網(wǎng)站方案設(shè)計(jì)
  • 做電商必須知道的網(wǎng)站短視頻關(guān)鍵詞優(yōu)化