Android 源码 输入系统之 InputChannel 通信通道建立

2024-05-01 21:08

本文主要是介绍Android 源码 输入系统之 InputChannel 通信通道建立,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一节完成了焦点窗口关联,现在可以分析如何将输入事件继续从 InputDispatcher 继续分发。InputChannel 的 sendMessage 将消息发送出去。实际是调用 socket 的 send 接口来发送消息的。具体一点其实使用的是 socketpair。所以我们先来学习 Linux 如何使用 socketpair,然后“破解” InputChannel 通信,最后再去分析输入事件窗口分发。

一、socketpair 使用

用于创建一对无名的、相互连接的套接字。

#include <sys/types.h> 
#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sv[2]);

参数

domain: 协议家族

type: 套接字类型

protocol: 协议类型

sv: 返回套接字对

返回值

成功返回 0,失败返回 -1

UNIX 域套接字用于在同一台计算机上运行的进程之间的通信。虽然因特网域套接字可用于 同一目的,但 UNIX 域套接字的效率更高。UNIX 域套接字仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不要产生顺序号,无需发送确认报文。

UNIX 域套接字提供流和数据报两种接口。UNIX 域数据报服务是可靠的,既不会丢失报文也不会传递出错。UNIX 域套接字就像是套接字和管道的混合。可以使用它们面向网络的域套接字接口或者使用 socketpair 函数來创建一对无命名的、相互连接的 UNIX 域套接字。

虽然接口足够通用,允许 socketpair 用于其他域,但一般来说操作系统仅对 UNIX 域提供支持。

下面是一个例程,父进程和子进程相互发送消息。父进程调用 sleep 休息 1 秒是为了子进程有机会执行。

#include <sys/types.h>
#include <sys/socket.h>#include <stdlib.h>
#include <stdio.h>int main () {int fd[2];int r = socketpair(AF_UNIX, SOCK_STREAM, 0, fd);if (r < 0) {perror("socketpair()");exit(1);}if (fork()) {/* Parent process: echo client */int val = 0;close(fd[1]);while (1) {sleep(1);++val;printf("Parent process Sending data: %d\n", val);write(fd[0], &val, sizeof(val));read(fd[0], &val, sizeof(val));printf("Parent process Data received: %d\n", val);}} else {/* Child process: echo server */int val;close(fd[0]);while (1) {read(fd[1], &val, sizeof(val));printf("Child process Data received: %d\n", val);++val;printf("Child process Sending data: %d\n", val);write(fd[1], &val, sizeof(val));}}
}

运行结果

Parent process Sending data: 1
Child process Data received: 1
Child process Sending data: 2
Parent process Data received: 2
Parent process Sending data: 3
Child process Data received: 3
Child process Sending data: 4
Parent process Data received: 4
Parent process Sending data: 5
Child process Data received: 5
Child process Sending data: 6
Parent process Data received: 6
Parent process Sending data: 7
Child process Data received: 7
Child process Sending data: 8
Parent process Data received: 8
Parent process Sending data: 9
Child process Data received: 9
Child process Sending data: 10
Parent process Data received: 10
Parent process Sending data: 11
Child process Data received: 11
Child process Sending data: 12
Parent process Data received: 12
Parent process Sending data: 13
Child process Data received: 13
Child process Sending data: 14
Parent process Data received: 14
Parent process Sending data: 15
Child process Data received: 15
Child process Sending data: 16
Parent process Data received: 16
......

二、InputChannel 通信

回顾《Android 源码 输入系统之窗口关联》一节,InputChannel 通信得从 ViewRootImpl 类 setView 继续分析。

