手機(jī)網(wǎng)站開(kāi)發(fā)學(xué)習(xí)免費(fèi)單頁(yè)網(wǎng)站在線制作
背景
近期完成了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è)方法的整理流程還是比較清晰的:
- 從
DeviceMirroringSettings
實(shí)例中獲取剪貼板同步的最大文本長(zhǎng)度,默認(rèn)為5000。 - 檢查是否需要發(fā)送剪貼板內(nèi)容。如果
forceSend
為true
,或者text
非空且與lastClipboardText
不同,那么就需要發(fā)送。 - 如果需要發(fā)送,根據(jù)
text
的長(zhǎng)度和maxSyncedClipboardLength
來(lái)調(diào)整要發(fā)送的文本內(nèi)容。如果text
的長(zhǎng)度小于或等于maxSyncedClipboardLength
,那么就發(fā)送text
。如果forceSend
為true
,那么發(fā)送空字符串。否則,函數(shù)直接返回,不做任何操作。 - 創(chuàng)建一個(gè)
StartClipboardSyncMessage
實(shí)例,這個(gè)實(shí)例包含了maxSyncedClipboardLength
和調(diào)整后的文本內(nèi)容。 - 調(diào)用
deviceController
的sendControlMessage
方法,將StartClipboardSyncMessage
實(shí)例發(fā)送給設(shè)備控制器。 - 將
lastClipboardText
設(shè)置為調(diào)整后的文本內(nèi)容。
這里涉及到兩個(gè)對(duì)象StartClipboardSyncMessage
和deviceController
,其中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.}...}
整體流程如下:
- 在協(xié)程中異步推送代理到設(shè)備.
- 創(chuàng)建一個(gè)異步服務(wù)器套接字通道并綁定到一個(gè)隨機(jī)端口.
- 創(chuàng)建設(shè)備反向代理,它將設(shè)備上的一個(gè)socket轉(zhuǎn)發(fā)到電腦上的一個(gè)TCP端口。
- 啟動(dòng)代理對(duì)象
- 建立代理連接
- 創(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)單了解一下他們的作用
screen-sharing-agent.jar
: 主要負(fù)責(zé)啟動(dòng)libscreen-sharing-agent.so
,處理NDK無(wú)法支持的MediaCodecList、MediaCodecInfo的編碼視頻流以及剪貼板監(jiān)聽(tīng)同步等功能。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方法:
- 將代理對(duì)象通過(guò)adb 命令發(fā)送到設(shè)備中
- 創(chuàng)建一個(gè)socket對(duì)象綁定隨機(jī)端口,通過(guò)adb命令將設(shè)備socket與此端口建立反向代理
- 啟動(dòng)代理DeviceClient后通過(guò)此socket獲取控制連接和視頻連接.
- 將控制連接用于創(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