深入Android S (12.0) 探索Framework之输入系统IMS的构成与启动
文章目录
- 前言
- 一、输入系统的基本组成部分
- 二、输入系统相关源码分析
- 1、IMS 构建
- 1.1、SystemServer # startOtherServices()
- 1.2、InputManagerService
- 1.3、NativeInputManager # nativeInit()
- 1.4、NativeInputManager
- 1.5、InputManager
- 1.6、InputDispatcher
- 1.7、InputReader
- 1.8、EventHub
- 1.9、小结
- 2、IMS 启动
- 2.1、IMS # start()
- 2.2、NativeInputManager # nativeStart()
- 2.3、InputManager # start()
- 2.4、InputDispatcher # start()
- 2.5、InputReader # start()
- 2.6、InputThread
- 3、IMS 系统就绪
- 三、总结
- 1、IMS 启动时序图
- 2、IMS 成员关系图
- 参考
前言
Android 输入系统(Input System) 的工作原理,包括:输入设备的管理、输入事件的加工方式及派发流程。首先输入设备包括:触摸屏,键盘,鼠标和手柄等,其中触摸屏与键盘是 Android 最普遍也是最标准的输入设备。当用户操作输入设备时,Linux 内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过输入系统内部的读取函数将原始事件数据读出,并进行一系列的翻译加工成 Android 输入事件,然后在所有的窗口中寻找合适的事件接收者,并派发给它来消费该输入事件。可见,输入系统在整个输入事件处理过程中起到了承上启下的衔接作用。
一、输入系统的基本组成部分
上图展示了输入事件的处理流程以及输入系统中最基本的参与者,下面简要介绍一下各个参与者:
- Linux 内核:接受输入设备的中断,并将原始输入事件的数据写入设备节点中;
- 设备节点:内核与 InputManagerService 的桥梁,它将原始事件的数据暴露给用户空间,以便 InputManagerService 可以从中读取事件;
- InputManagerService:Android 系统服务,以后简称 IMS,其分为 Java 层和 Native 层两部分。Java 层负责与 WindowManagerService 通信。而 Native 层则是InputReader 和 InputDispatcher 两个输入系统关键组件的运行容器;
- EventHub:直接访问所有的设备节点。并且正如其名字所描述的,它通过一个名为 getEvents( ) 的函数将所有输入系统相关的待处理的底层事件返回给使用者,包括原始输入事件、设备节点的增删等。
- InputReader:IMS 中的关键组件之一,运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。通过其线程循环不断地通过 getEvent( ) 函数从 EventHub 中将事件取出并进行处理。对于设备节点的增删事件,将会更新输入设备列表与配置。对于原始输入事件,InputReader 对其进行翻译、组装、封装为包含更多信息、更具可读性的输入事件,然后交给 InputDispatcher 进行派发;
- InputReaderPolicy:为 InputReader 的事件加工处理提供一些策略配置,例如键盘布局信息等;
- InputDispatcher:IMS 中的另一个关键组件,也运行于一个独立的线程中。InputDispatcher 中保管了来自 WindowManagerService 的所有窗口的信息,其收到来自 InputReader 的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口;
- InputDispatcherPolicy:为 InputDispatcher 的派发过程提供策略控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是 HOME 键被 InputDispatcherPolicy 截取到 PhoneWindowManager 中进行处理,并阻止窗口收到 HOME 键按下的事件;
- WindowManagerService:虽不是输入系统中的成员,但却对 InputDispatcher 的正常工作起到了至关重要的作用。当新建窗口时,WMS 为新窗口和 IMS 之间创建了事件传递所用的通道。另外,WMS 还将所有窗口的信息,包括窗口的可点击区域、焦点窗口等信息,实时地更新到 IMS 的 InputDispatcher 中,使得 InputDispatcher 可以正确地将事件派发到指定的窗口;
- ViewRootImpl:对某些窗口,如壁纸窗口、SurfaceView 的窗口来说,窗口就是输入事件派发的终点。而对其他的如 Activity、对话框等使用了 Android 控件系统的窗口来说,输入事件的终点是控件 View。ViewRootImpl 将窗口所接收的输入事件沿着控件树将事件派发给感兴趣的控件 View;
二、输入系统相关源码分析
我们知道,Zygote 进程创建并启动后,在 fork 出的子进程 SystemServer 的初始化过程中启动 Android 系统所有的 Service 服务,这些系统服务分为三大类:引导服务、核心服务及其他服务,具体的启动流程可参考探索Framework之SystemServer进程的启动详解。
输入系统服务 IMS 是在启动其他服务里面启动的,接下来从源码的角度来继续探索分析。
1、IMS 构建
在 SystemServer 类中找到启动其他服务 startOtherServices() 方法的代码,提取主要的逻辑代码进行分析,源码如下:
1.1、SystemServer # startOtherServices()
xref: /frameworks/base/services/java/com/android/server/SystemServer.java
public final class SystemServer implements Dumpable {......private void startOtherServices(@NonNull TimingsTraceAndSlog t) {final Context context = mSystemContext;WindowManagerService wm = null;......InputManagerService inputManager = null;......try {......// 启动 InputManagerService 服务t.traceBegin("StartInputManagerService"); // 新建 InputManagerService 对象inputManager = new InputManagerService(context);......t.traceBegin("StartWindowManagerService"); // 启动 WindowManagerService 服务mSystemServiceManager.startBootPhase(t, SystemService.PHASE_WAIT_FOR_SENSOR_SERVICE);// 使用新建的 IMS 对象来构建 WMS 对象wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);......// 将 InputManagerService 发布到 ServiceManager 以便调用者可以访问 IMS 提供的接口ServiceManager.addService(Context.INPUT_SERVICE, inputManager,/* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);t.traceBegin("SetWindowManagerService"); // ActivityManagerService 设置 WindowManagerServicemActivityManagerService.setWindowManager(wm);t.traceBegin("StartInputManager"); // 设置向 WMS 发起回调的 callback 对象inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());inputManager.start(); // 启动 InputManagerService,具体见......} catch (Throwable e) {......// 日志输出并抛出异常}......final InputManagerService inputManagerF = inputManager;t.traceBegin("MakeInputManagerServiceReady");try {if (inputManagerF != null) {// 输入系统 IMS 准备就绪inputManagerF.systemRunning();}} catch (Throwable e) {reportWtf("Notifying InputManagerService running", e);}......}......
}
IMS 的启动流程可以分为以下三个步骤:
- 构建 IMS 实例对象,并建立上层与底层的映射关系。
- 启动 IMS,其内部就是启动 native 层输入系统的几个重要参与者(后续会分析)。
- IMS 系统就绪,此时 Java 层会同步一些配置给 native 层输入系统。
首先是 IMS 实例对象的构建,分析查看 IMS 类的源码
1.2、InputManagerService
xref: /frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stubimplements Watchdog.Monitor {......private final Context mContext;private final InputManagerHandler mHandler;......private static native long nativeInit(InputManagerService service,Context context, MessageQueue messageQueue);......public InputManagerService(Context context) {this.mContext = context;// 获取 DisplayThread 的 Looper 创建 IMS 内部的 InputManagerHandler 对象this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());mStaticAssociations = loadStaticInputPortAssociations();mUseDevInputEventForAudioJack =context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack="+ mUseDevInputEventForAudioJack);// 每一个分为 Java 和 Native 两部分的对象在创建时都会有一个 native 函数// 在创建 Java 层对象的同时 native 层也创建一个,注意:使用的是同一个 Looper 对象// mPtr 指向底层创建的 NativeInputManager 对象mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());String doubleTouchGestureEnablePath = context.getResources().getString(R.string.config_doubleTouchGestureEnableFile);mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null :new File(doubleTouchGestureEnablePath);// 新建 IMS 的本地系统服务 LocalService,其继承自 InputManagerInternal 抽象接口,并加入到 LocalServices 中LocalServices.addService(InputManagerInternal.class, new LocalService());}
}
方法中,获取 DisplayThread 的 Looper,新建 InputManagerHandler 对象,然后调用 native 层的 nativeInit() 函数,创建NativeInputManager 对象,最后新建 IMS 的本地系统服务 LocalService,其继承自 InputManagerInternal 抽象接口,并加入到 LocalServices 中。
DisplayThread 在 system_server 进程中是单例的,且只能被 WindowManager、DisplayManager、InputManager 使用。
1.3、NativeInputManager # nativeInit()
xref: /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
static const JNINativeMethod gInputManagerMethods[] = { // JNI 注册的映射关系/* name, signature, funcPtr */{"nativeInit","(Lcom/android/server/input/InputManagerService;Landroid/content/Context;Landroid/os/""MessageQueue;)J",(void*)nativeInit},
};static jlong nativeInit(JNIEnv*env, jclass /* clazz */,jobject serviceObj, jobject contextObj, jobject messageQueueObj) {// 由传入的 Java 层的 MessageQueue 转换获取 native 层的 MessageQueuesp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);if (messageQueue == nullptr) {jniThrowRuntimeException(env, "MessageQueue is not initialized.");return 0;}// 新建 NativeInputManager 对象,此对象将是 native 层组件与 Java 层 IMS 进行通信的桥梁NativeInputManager * im = new NativeInputManager(contextObj, serviceObj,messageQueue -> getLooper());im -> incStrong(0);// 返回指向 NativeInputManager 对象的指针给 Java 层的 IMS,IMS 将其保存在 mPtr 成员变量中return reinterpret_cast < jlong > (im);
}
通过 JNI 注册的映射关系,找到 native 层的 nativeInit() 函数,首先由传入的 Java 层的 MessageQueue 转换获取 native 层的 NativeMessageQueue 对象,然后新建 NativeInputManager 对象。
1.4、NativeInputManager
xref: /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager : public virtual RefBase,public virtual InputReaderPolicyInterface,public virtual InputDispatcherPolicyInterface,public virtual PointerControllerPolicyInterface {public:NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper);inline sp<InputManagerInterface> getInputManager() const { return mInputManager; }private:sp<InputManagerInterface> mInputManager; jobject mServiceObj; // IMS 对象sp<Looper> mLooper; // Looper 对象Mutex mLock;struct Locked {......例如,mLocked.showTouches 是// 如果为 True,则启用指针手势bool pointerGesturesEnabled;// 由开发者选项中 "Show taps" 决定的,其功能是在屏幕上显示一个触摸点bool showTouches;......} mLocked GUARDED_BY(mLock);std::atomic<bool> mInteractive;......static inline JNIEnv* jniEnv() {return AndroidRuntime::getJNIEnv();}
};NativeInputManager::NativeInputManager(jobject contextObj,jobject serviceObj, const sp<Looper>&looper) :mLooper(looper),mInteractive(true) {JNIEnv * env = jniEnv();// 保存 Java 层的 InputManagerService 对象mServiceObj = env -> NewGlobalRef(serviceObj);{ // mLocked 的类型是 struct Locked,这里初始化了一些参数,这些参数会被 Java 层改变AutoMutex _l (mLock);mLocked.systemUiLightsOut = false;mLocked.pointerSpeed = 0;mLocked.pointerGesturesEnabled = true;mLocked.showTouches = false;mLocked.pointerCapture = false;mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT;}mInteractive = true;// 创建了 native 层的 InputManager,它才是底层输入系统的服务InputManager * im = new InputManager(this, this);mInputManager = im;// 将 InputManager 注册到 ServiceManager 中defaultServiceManager()->addService(String16("inputflinger"), im);
}
- 在 NativeInputManager 的构造函数中,创建一个全局引用,并通过 mServiceObj 指向 Java 层的 IMS 对象,便于后续可以通过 mServiceObj 调用 Java 层 IMS 对象的方法。
- 初始化参数,这里要注意一个结构体变量 mLocked,它的一些参数都是由 Java 层控制的。
- 然后将自己作为参数来新建 InputManager 对象,并将 InputManager 注册到 ServiceManager 中,InputManager 才是 native 层输入系统的服务。
注意:由 NativeInputManager 类的声明可以看到,其实现了 InputReaderPolicyInterface 与 InputDispatcherPolicyInterface 两个接口。
1.5、InputManager
xref: /frameworks/native/services/inputflinger/InputManager.h
class InputManager : public InputManagerInterface, public BnInputFlinger {
protected:~InputManager() override;public:InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,const sp<InputDispatcherPolicyInterface>& dispatcherPolicy);......
private:sp<InputReaderInterface> mReader;sp<InputClassifierInterface> mClassifier;sp<InputDispatcherInterface> mDispatcher;
};
xref: /frameworks/native/services/inputflinger/InputManager.cpp
InputManager::InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {// 创建 InputDispatcher 对象,使用 InputDispatcherPolicyInterface 接口,用于对事件进行分发mDispatcher = createInputDispatcher(dispatcherPolicy);// 创建 InputClassifier 对象,使用 InputListenerInterface,用于对事件分类mClassifier = new InputClassifier(mDispatcher);// 创建 InputReader 对象,使用 InputReaderPolicyInterface 和 InputListenerInterface// 其通过 EventHub 监听"/dev/input"事件,获取事件,然后把事件加工后,发送给 InputClassfiermReader = createInputReader(readerPolicy, mClassifier);
}
在 InputManager 内部创建了三个子模块:InputReader、InputClassifier、InputDispatcher,其作用如下:
- InputReader:负责从 EventHub 中获取事件,然后把事件加工后,发送给 InputClassfier。
- InputClassifer:负责把事件发送给 InputDispatcher,但是它会对触摸事件进行一个分类工作。
- InputDispatcher:对进行事件分发。
此外,在上一小节的分析中,我们知道在构建 InputManager 实例对象时使用了两个 this 参数,而 InputManager 构造函数需要的两个接口参数正是由 NativeInputManager 实现的,而具体使用这两个接口的不是 InputManager 自身,而是它内部的子模块 InputDispatcher 和 InputReader。
InputDispatcher 和 InputReader 在构建时都传递了 NativeInputManager 对象参数,并赋值到各自的 mPolicy 变量,后续可直接通过 mPolicy 调用 Java 层 IMS 对象方法,因此 InputManager 向 Java 层通信的能力是由子模块 InputDispatcher 和 InputReader 实现的。
接下来首先来看看 InputDispatcher 是如何通过 createInputDispatcher() 函数创建的,详见接下来两节的源码分析。
1.6、InputDispatcher
xref: /frameworks/native/services/inputflinger/dispatcher/InputDispatcherFactory.cpp
sp<InputDispatcherInterface> createInputDispatcher(const sp<InputDispatcherPolicyInterface>& policy) {return new android::inputdispatcher::InputDispatcher(policy);
}
方法很简单,内部直接新建 InputDispatcher 对象,再继续查看 InputDispatcher 的构造函数:
xref: /frameworks/native/services/inputflinger/dispatcher/InputDispatcher.h
class InputDispatcher : public android::InputDispatcherInterface {
protected:~InputDispatcher() override;public:explicit InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy);
......
private:std::unique_ptr<InputThread> mThread;sp<InputDispatcherPolicyInterface> mPolicy;sp<Looper> mLooper;sp<InputReporterInterface> mReporter;
};
xref: /frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
InputDispatcher::InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy): mPolicy(policy),mPendingEvent(nullptr),mLastDropReason(DropReason::NOT_DROPPED),mIdGenerator(IdGenerator::Source::INPUT_DISPATCHER),mAppSwitchSawKeyDown(false),mAppSwitchDueTime(LONG_LONG_MAX),mNextUnblockedEvent(nullptr),mDispatchEnabled(false),mDispatchFrozen(false),mInputFilterEnabled(false),// mInTouchMode will be initialized by the WindowManager to the default device config.// To avoid leaking stack in case that call never comes, and for tests,// initialize it here anyways.mInTouchMode(true),mMaximumObscuringOpacityForTouch(1.0f),mFocusedDisplayId(ADISPLAY_ID_DEFAULT),mFocusedWindowRequestedPointerCapture(false),mWindowTokenWithPointerCapture(nullptr),mLatencyAggregator(),mLatencyTracker(&mLatencyAggregator),mCompatService(getCompatService()) {mLooper = new Looper(false); // 新建自己的 Looper 对象mReporter = createInputReporter(); // 新建 InputReporter 对象mKeyRepeatState.lastKeyEntry = nullptr;policy->getDispatcherConfiguration(&mConfig);
}
在调用 InputDispatcher 的构造函数构建实例对象的同时将入参 policy 赋值给 mPolicy 进行保存 (这里入参 policy 即是 NativeInputManager 对象)。其次新建自己的 Looper 对象,然后使用类似创建 InputDispatcher 的 createInputReporter() 函数新建 InputReporter 对象,代码比较简单,不再深入追踪,感兴趣的可自行查看。
1.7、InputReader
接着来看看 InputReader 是如何通过 createInputReader() 函数创建的,一起跟着源码来学习。
xref: /frameworks/native/services/inputflinger/reader/InputReaderFactory.cpp
sp<InputReaderInterface> createInputReader(const sp<InputReaderPolicyInterface>& policy,const sp<InputListenerInterface>& listener) {// 创建 EventHub 对象传入到 InputReader 的构造函数中来新建 InputReader 对象return new InputReader(std::make_unique<EventHub>(), policy, listener);
}
该方法里面,在新建 InputReader 对象时,结合 InputReader 类的构造函数可知,第一个参数是 EventHub 的实例对象,那么 EventHub 对象是怎么创建的呢?
这里需要知道一些 C++ 有关的知识,std::make_unique 的语法如下:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args);
std::make_unique:是 C++11 标准引入的一个模版函数,用于动态分配指定类型的内存,并返回一个指向分配内存的唯一指针 (即 std::unique_ptr)。语法中,T 是指定的类型,Args 是可变长模板参数包,用于传递给指定类型的构造函数的参数。在调用 std::make_unique 时,通过 Args 包传入构造函数的参数会被转发给类型 T 的构造函数,以生成相应的对象实例。该函数返回的指针是一个 std::unique_ptr 类型,表示一个拥有指向动态内存的所有权的对象。
xref: /frameworks/native/services/inputflinger/reader/include/InputReader.h
class InputReader : public InputReaderInterface {
public:InputReader(std::shared_ptr<EventHubInterface> eventHub,const sp<InputReaderPolicyInterface>& policy,const sp<InputListenerInterface>& listener);virtual ~InputReader();
protected:// 在循环过程的每次迭代中,InputReader 读取并处理一条来自 EventHub 的传入消息void loopOnce();private:std::unique_ptr<InputThread> mThread;std::shared_ptr<EventHubInterface> mEventHub;sp<InputReaderPolicyInterface> mPolicy;sp<QueuedInputListener> mQueuedListener;
};
xref: /frameworks/native/services/inputflinger/reader/InputReader.cpp
InputReader::InputReader(std::shared_ptr<EventHubInterface> eventHub,const sp<InputReaderPolicyInterface>& policy,const sp<InputListenerInterface>& listener): mContext(this),mEventHub(eventHub),mPolicy(policy),mGlobalMetaState(0),mLedMetaState(AMETA_NUM_LOCK_ON),mGeneration(1),mNextInputDeviceId(END_RESERVED_ID),mDisableVirtualKeysTimeout(LLONG_MIN),mNextTimeout(LLONG_MAX),mConfigurationChangesToRefresh(0) {mQueuedListener = new QueuedInputListener(listener);{ // acquire lockstd::scoped_lock _l(mLock);refreshConfigurationLocked(0);updateGlobalMetaStateLocked();} // release lock
}
与构建 InputDispatcher 对象类似,在调用 InputReader 的构造函数构建实例对象的同时将入参 policy 赋值给 mPolicy,eventHub 对象赋值给 mEventHub 保存,同时新建 QueuedInputListener 监听对象。
通过前一小节的分析可知,EventHub 实例对象是通过调用 std::make_unique() 函数来创建的,那接下来一起去看看 EventHub 具体都做了些什么?
1.8、EventHub
xref: /frameworks/native/services/inputflinger/reader/include/EventHub.h
class EventHub : public EventHubInterface {
public:EventHub();
private:int32_t mNextDeviceId;BitSet32 mControllerNumbers;std::unordered_map<int32_t, std::unique_ptr<Device>> mDevices;std::vector<std::unique_ptr<Device>> mOpeningDevices;std::vector<std::unique_ptr<Device>> mClosingDevices;int mEpollFd;int mINotifyFd;int mWakeReadPipeFd;int mWakeWritePipeFd;int mInputWd;int mVideoWd;// 一次最多可处理的信号fd的数量static const int EPOLL_MAX_EVENTS = 16;// 挂起的 epoll 事件数组和下一个要处理的事件的索引struct epoll_event mPendingEventItems[EPOLL_MAX_EVENTS];size_t mPendingEventCount;size_t mPendingEventIndex;bool mPendingINotify;
};
xref: /frameworks/native/services/inputflinger/reader/EventHub.cpp
EventHub::EventHub(void): mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),mNextDeviceId(1),mControllerNumbers(),mOpeningDevices(nullptr),mClosingDevices(nullptr),mNeedToSendFinishedDeviceScan(false),mNeedToReopenDevices(false),mNeedToScanDevices(true),mPendingEventCount(0),mPendingEventIndex(0),mPendingINotify(false) {ensureProcessCanBlockSuspend();// 创建 Epoll 对象的描述符,监听设备节点是否有数据可读(有无事件发生)mEpollFd = epoll_create1(EPOLL_CLOEXEC);LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));// 创建 INotify 对象,用于监听设备节点的路径 /dev/input,是否有变化,如有设备增删则对应的设备节点的文件也会增删mINotifyFd = inotify_init();// 添加 watch 监听存储设备节点的路径 DEVICE_PATH 的创建与删除,当有设备节点发生变化时,通过 INotify 对象可以读取事件的详细信息mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);LOG_ALWAYS_FATAL_IF(mInputWd < 0, "Could not register INotify for %s: %s", DEVICE_PATH,strerror(errno));if (isV4lScanningEnabled()) {mVideoWd = inotify_add_watch(mINotifyFd, VIDEO_DEVICE_PATH, IN_DELETE | IN_CREATE);LOG_ALWAYS_FATAL_IF(mVideoWd < 0, "Could not register INotify for %s: %s",VIDEO_DEVICE_PATH, strerror(errno));} else {mVideoWd = -1;ALOGI("Video device scanning disabled");}// 构建 epoll_event 结构体,并为每一个需要监控的描述符填充该结构体,以描述监控事件struct epoll_event eventItem = {};eventItem.events = EPOLLIN | EPOLLWAKEUP; // 事件掩码,指明需要监听的事件类型,可读eventItem.data.fd = mINotifyFd; // 数据字段,设置需要监听的描述符,这里是 mINotifyFd,即监听设备节点的路径// 调用 epoll_ctl() 函数将 INotify 对象注册到 Epoll 中,监听其文件描述符对应的文件夹下是否有设备节点的增删信息// 第一个参数即前面创建的 Epoll 对象的描述符,第二个参数表示具体操作,这里 ADD 表示增加注册事件// 第三个参数表示需要监听的描述符,第四个参数是描述监听事件的详细信息的 epoll_event 结构体int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, & eventItem);LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance. errno=%d", errno);// 创建匿名管道 wakeFds,并将读端交给 Epoll,写端交给 InputReader,用于唤醒 Epoll,避免其阻塞在 epoll_wait()int wakeFds[ 2];result = pipe(wakeFds);LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe. errno=%d", errno);// mWakeReaderFD 和 mWakeWriterFD 对应管道的两端mWakeReadPipeFd = wakeFds[0];mWakeWritePipeFd = wakeFds[1];result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking. errno=%d", errno);result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking. errno=%d", errno);eventItem.data.fd = mWakeReadPipeFd;// epoll_ctl() 函数可重复调用,将多个文件描述符的多种事件监听注册到 Epoll 对象中// 将匿名管道的读取端的描述符也注册到 Epoll 中,用于监听读取端的可读事件,当写入端有数据写入时// 管道的读取端就有数据可读,使得 epoll_wait() 得以返回,从而达到唤醒 InputReader 线程的目的,避免其一直阻塞result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance. errno=%d", errno);
}
EventHub 的构造函数主要工作有:
- 新建并初始化 Epoll、INotify 对象等;
- 调用 inotify_add_watch 函数,监听 “/dev/input” 目录下的设备节点创建与删除操作,然后通过 read() 函数读取事件;
- 将 INotify 添加到 Epoll 中,作为一个监控对象;
- 创建管道,将管道读取端的可读事件添加到 Epoll 中,使 epoll_wait() 函数返回,唤醒 InputReader 线程来处理事件。
至此,IMS 在 Java 层和 native 层的实例对象都已创建完成,并且在这个过程中,输入系统的重要参与者也均创建完成。
1.9、小结
Java 层的 IMS 的主要工作是为 ReaderPolicy 与 DispatcherPolicy 提供实现,以及与 Android 其他系统服务进行协作,其中最主要的协作者是 WMS。
NativeInputManager 位于 IMS 的 JNI 层,负责 Native 层的组件与 Java 层的 IMS 之间的相互通信。同时为 InputReader 及 InputDispatcher 提供了策略请求的接口。策略请求被他转发给 Java 层的 IMS,由 IMS 进行最终的决策定夺。
InputManager 是 InputReader 与 InputDispatcher 的运行容器,在启动 InputReader 与 InputDispatcher 时,分别新建自己的运行线程 InputThreadImpl 并启动运行。
2、IMS 启动
在上一节的 SystemServer # startOtherServices() 方法中,在构建完 IMS 后,IMS 系统中的各个重要参与者仍处于待命状态,需调用 IMS # start() 函数来启动 IMS,继续追踪源码分析:
2.1、IMS # start()
xref: /frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stubimplements Watchdog.Monitor {......private static native void nativeStart(long ptr);......public void start() {Slog.i(TAG, "Starting input manager");// 启动 native 层的 IMSnativeStart(mPtr);Watchdog.getInstance().addMonitor(this);// 监听Settings.System.POINTER_SPEED,这个表示手指的速度registerPointerSpeedSettingObserver();// 监听Settings.System.SHOW_TOUCHES,这个表示是否在屏幕上显示触摸坐标registerShowTouchesSettingObserver();registerAccessibilityLargePointerSettingObserver();registerLongPressTimeoutObserver();registerMaximumObscuringOpacityForTouchSettingObserver();registerBlockUntrustedTouchesModeSettingObserver();// 监听用户切换mContext.registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {updatePointerSpeedFromSettings();updateShowTouchesFromSettings();updateAccessibilityLargePointerFromSettings();updateDeepPressStatusFromSettings("user switched");}}, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);// 从数据库获取值,并传递给 native 层updatePointerSpeedFromSettings();updateShowTouchesFromSettings();updateAccessibilityLargePointerFromSettings();updateDeepPressStatusFromSettings("just booted");updateMaximumObscuringOpacityForTouchFromSettings();updateBlockUntrustedTouchesModeFromSettings();}......
}
IMS 的启动过程如下:
- 启动 native 层输入系统,其实就是启动刚刚说到的 InputReader 和 InputDispatcher。
- 注册监听广播,因为这些广播与输入系统的配置有关,当接收到这些广播,会更新配置到 native 层。
- 直接读取配置,更新到 native 层输入系统。
2.2、NativeInputManager # nativeStart()
xref: /frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
static const JNINativeMethod gInputManagerMethods[] = { // JNI 注册的映射关系/* name, signature, funcPtr */{"nativeStart", "(J)V", (void*)nativeStart},
};static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) {// 将 Java 层保存的 NativeInputManager 对象的指针转换成 NativeInputManager 对象NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);// 查看 1.4 NativeInputManager 的源码可知,获取到 InputManager 对象,然后调用其 start() 函数status_t result = im->getInputManager()->start();if (result) {jniThrowRuntimeException(env, "Input manager could not be started.");}
}
首先将 Java 层保存 NativeInputManager 对象的指针 mPtr 转换成 NativeInputManager 对象,然后调用 NativeInputManager # getInputManager() 函数获取到 InputManager 对象,接着调用 InputManager # start() 函数继续启动流程。
reinterpret_cast 的功能可以分为两类:1、指针和整数之间的转换;2、不同类型的指针/成员指针/引用之间的转换。
2.3、InputManager # start()
xref: /frameworks/native/services/inputflinger/InputManager.cpp
status_t InputManager::start() {// 启动承载 InputDispatcher 的线程status_t result = mDispatcher->start();if (result) {ALOGE("Could not start InputDispatcher thread due to error %d.", result);return result;}// 启动承载 InputReader 的线程result = mReader->start();if (result) {ALOGE("Could not start InputReader due to error %d.", result);mDispatcher->stop();return result;}return OK;
}
InputManager 的启动过程很简单,调用 InputDispatcher 和 InputReader 的 start() 函数,启动承载它们运行的线程,来看一下它们是如何启动线程的。
2.4、InputDispatcher # start()
xref: /frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
status_t InputDispatcher::start() {if (mThread) {return ALREADY_EXISTS;}mThread = std::make_unique<InputThread>("InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });return OK;
}
在方法内,首先判断 mThread 是否已存在,存在则直接返回,不存在则通过 std::make_unique 函数来构建 InputThread 的实例对象,但没有看到启动线程的代码逻辑,带着这个疑问,我们再去看看 InputReader 的 start() 方法。
2.5、InputReader # start()
xref: /frameworks/native/services/inputflinger/reader/InputReader.cpp
status_t InputReader::start() {if (mThread) {return ALREADY_EXISTS;}mThread = std::make_unique<InputThread>("InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });return OK;
}
方法的代码逻辑跟 InputDispatcher 的差不多,也是没有看到启动线程的代码逻辑,既然都是新建 InputThread 对象,那就具体来看一下 InputThread 类。
2.6、InputThread
xref: /frameworks/native/services/inputflinger/include/InputThread.h
class InputThread {
public:explicit InputThread(std::string name, std::function<void()> loop,std::function<void()> wake = nullptr);virtual ~InputThread();bool isCallingThread();
private:std::string mName; // 线程名std::function<void()> mThreadWake;sp<Thread> mThread; // 承载 InputDispatcher\InputReader 运行的线程
};
xref: /frameworks/native/services/inputflinger/InputThread.cpp
class InputThreadImpl : public Thread {
public: // explicit 关键字的作用就是防止类构造函数的隐式自动转换,且只对有一个参数的类构造函数有效explicit InputThreadImpl(std::function<void()> loop): Thread(/* canCallJava */ true), mThreadLoop(loop) {}~InputThreadImpl() {}private:std::function<void()> mThreadLoop; // 存储一个可调用对象,这里指的是 lambda 表达式bool threadLoop() override {mThreadLoop();return true;}
};InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake): mName(name), mThreadWake(wake) {// 使用封装的可调用对象 loop 新建 InputThreadImpl 对象mThread = new InputThreadImpl(loop);// 启动 InputThreadImpl 线程mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);
}
std::function:是一个通用的函数封装类,它可以存储、复制和调用任意可调用对象,包括函数指针、函数对象、成员函数指针和lambda 表达式等。通过使用 std::function 作为函数参数,我们可以实现更加灵活的函数调用方式,提高代码的可读性和可维护性。
由代码可知 InputThread 类其本身不是一个线程,其内部是 InputThreadImpl 类来实现线程的具体功能。使用封装的可调用对象 loop 构建 InputThreadImpl 对象,然后调用其 run() 函数来启动线程。
InputThreadImpl 类继承自 Thread 类,而 C++ 的 Thread 类提供了一个名为 threadLoop() 的纯虚函数,当线程开始运行后,将会在内建的线程循环中不断地调用 threadLoop() 函数,直到此函数返回 false,则退出线程循环结束线程。但从 InputThreadImpl 类的定义可以看出,threadLoop() 函数会一直保持循环(返回值始终为 true),并且每一次循环,会调用一次 mThreadLoop() 函数,而 mThreadLoop() 函数是由 InputDispacher 和 InputReader 在启动时封装好传入的可调用对象。
到这里,终于搞明白了,在 InputDispatcher 启动时,会创建一个线程,然后循环调用 dispatchOnce() 函数,同样 InputReader 启动时,也会创建一个线程,然后循环调用 loopOnce() 函数。
3、IMS 系统就绪
上面两节已完成 IMS 及其重要参与者的构建,并启动了 IMS 系统,接下来就是通知系统 IMS 系统已完成启动并准备就绪,具体看一下源码
xref: /frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stubimplements Watchdog.Monitor {......// IMS 系统内部的 Handler,用来处理键盘等输入设备有关的消息private final InputManagerHandler mHandler;private WiredAccessoryCallbacks mWiredAccessoryCallbacks; // 有线连接的设备回调private boolean mSystemReady; // 标志系统是否准备完毕private NotificationManager mNotificationManager; // 通知管理......public void systemRunning() {......mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);synchronized (mLidSwitchLock) {mSystemReady = true;......int switchState = getSwitchState(-1 /* deviceId */, InputDevice.SOURCE_ANY, SW_LID);for (int i = 0; i < mLidSwitchCallbacks.size(); i++) {LidSwitchCallback callback = mLidSwitchCallbacks.get(i);callback.notifyLidSwitchChanged(0 /* whenNanos */, switchState == KEY_STATE_UP);}}// 监听广播,通知 native 层加载键盘布局IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);filter.addAction(Intent.ACTION_PACKAGE_REMOVED);filter.addAction(Intent.ACTION_PACKAGE_CHANGED);filter.addAction(Intent.ACTION_PACKAGE_REPLACED);filter.addDataScheme("package");mContext.registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 其内部继续调用 reloadKeyboardLayouts() 函数updateKeyboardLayouts();}}, filter, null, mHandler);// 监听广播,通知 native 层加载设备别名filter = new IntentFilter(BluetoothDevice.ACTION_ALIAS_CHANGED);mContext.registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {reloadDeviceAliases();}}, filter, null, mHandler);// 通过 InputManagerHandler 发送消息来通知 native 层加载键盘布局和加载设备别名mHandler.sendEmptyMessage(MSG_RELOAD_DEVICE_ALIASES);mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS);// 如果与系统连接的有线设备的回调不为空,则须回调通知其输入系统 IMS 已准备完毕if (mWiredAccessoryCallbacks != null) { mWiredAccessoryCallbacks.systemReady();}}private void reloadKeyboardLayouts() {if (DEBUG) {Slog.d(TAG, "Reloading keyboard layouts.");}// 调用 native 层函数来加载键盘布局nativeReloadKeyboardLayouts(mPtr);}private void reloadDeviceAliases() {if (DEBUG) {Slog.d(TAG, "Reloading device names.");}// 调用 native 层函数来加载设备别名nativeReloadDeviceAliases(mPtr);}......
}
注册监听广播,通知 native 层加载键盘布局、设备别名,最后通过 JNI 调用 native 层的函数来加载键盘布局、设备别名。此外,如果与系统连接的有线设备注册的回调不为空,则需回调通知其输入系统 IMS 已准备就绪。
三、总结
1、IMS 启动时序图
2、IMS 成员关系图
最后结合 IMS 的启动时序图和成员关系图可以更深刻的理解 IMS 系统的构成与启动过程,下一篇文章来继续探索输入系统的重要组成成员,等待后续吧!
参考
- 深入理解Android:卷III
相关文章:
深入Android S (12.0) 探索Framework之输入系统IMS的构成与启动
文章目录 前言一、输入系统的基本组成部分二、输入系统相关源码分析1、IMS 构建1.1、SystemServer # startOtherServices()1.2、InputManagerService1.3、NativeInputManager # nativeInit()1.4、NativeInputManager1.5、InputManager1.6、InputDispatcher1.7、InputReader1.8、…...
SoC with CPLD and MCU ?
AG32 MCU 产品支持多种接口外设,具备与业界主流产品的兼容性,并内置额外的2K FPGA 可编程逻辑。 产品支持 LQFP-48,LQFP-64,LQFP-100 ,QFN-32等不同封装。其所有可用 IO 都可以任意地进行映射和互换,以灵活…...
基于AWS Serverless的Glue服务进行ETL(提取、转换和加载)数据分析(二)——数据清洗、转换
2 数据清洗、转换 此实验使用S3作为数据源 ETL: E extract 输入 T transform 转换 L load 输出 大纲 2 数据清洗、转换2.1 架构图2.2 数据清洗2.3 编辑脚本2.3.1 连接数据源(s3)2.3.2. 数据结构转换2.3.2 数据结构拆分…...
vuepress-----6、时间更新
# 6、时间更新 基于Git提交时间修改文字时间格式 moment # 最后更新时间 # 时间格式修改 下载库文件 yarn add momentconst moment require(moment); moment.locale(zh-cn)module.exports {themeConfig: {lastUpdated: 更新时间,},plugins: [[vuepress/last-updated,{trans…...
C++ ini配置文件的简单读取使用
ini文件就是简单的section 下面有对应的键值对 std::map<std::string, std::map<std::string, std::string>>MyIni::readIniFile() {std::ifstream file(filename);if (!file.is_open()) {std::cerr << "Error: Unable to open file " << …...
【稳定检索|投稿优惠】2024年经济管理与安全科学国际学术会议(EMSSIC 2024)
2024年经济管理与安全科学国际学术会议(EMSSIC 2024) 2024 International Conference on Economic Management and Security Sciences(EMSSIC 2024) 一、【会议简介】 2024年经济管理与安全科学国际学术会议(EMSSIC 2024),将于繁华的上海城召开。这次会议的主题是“…...
什么是网站?
这篇文章是我学习网站开发,阶段性总结出来的。可以帮助你 通俗易懂 地更加深刻理解网站的这个玩意。 一,网站和网页的区别? 网站是由一个个网页组成。我们在浏览器上面看到的每一个页面就是网页,这些 相关的 网页组成一个网站。…...
pg_stat_replication.state 含义
在PostgreSQL中,pg_stat_replication视图提供了有关连接到主服务器的流式复制进程(备用服务器)的信息。该视图中的一个列是state,它指示复制进程的当前状态。 state列可以具有各种值: startup: This WAL sender 刚开始运行 catc…...
JavaWeb(六)
一、Maven的常用命令 maven的常用命令有:compile(编译)、clean(清理)、test(测试)、package(打包)、install(安装)。 1.1、compile(编译) compile(编译)的作用有如下两点: 1、从阿里云下载编译需要的jar包,在本地仓库也能看到下载好的插件(远程仓库配置的是阿里…...
GPIO的使用--时钟使能含义--代码封装
目录 一、时钟使能的含义 1.为什么要时钟使能? 2.什么是时钟使能? 3.GPIO的使能信号? 二、代码封装 1.封装前完整代码 2.封装结构 封装后代码 led.c led.h key.c key.h main.c 一、时钟使能的含义 1.为什么要时钟使能?…...
最小化安装 Neokylin7.0 用于搭建 Hadoop 集群
文章目录 环境搭建背景虚拟机创建和环境配置安装过程注意事项虚拟机设置软件选择KOUMP系统分区网络和主机名打开以太网,并记录信息配置 IPv4修改主机名 创建用户 hadoop完全分布式搭建-CSDN博客 环境搭建背景 为什么不从hadoop100或者hadoop101开始,而是…...
苍穹外卖面试题-中
8. 如何理解分组校验 很多情况下,我们会将校验规则写到实体类中的属性上,而这个实体类有可能作为不同功能方法的参数使用,而不同的功能对象参数对象中属性的要求是不一样的。比如我们在新增和修改一个用户对象时,都会接收User对象…...
Python 重要数据类型
目录 列表 序列操作 列表内置方法 列表推到式 字典 声明字典 字典基本操作 列表内置方法 字典进阶使用 字典生成式 附录 列表 在实际开发中,经常需要将一组(不只一个)数据存储起来,以便后边的代码使用。列表就是这样的…...
03、pytest初体验
官方实例 # content of test_sample.py def func(x):return x 1def test_ansewer():assert func(3) 5步骤解释 [100%]指的是所有测试用例的总体进度,完成后,pytest显示一个失败报告,因为func(3)没有返回5 注意:你可以使用ass…...
智能指针及强相关知识经验总结 --- 移动语义、引用计数、循环引用、move()、自定义删除器等
目录 前言 一、shared_ptr 1. 基本用法和构造方法 2. 引用计数机制 3. weak_ptr 解决循环引用 二、unique_ptr 1. 基本用法和构造方法 2. 独占性 3. 所有权转移 1)unique_ptr :: release() 2)移动语义 和 move() 三、 对比 shared_ptr 和 un…...
Gson 自动生成适配器插件
在json解析方面 我们常见有下面几方面困扰 1. moshi code-gen能自动生成适配器,序列化效率比gson快,但是自定义程度不如gson,能java kotlin共存 且解决了默认值的问题 2.gson api 强大自由,但是 第一次gson的反射缓存比较慢,而且生成对象都是反射,除非主动注册com.google.gson…...
React创建项目
React创建项目 提前安装好nodejs再进行下面的操作,通过node -v验证是否安装 1.设置源地址 npm config set registry https://registry.npmmirror.com/2.确认源地址 npm config get registry返回如下 https://registry.npmmirror.com/3.输入命令 npx create-re…...
Redis5新特性-stream
Stream队列 Redis5.0 最大的新特性就是多出了一个数据结构 Stream,它是一个新的强大的 支持多播的可持久化的消息队列,作者声明 Redis Stream 地借鉴了 Kafka 的设计。 生产者 xadd 追加消息 xdel 删除消息,这里的删除仅仅是设置了标志位&am…...
删除PPT文件的备注内容
解决方案的工作经常汇报以及经常做ppt的回报工作,但是删除备注很痛苦。 在网上或者拿历史的ppt文件修改后,需要删除ppt备注内容以及删除ppt个人文件信息的办法: 现象:很多备注信息,需要删除 解决办法一、 文件--信息-…...
2023年亚太杯APMCM数学建模大赛B题玻璃温室小气候调控
2023年亚太杯APMCM数学建模大赛 B题 玻璃温室小气候调控 原题再现 温室作物的产量受各种气候因素的影响,包括温度、湿度和风速[1]。其中,适宜的温度和风速对植物生长至关重要[2]。为了调节玻璃温室内的温度、风速等气候因素,在温室设计中常…...
Oracle 查询语句限制只选择最前面几行,和最后面几行的实现方式。
查询最前面几行 在Oracle中,可以使用 ROWNUM 关键字来限制查询结果的行数。要选择前10条记录,可以使用以下查询语句: SELECT * FROM your_table WHERE ROWNUM < 10;实际查询时将your_table替换为要查询的表名。以上查询将返回表中的前10…...
.NET Core6.0 MVC+layui+SqlSugar 简单增删改查
HTML部分: {ViewData["Title"] "用户列表"; } <!DOCTYPE html> <html> <head><meta charset"utf-8"><title>用户列表</title><meta name"renderer" content"webkit"><meta …...
在 Mac 上使用浅色或深色外观
在 Mac 上,选取苹果菜单 >“系统设置”,然后点按边栏中的“外观” 。(你可能需要向下滚动。)选择右侧的“浅色”、“深色”或“自动”。 “浅色”表示不会发生变化的浅色外观。 “深色”表示不会发生变化的深色外观。“深色模式…...
华为手环关闭智能适时测量
问题 使用华为手环并使用华为创新研究APP后,会自动打开智能适时测量开关,此开关开启后,手环会在睡眠时间自动测量血氧,增加手环功耗从而影响续航,用户可根据自身需求决定是否开启,下文介绍如何找到此开关。…...
1-Hadoop原理与技术
单选题 题目1:安装Hadoop集群时,是在哪个文件指定哪些机器作为集群的从机? 选项: A datanode B slaves C yarn-site.xml D core-site.xml 答案:B ------------------------------ 题目2:Hadoop配置文件所在目录是哪…...
YoloV5改进策略:Swift Parameter-free Attention,无参注意力机制,超分模型的完美迁移
摘要 https://arxiv.org/pdf/2311.12770.pdf https://github.com/hongyuanyu/SPAN SPAN是一种超分网络模型。SPAN模型通过使用参数自由的注意力机制来提高SISR的性能。这种注意力机制能够增强重要信息并减少冗余,从而在图像超分辨率过程中提高图像质量。 具体来说,SPAN模…...
DAPP开发【04】测试驱动开发
测试驱动开发(Test Driven Development),是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,…...
Raspberry Pi 2, 2 of n - Pi 作为 IoT 消息代理
目录 介绍 环境 先决条件 - 设置静态 IP 地址 安装 Mosquitto 启动/停止 Mosquitto 配置先决条件 - 安装 mqtt_spy 配置 Mosquitto 配置 Mosquitto - 无安全性 测试 Mosquitto 配置 - 无安全性 配置 Mosquitto - 使用密码身份验证 Mosquitto 测试 - 带密码验证 概括 介绍 在本文…...
linux服务器环境搭建(使用yum 安装mysql、jdk、redis)
一:yum的安装 1:下载yum安装包并解压 wget http://yum.baseurl.org/download/3.2/yum-3.2.28.tar.gz tar xvf yum-3.2.28.tar.gz 2.进入yum-3.2.28文件夹中进行安装,执行安装指令 cd yum-3.2.28 sudo apt install yum 3.更新版本 yum check-update yum update yum cle…...
互联网Java工程师面试题·Spring Boot篇·第二弹
目录 8、什么是 YAML? 9、如何实现 Spring Boot 应用程序的安全性? 10、如何集成 Spring Boot 和 ActiveMQ? 11、如何使用 Spring Boot 实现分页和排序? 12、什么是 Swagger?你用 Spring Boot 实现了它吗? …...
网站百度权重查询/谷歌广告联盟怎么做
题目 题目链接 题解 太水了啊。 就输入判断就行,不会STL的string就去学一学,不想学的就死一边子去。 当然也可以用strcmp,不过我都快忘了咋用了。 我也懒得写for了,就while输入了。 我的锅,原来是入门题ÿ…...
南宁培训网站建设/看网站时的关键词
数据库是应用及计算机的核心元素,负责存储运行软件应用所需的一切重要数据。为了保障应用正常运行,总有一个甚至多个数据库在默默运作。我们可以把数据库视为信息仓库,以结构化的方式存储了大量的相关信息,并合理分类,…...
什么是网站名称/谷歌paypal下载
开启三台虚拟机 实战:使用varnish加速多个不同域名站点的web服务器 varnish:192.168.80.100 //需要联网 web1:192.168.80.101——www.aa.com web2:192.168.80.102——www.bb.com 三台服务器全都要操作 systemctl stop f…...
广东佛山如何制作网站公司/google app
1.简单工厂模式 简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 注意: 实际上简单工厂不是一个设计模式,更多程度上比较像一种编程习惯。 结构图: Factory:工厂类ÿ…...
赌博 网站 建设/厦门seo排名收费
memcached是一款数据库缓存加速的功能,他可以在很多平台中使用如windows,linux系统中都可以,下面我来介绍在windows搭建memcached环境配置方法,Windows下的Memcache安装1、下载memcache for windows。下载地址:http://jehiah.cz/p…...
网站宣传的传统方式有哪些/网络外贸推广
sem_post,,信号量,,,共享内存,,互斥锁,,内存映射 kmustchenb 的 信号量sem_t,互斥锁pthread_mutex_t的使用 信号量的数据类型为结构sem_t,它本质上是一个长整型的数。 …...