frameworks/base/core/java/android/view/ViewRootImpl.java

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {......public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {mView = view;......if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {// 创建客户端 InputChannel 对象mInputChannel = new InputChannel();}try {......// Binder 机制调用 Session 对象 addToDisplay 方法,注意入参包括 InputChannel 对象res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);} catch (RemoteException e) {......} finally {......}......if (mInputChannel != null) {if (mInputQueueCallback != null) {// 创建 InputQueue 对象mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}// 接收输入事件,创建 WindowInputEventReceiver 对象mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}......}}}......
}

输入通道(InputChannel)指定用于将输入事件发送到另一个进程中的窗口的文件描述符。它是可打包的,因此可以将其发送到接收事件的进程。每次只应该有一个线程从 InputChannel 中读取数据。

frameworks/base/core/java/android/view/InputChannel.java

public final class InputChannel implements Parcelable {......// 创建一个未初始化的输入通道。// 它可以通过从一个 Parcel 读取数据或将另一个输入通道的状态传输到这个输入通道来初始化。public InputChannel() {}......
}

Session 类 addToDisplay 方法实际工作由 WindowManagerService 类 addWindow 方法完成。

frameworks/base/services/core/java/com/android/server/wm/Session.java

final class Session extends IWindowSession.Stubimplements IBinder.DeathRecipient {final WindowManagerService mService;......@Overridepublic int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,Rect outOutsets, InputChannel outInputChannel) {return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,outContentInsets, outStableInsets, outOutsets, outInputChannel);}......
}
  1. 创建 InputChannel 对
  2. 将服务端 InputChannel 赋给服务端的 WindowState
  3. 将客户端 InputChannel 传递到 outInputChannel, 最终返回客户端应用进程
  4. 将服务端 InputChannel 注册到 InputDispatcher

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

public class WindowManagerService extends IWindowManager.Stubimplements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {......final InputMonitor mInputMonitor = new InputMonitor(this);......public int addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, int viewVisibility, int displayId,Rect outContentInsets, Rect outStableInsets, Rect outOutsets,InputChannel outInputChannel) {......synchronized(mWindowMap) {......// WindowState 代表窗口管理器中的窗口。WindowState win = new WindowState(this, session, client, token,attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);......if (outInputChannel != null && (attrs.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {String name = win.makeInputChannelName();// 创建 InputChannel 对InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);// 将服务端 InputChannel 赋给服务端的 WindowStatewin.setInputChannel(inputChannels[0]);// 将客户端 InputChannel 传递到 outInputChannel, 最终返回客户端应用进程inputChannels[1].transferTo(outInputChannel);// 将服务端 InputChannel 注册到 InputDispatcher mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);}......}......}......
}

创建 InputChannel 对,实际是使用 socketpair 来完成的。openInputChannelPair 简单做了 name 判空后,实际工作由 nativeOpenInputChannelPair 方法完成。

frameworks/base/core/java/android/view/InputChannel.java

public final class InputChannel implements Parcelable {......private static native InputChannel[] nativeOpenInputChannelPair(String name);......public static InputChannel[] openInputChannelPair(String name) {if (name == null) {throw new IllegalArgumentException("name must not be null");}if (DEBUG) {Slog.d(TAG, "Opening input channel pair '" + name + "'");}return nativeOpenInputChannelPair(name);}......
}

不难看出 nativeOpenInputChannelPair 只是一个 jni 接口,实际工作由 jni native 层对应函数(android_view_InputChannel_nativeOpenInputChannelPair)完成。

  1. name 转化(从 java String 对象转化为 C++ String8)
  2. 调用 Native InputChannel 对象 openInputChannelPair 方法
  3. 构建 NativeInputChannel 对象,并将 NativeInputChannel 对象转化为 Java InputChannel 对象返回

注意 Native InputChannel 对象和 NativeInputChannel 对象,前者为 C++ 类 InputChannel,后者为 C++ 类 NativeInputChannel。

frameworks/base/core/jni/android_view_InputChannel.cpp

static jobjectArray android_view_InputChannel_nativeOpenInputChannelPair(JNIEnv* env,jclass clazz, jstring nameObj) {const char* nameChars = env->GetStringUTFChars(nameObj, NULL);String8 name(nameChars);env->ReleaseStringUTFChars(nameObj, nameChars);sp<InputChannel> serverChannel;sp<InputChannel> clientChannel;status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);if (result) {String8 message;message.appendFormat("Could not open input channel pair.  status=%d", result);jniThrowRuntimeException(env, message.string());return NULL;}jobjectArray channelPair = env->NewObjectArray(2, gInputChannelClassInfo.clazz, NULL);if (env->ExceptionCheck()) {return NULL;}jobject serverChannelObj = android_view_InputChannel_createInputChannel(env,new NativeInputChannel(serverChannel));if (env->ExceptionCheck()) {return NULL;}jobject clientChannelObj = android_view_InputChannel_createInputChannel(env,new NativeInputChannel(clientChannel));if (env->ExceptionCheck()) {return NULL;}env->SetObjectArrayElement(channelPair, 0, serverChannelObj);env->SetObjectArrayElement(channelPair, 1, clientChannelObj);return channelPair;
}
  1. 调用 socketpair 创建一对无名的、相互连接的套接字
  2. setsockopt() 函数,用于任意类型、任意状态套接口的设置选项值

SO_RCVBUF int 为接收确定缓冲区大小。
SO_RCVBUF int 为接收确定缓冲区大小。

  1. 分别创建服务端和客户端 Native InputChannel 对象

frameworks/native/libs/input/InputTransport.cpp

status_t InputChannel::openInputChannelPair(const String8& name,sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {int sockets[2];if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {status_t result = -errno;ALOGE("channel '%s' ~ Could not create socket pair.  errno=%d",name.string(), errno);outServerChannel.clear();outClientChannel.clear();return result;}int bufferSize = SOCKET_BUFFER_SIZE;setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));String8 serverChannelName = name;serverChannelName.append(" (server)");outServerChannel = new InputChannel(serverChannelName, sockets[0]);String8 clientChannelName = name;clientChannelName.append(" (client)");outClientChannel = new InputChannel(clientChannelName, sockets[1]);return OK;
}

NativeInputChannel 对象将传递来的 InputChannel 指针保存在自己的成员变量 mInputChannel 中。

frameworks/base/core/jni/android_view_InputChannel.cpp

NativeInputChannel::NativeInputChannel(const sp<InputChannel>& inputChannel) :mInputChannel(inputChannel), mDisposeCallback(NULL) {
}

再来分析 android_view_InputChannel_createInputChannel 函数。

  1. 调用 jni 方法 NewObject 创建一个 Java InputChannel 对象
  2. 调用 android_view_InputChannel_setNativeInputChannel 将 nativeInputChannel 对象指针强转为 jlong,然后将其设置到 Java InputChannel 类 mPtr 成员上。

frameworks/base/core/jni/android_view_InputChannel.cpp

static jobject android_view_InputChannel_createInputChannel(JNIEnv* env,NativeInputChannel* nativeInputChannel) {jobject inputChannelObj = env->NewObject(gInputChannelClassInfo.clazz,gInputChannelClassInfo.ctor);if (inputChannelObj) {android_view_InputChannel_setNativeInputChannel(env, inputChannelObj, nativeInputChannel);}return inputChannelObj;
}

InputChannel 类 transferTo 函数,将 InputChannel 内部状态的所有权转移到另一个实例,并使该实例无效。这用于在 binder 调用中将输入通道作为 out 参数传递。

frameworks/base/core/java/android/view/InputChannel.java

public final class InputChannel implements Parcelable {......public void transferTo(InputChannel outParameter) {if (outParameter == null) {throw new IllegalArgumentException("outParameter must not be null");}nativeTransferTo(outParameter);}......
}

otherObj 即对应的入参,调用 android_view_InputChannel_getNativeInputChannel 方法可将 Java InputChannel 对象的 mPtr 成员变量强转为 NativeInputChannel 对象,由于传递过来的 Java InputChannel 对象是个“空壳”,所以强转以后一定为空,否则抛出异常。

接下来调用 android_view_InputChannel_getNativeInputChannel 将 obj 转化为 NativeInputChannel 对象。并将其(实际是指针)设置到 otherObj mPtr 成员变量上。

最后将 obj mPtr 成员变量设置为 0,即表示 NativeInputChannel 对象为 NULL。

frameworks/base/core/jni/android_view_InputChannel.cpp

static void android_view_InputChannel_nativeTransferTo(JNIEnv* env, jobject obj,jobject otherObj) {if (android_view_InputChannel_getNativeInputChannel(env, otherObj) != NULL) {jniThrowException(env, "java/lang/IllegalStateException","Other object already has a native input channel.");return;}NativeInputChannel* nativeInputChannel =android_view_InputChannel_getNativeInputChannel(env, obj);android_view_InputChannel_setNativeInputChannel(env, otherObj, nativeInputChannel);android_view_InputChannel_setNativeInputChannel(env, obj, NULL);
}

下面是 android_view_InputChannel_getNativeInputChannel 和 android_view_InputChannel_setNativeInputChannel 函数,主要调用 jni 方法 GetLongField 和 SetLongField,实现 Native 层操作 java 对象成员变量。

frameworks/base/core/jni/android_view_InputChannel.cpp

static NativeInputChannel* android_view_InputChannel_getNativeInputChannel(JNIEnv* env,jobject inputChannelObj) {jlong longPtr = env->GetLongField(inputChannelObj, gInputChannelClassInfo.mPtr);return reinterpret_cast<NativeInputChannel*>(longPtr);
}static void android_view_InputChannel_setNativeInputChannel(JNIEnv* env, jobject inputChannelObj,NativeInputChannel* nativeInputChannel) {env->SetLongField(inputChannelObj, gInputChannelClassInfo.mPtr,reinterpret_cast<jlong>(nativeInputChannel));
}

回到 WindowManagerService addWindow 方法,继续分析将服务端 InputChannel 注册到 InputDispatcher 的过程。registerInputChannel 函数实际调用了 nativeRegisterInputChannel 完成注册。

frameworks/base/services/core/java/com/android/server/input/InputManagerService.java

public class InputManagerService extends IInputManager.Stubimplements Watchdog.Monitor {......private static native void nativeRegisterInputChannel(long ptr, InputChannel inputChannel,InputWindowHandle inputWindowHandle, boolean monitor);......public void registerInputChannel(InputChannel inputChannel,InputWindowHandle inputWindowHandle) {if (inputChannel == null) {throw new IllegalArgumentException("inputChannel must not be null.");}nativeRegisterInputChannel(mPtr, inputChannel, inputWindowHandle, false);}......
}
  1. 将 ptr 强转为 NativeInputManager 对象
  2. 将 inputChannelObj jobject 转化为 Native InputChannel 对象
  3. 将 inputWindowHandleObj jobject 转化为 Native InputWindowHandle 对象
  4. 调用 NativeInputManager 类 registerInputChannel 方法完成实际注册工作

frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp

static void nativeRegisterInputChannel(JNIEnv* env, jclass /* clazz */,jlong ptr, jobject inputChannelObj, jobject inputWindowHandleObj, jboolean monitor) {NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,inputChannelObj);if (inputChannel == NULL) {throwInputChannelNotInitialized(env);return;}sp<InputWindowHandle> inputWindowHandle =android_server_InputWindowHandle_getHandle(env, inputWindowHandleObj);status_t status = im->registerInputChannel(env, inputChannel, inputWindowHandle, monitor);if (status) {String8 message;message.appendFormat("Failed to register input channel.  status=%d", status);jniThrowRuntimeException(env, message.string());return;}......
}

NativeInputManager 类 registerInputChannel 方法最终调用 InputDispatcher 类的 registerInputChannel 完成真正的注册。

frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp

status_t NativeInputManager::registerInputChannel(JNIEnv* /* env */,const sp<InputChannel>& inputChannel,const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {return mInputManager->getDispatcher()->registerInputChannel(inputChannel, inputWindowHandle, monitor);
}
  1. 检查 InputChannel 是否已经注册过
  2. 创建 Connection 对象,他表示客户端和服务端的一个输入数据通道
  3. 以 fd 为 key 将 Connection 添加到容器(mConnectionsByFd)中
  4. 将 fd 添加到 Looper 监听列表中。一旦对端的 Socket 写入数据,Looper 就会被唤醒,接着就会调用 handleReceiveCallback

frameworks/native/services/inputflinger/InputDispatcher.cpp

status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {......{ // acquire lockAutoMutex _l(mLock);// 进入此 case 表示 inputChannel 已经注册过了if (getConnectionIndexLocked(inputChannel) >= 0) {ALOGW("Attempted to register already registered input channel '%s'",inputChannel->getName().string());return BAD_VALUE;}// Connection 表示客户端和服务端的一个输入数据通道sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);// 获取 socketpair fdint fd = inputChannel->getFd();// 以 fd 为 key 将 Connection 添加到容器中mConnectionsByFd.add(fd, connection);......// 将 fd 添加到 Looper 监听列表中,一旦对端的 socket 写入数据,Looper 就会被唤醒// 接着就会调用 handleReceiveCallbackmLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);} // release lock// 唤醒 Looper,因为一些连接(Connection)已经改变mLooper->wake();return OK;
}

代码走到这里,InputDispatcher 就和 InputChannel 建立了关联。现在是时候回到 ViewRootImpl setView 方法中具体分析如何关联客户端 InputChannel。马上来分析创建 WindowInputEventReceiver 对象流程。

在 WindowInputEventReceiver 构造器中调用了 InputEventReceiver 构造器。

frameworks/base/core/java/android/view/ViewRootImpl.java

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {......final class WindowInputEventReceiver extends InputEventReceiver {public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {super(inputChannel, looper);}@Overridepublic void onInputEvent(InputEvent event) {enqueueInputEvent(event, this, 0, true);}@Overridepublic void onBatchedInputEventPending() {if (mUnbufferedInputDispatch) {super.onBatchedInputEventPending();} else {scheduleConsumeBatchedInput();}}@Overridepublic void dispose() {unscheduleConsumeBatchedInput();super.dispose();}}......
}

InputEventReceiver 构造器中首先检查入参 inputChannel 和 looper 是否为 null,接着从 looper 中获取 MessageQueue (mMessageQueue),最后调用 nativeInit 进一步初始化。

frameworks/base/core/java/android/view/InputEventReceiver.java

public abstract class InputEventReceiver {......public InputEventReceiver(InputChannel inputChannel, Looper looper) {if (inputChannel == null) {throw new IllegalArgumentException("inputChannel must not be null");}if (looper == null) {throw new IllegalArgumentException("looper must not be null");}mInputChannel = inputChannel;mMessageQueue = looper.getQueue();mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),inputChannel, mMessageQueue);mCloseGuard.open("dispose");}......
}
  1. 转化 Java InputChannel 对象为 Native InputChannel 对象
  2. 转化 Java MessageQueue 对象为 Native MessageQueue 对象
  3. 构建 NativeInputEventReceiver 对象
  4. 调用 NativeInputEventReceiver 对象 initialize() 方法初始化

frameworks/base/core/jni/android_view_InputEventReceiver.cpp

static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,jobject inputChannelObj, jobject messageQueueObj) {sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,inputChannelObj);if (inputChannel == NULL) {jniThrowRuntimeException(env, "InputChannel is not initialized.");return 0;}sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);if (messageQueue == NULL) {jniThrowRuntimeException(env, "MessageQueue is not initialized.");return 0;}sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,receiverWeak, inputChannel, messageQueue);status_t status = receiver->initialize();if (status) {String8 message;message.appendFormat("Failed to initialize input event receiver.  status=%d", status);jniThrowRuntimeException(env, message.string());return 0;}receiver->incStrong(gInputEventReceiverClassInfo.clazz); // 保留对象的引用return reinterpret_cast<jlong>(receiver.get());
}

NativeInputEventReceiver 构造函数将传递来的入参保存到对应的成员变量中。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp

NativeInputEventReceiver::NativeInputEventReceiver(JNIEnv* env,jobject receiverWeak, const sp<InputChannel>& inputChannel,const sp<MessageQueue>& messageQueue) :mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),mInputConsumer(inputChannel), mMessageQueue(messageQueue),mBatchedInputEventPending(false), mFdEvents(0) {if (kDebugDispatchCycle) {ALOGD("channel '%s' ~ Initializing input event receiver.", getInputChannelName());}
}

initialize() 函数仅仅调用了 setFdEvents() 函数,然后直接返回 OK。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp

status_t NativeInputEventReceiver::initialize() {setFdEvents(ALOOPER_EVENT_INPUT);return OK;
}

由于 mFdEvents 初始化为 0, ALOOPER_EVENT_INPUT = 1 << 0,因此会赋值 mFdEvents = ALOOPER_EVENT_INPUT。然后取出 socket fd,就会向客户端 Looper 添加 fd 描述符,当有数据从服务端写入,就会唤醒 Looper。最终回调 NativeInputEventReceiver 的 handleEvent 方法。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp

void NativeInputEventReceiver::setFdEvents(int events) {if (mFdEvents != events) {mFdEvents = events;int fd = mInputConsumer.getChannel()->getFd();if (events) {mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);} else {mMessageQueue->getLooper()->removeFd(fd);}}
}

老规矩时序图作为最后的总结。
在这里插入图片描述

这篇关于Android 源码 输入系统之 InputChannel 通信通道建立的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/952667

相关文章

如何解决idea的Module:‘:app‘platform‘android-32‘not found.问题

《如何解决idea的Module:‘:app‘platform‘android-32‘notfound.问题》:本文主要介绍如何解决idea的Module:‘:app‘platform‘andr... 目录idea的Module:‘:app‘pwww.chinasem.cnlatform‘android-32

Android实现打开本地pdf文件的两种方式

《Android实现打开本地pdf文件的两种方式》在现代应用中,PDF格式因其跨平台、稳定性好、展示内容一致等特点,在Android平台上,如何高效地打开本地PDF文件,不仅关系到用户体验,也直接影响... 目录一、项目概述二、相关知识2.1 PDF文件基本概述2.2 android 文件访问与存储权限2.

Android Studio 配置国内镜像源的实现步骤

《AndroidStudio配置国内镜像源的实现步骤》本文主要介绍了AndroidStudio配置国内镜像源的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、修改 hosts,解决 SDK 下载失败的问题二、修改 gradle 地址,解决 gradle

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

使用Python实现一键隐藏屏幕并锁定输入

《使用Python实现一键隐藏屏幕并锁定输入》本文主要介绍了使用Python编写一个一键隐藏屏幕并锁定输入的黑科技程序,能够在指定热键触发后立即遮挡屏幕,并禁止一切键盘鼠标输入,这样就再也不用担心自己... 目录1. 概述2. 功能亮点3.代码实现4.使用方法5. 展示效果6. 代码优化与拓展7. 总结1.

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

Android中Dialog的使用详解

《Android中Dialog的使用详解》Dialog(对话框)是Android中常用的UI组件,用于临时显示重要信息或获取用户输入,本文给大家介绍Android中Dialog的使用,感兴趣的朋友一起... 目录android中Dialog的使用详解1. 基本Dialog类型1.1 AlertDialog(