View的绘制流程一 DecorView是什么时候添加到Window上的

2024-01-20 02:12

本文主要是介绍View的绘制流程一 DecorView是什么时候添加到Window上的,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

setContentView做了什么?

setContentView只是创建了一个Decorview,把我们的xml添加到Decorview里面。但是DecorView并没有添加到PhoneView里面。

比如果在onCreate,onResume去拿控件的高度是拿不到的。

Activity的onResume方法在什么时候执行呢?

ActivityThread的handleResumeActivity方法里面触发performResumeActivity方法,performResumeActivity方法里面会执行r.activity.performResume,接着走到mInstrumentation.callActivityOnResume,这个方法里面触发activity.onResume()。

handleResumeActivity(ActivityThread.java) 
--> performResumeActivity(ActivityThread.java) --> r.activity.performResume(Activity.java) --> mInstrumentation.callActivityOnResume(Activity.java) -->activity.onResume(Instrumentation.java)
 ActivityThread.java@Overridepublic void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, String reason) {// If we are getting ready to gc after going to the background, well// we are back active so skip it.unscheduleGcIdler();mSomeActivitiesChanged = true;// TODO Push resumeArgs into the activity for consideration// skip below steps for double-resume and r.mFinish = true case.if (!performResumeActivity(r, finalStateRequest, reason)) {return;}........
}public boolean performResumeActivity(ActivityClientRecord r, boolean finalStateRequest,String reason) {......try {r.activity.onStateNotSaved();r.activity.mFragments.noteStateNotSaved();checkAndBlockForNetworkAccess();if (r.pendingIntents != null) {deliverNewIntents(r, r.pendingIntents);r.pendingIntents = null;}if (r.pendingResults != null) {deliverResults(r, r.pendingResults, reason);r.pendingResults = null;}r.activity.performResume(r.startsNotResumed, reason);r.state = null;r.persistentState = null;r.setState(ON_RESUME);reportTopResumedActivityChanged(r, r.isTopResumedActivity, "topWhenResuming");} ......}Activity.javafinal void performResume(boolean followedByPause, String reason) {......// mResumed is set by the instrumentationmInstrumentation.callActivityOnResume(this);......}Instrumentation.javapublic void callActivityOnResume(Activity activity) {activity.mResumed = true;activity.onResume();......}

DecorView是什么时候添加到Window上的?

ActivityThread的handleResumeActivity方法:

 @Overridepublic void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, String reason) {........final Activity a = r.activity;........if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();//拿到DecorViewView decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;......if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;//把DecorView与Phone关联起来wm.addView(decor, l);} else {// The activity will get a callback for this {@link LayoutParams} change// earlier. However, at that time the decor will not be set (this is set// in this method), so no action will be taken. This call ensures the// callback occurs with the decor set.a.onWindowAttributesChanged(l);}}// If the window has already been added, but during resume// we started another activity, then don't yet make the// window visible.} ........}

在handleResumeActivity方法里面,先拿到DecorView,然后调用WindowManager的addView方法,把DecorView与PhoneWindow关联起来。

WindowManager是啥?

1.WindowManager在什么时候创建?

在ActivityThread的performLaunchActivity里面执行了activity的attach方法:

 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {........if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "+ r.activityInfo.name + " with config " + config);Window window = null;if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {window = r.mPendingRemoveWindow;r.mPendingRemoveWindow = null;r.mPendingRemoveWindowManager = null;}// Activity resources must be initialized with the same loaders as the// application context.appContext.getResources().addLoaders(app.getResources().getLoaders().toArray(new ResourcesLoader[0]));appContext.setOuterContext(activity);//会在这个方法中创建Activity的PhoneWindow,并绑定对应的WindowManager。activity.attach(appContext, this, getInstrumentation(), r.token,r.ident, app, r.intent, r.activityInfo, title, r.parent,r.embeddedID, r.lastNonConfigurationInstances, config,r.referrer, r.voiceInteractor, window, r.activityConfigCallback,r.assistToken, r.shareableActivityToken);........}

 再看看Activty的attach方法:

  @UnsupportedAppUsagefinal void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, IVoiceInteractor voiceInteractor,Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {//  PhoneWindow mWindow = new PhoneWindow(this, window, activityConfigCallback);mWindow.setWindowControllerCallback(mWindowControllerCallback);// PhoneWindow 的 callback 设置为activitymWindow.setCallback(this);........//设置 PhoneWindow的WindowManagermWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);if (mParent != null) {mWindow.setContainer(mParent.getWindow());}// 获取 WindowManagerImpl 作为windowManagermWindowManager = mWindow.getWindowManager();mCurrentConfig = config;........}

 WindowManager的创建时机在执行ActivityThread的performLaunchActivity方法里面,因为调用了Activity的attach而创建出来。

2.WindowManger与WindowManagerImpl,WindowManagerGlobal关系

再来看到setWindowManager方法:

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,boolean hardwareAccelerated) {mAppToken = appToken;mAppName = appName;mHardwareAccelerated = hardwareAccelerated;if (wm == null) {wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);}mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);}

我们发现wm原来是WindowManagerImpl,那么在handResumeActivity方法里面的wm.addview会执行WindowManagerImpl里面的addView方法。

    @UnsupportedAppUsageprivate final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyTokens(params);mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,mContext.getUserId());}

 WindowManagerImpl的addView最终又是调用WindowManagerGlobal的addView方法。那么WindowManger,WindowManagerImpl,WindowManagerGlobal关系是怎么样的呢?

WindowManagerImpl:确定View属于哪个屏幕,哪个父窗口

WindowManagerGlobal:管理整个进程,所有的窗口信息

ViewRootImpl:WindowManagerGlobal 实际操作者,操作自己的窗口

WindowManager是一个接口类,继承自接口ViewManager,负责窗口的管理(增、删、改)。它的实现类是WindowManagerlmpl,而具体操作实际上又会交给WindowManagerGlobal来处理,它是个单例,进程唯一。WindowManagerGlobal执行addView的方法中会传入DecorView,还会初始化一个ViewRootlmpl。WindowManagerGlobal因为是单例的,它内部会有两个List来分别保存这两个对象,来统一管理。

3.ViewRootImpl是什么

接下来我们来看WindowManagerGlobal的addView方法:

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow, int userId) {if (view == null) {throw new IllegalArgumentException("view must not be null");}if (display == null) {throw new IllegalArgumentException("display must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;/* 如果当前窗口需要被添加为另一个窗口的附属窗口(子窗口),则需要让父窗口视自己的情况,对当前窗口的布局参数(LayoutParams)进行一些修改 */if (parentWindow != null) {parentWindow.adjustLayoutParamsForSubWindow(wparams);} else {// If there's no parent, then hardware acceleration for this view is// set from the application's hardware acceleration setting.final Context context = view.getContext();if (context != null&& (context.getApplicationInfo().flags& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;}}ViewRootImpl root;View panelParentView = null;synchronized (mLock) {// Start watching for system property changes.if (mSystemPropertyUpdater == null) {mSystemPropertyUpdater = new Runnable() {@Override public void run() {synchronized (mLock) {for (int i = mRoots.size() - 1; i >= 0; --i) {mRoots.get(i).loadSystemProperties();}}}};SystemProperties.addChangeCallback(mSystemPropertyUpdater);}// WindowManager不允许同一个View被添加两次int index = findViewLocked(view, false);if (index >= 0) {if (mDyingViews.contains(view)) {// Don't wait for MSG_DIE to make it's way through root's queue.mRoots.get(index).doDie();} else {throw new IllegalStateException("View " + view+ " has already been added to the window manager.");}// The previous removeView() had not completed executing. Now it has.}// If this is a panel window, then find the window it is being// attached to for future reference.if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {final int count = mViews.size();for (int i = 0; i < count; i++) {if (mRoots.get(i).mWindow.asBinder() == wparams.token) {panelParentView = mViews.get(i);}}}// 创建一个ViewRootImpl对象,ViewRootImpl实现了一个控件树的根// 它负责与WMS进行直接的通讯,负责管理Surface,负责触发控件的测量与布局,负责触发控件的绘制,同时也是输入事件的中转站。root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);// 将下面三个参数保存到对应集合中,以供之后的查询,控件、布局参数、ViewRootImpl三者共同组成了客户端的一个窗口// 将 DecorView 添加到 mViews 中mViews.add(view);// 将 ViewRootImpl 添加到 mRoots 中mRoots.add(root);// 将 DecorView 的 LayoutParams参数 添加到 mParams 中mParams.add(wparams);// do this last because it fires off messages to start doing thingstry {/* 将作为窗口的控件设置给ViewRootImpl。这个动作将导致ViewRootImpl向WMS添加新的窗口、申请Surface以及托管控件在Surface上的重绘动作。这才是真正意义上完成了窗口的添加操作*/root.setView(view, wparams, panelParentView, userId);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.if (index >= 0) {removeViewLocked(index, true);}throw e;}}}

这个方法里面主要是创建了ViewRootImpl,把WindowManager.LayoutParams设置给DecorView了。并且设置了三个list,分别把ViewRootImpl,DecorView和WindowManager.LayoutParams给保存起来,调用ViewRootImpl的setView方法,把ViewRootImpl设置为DecorView的父布局,开启view绘制流程:

 // 创建一个ViewRootImpl对象,ViewRootImpl实现了一个控件树的根// 它负责与WMS进行直接的通讯,负责管理Surface,负责触发控件的测量与布局,负责触发控件的绘制,同时也是输入事件的中转站。root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);// 将下面三个参数保存到对应集合中,以供之后的查询,控件、布局参数、ViewRootImpl三者共同组成了客户端的一个窗口// 将 DecorView 添加到 mViews 中mViews.add(view);// 将 ViewRootImpl 添加到 mRoots 中mRoots.add(root);// 将 DecorView 的 LayoutParams参数 添加到 mParams 中mParams.add(wparams);
// do this last because it fires off messages to start doing thingstry {/* 将作为窗口的控件设置给ViewRootImpl。这个动作将导致ViewRootImpl向WMS添加新的窗口、申请Surface以及托管控件在Surface上的重绘动作。这才是真正意义上完成了窗口的添加操作*/root.setView(view, wparams, panelParentView, userId);}
 ViewRootImpl构造方法

我们进入ViewRootImpl的构造方法一探究竟:

 public ViewRootImpl(Context context, Display display, IWindowSession session,boolean useSfChoreographer) {....../* 保存当前线程到mThread。这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常 */mThread = Thread.currentThread();....../* mDirty用于收集窗口中的无效区域。所谓无效区域是指由于数据或状态发生改变时而需要进行重绘的区域。举例说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效区域,并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便可以将新的文字绘制在这块区域上 */mDirty = new Rect();mTempRect = new Rect();mVisRect = new Rect();/* mWinFrame,描述了当前窗口的位置和尺寸。与WMS中WindowState.mFrame保持着一致 */mWinFrame = new Rect();/* 创建一个W类型的实例,W是IWindow.Stub的子类。即它将在WMS中作为新窗口的ID,以及接 收来自WMS的回调 */mWindow = new W(this);....../* 创建mAttachInfo。mAttachInfo是控件系统中很重要的对象。它存储了此当前控件树所贴附 的窗口的各种有用的信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession,窗口的实例(即mWindow)ViewRootImpl实例,窗口所属的Display,窗口的Surface以及窗口在屏幕上的位置等等。所以,当需要在一个View中查询与当前窗口相关的信息时,非常值得在mAttachInfo中搜索一下 */mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,    context);......}

ViewRootImpl创建的时候,有三个地方需要注意:

1.保存当前线程到mThread

这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程。在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检查发起请求的thread与这个mThread是否相同,倘若不同则会拒绝这个请求并抛出一个异常。

2.创建mDirty区域

mDirty用于收集窗口中的无效区域。所谓无效区域是指由于数据或状态发生改变时而需要进行重绘
的区域。举例说明,当应用程序修改了一个TextView的文字时,TextView会将自己的区域标记为无效区域,并通过invalidate()方法将这块区域收集到这里的mDirty中。当下次绘制时,TextView便可以将新的文字绘制在这块区域上。

3.创建mAttachInfo

mAttachInfo是控件系统中很重要的对象。它存储了此当前控件树所贴附的窗口的各种有用的信息,并且会派发给控件树中的每一个控件。这些控件会将这个对象保存在自己的mAttachInfo变量中。mAttachInfo中所保存的信息有WindowSession,窗口的实例(即mWindow),ViewRootImpl实例,窗口所属的Display,窗口的Surface以及窗口在屏幕上的位置等等。所以,当需要在一个View中查询与当前窗口相关的信息时,非常值得在mAttachInfo中搜索一下。

真正把DecorView添加到PhoneWindow的地方

接着来看ViewRootImpl.setView方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {synchronized (this) {if (mView == null) {// mView 保存了控件树的根mView = view;......//在添加到窗口管理器之前安排第一个布局,以确保在从系统接收任何其他事件之前进行重新布局?               /* 在添加窗口之前,先通过requestLayout()方法在主线程上安排一次“遍历”。//所谓“遍历”是指ViewRootImpl中的核心方法performTraversals()。    这个方法实现了对控件树进行测量、布局、向WMS申请修改窗口属性以及重绘的所有工作。由于此“遍历”操作对于初次遍历做了一些特殊处理,而来自WMS通过mWindow发生的回调会导致一些属性发生变化?如窗口的尺寸、Insets以及窗口焦点等,从而有可能使得初次“遍历”的现场遭到破坏。因此,需要在添加窗口之前,先发送一个“遍历”消息到主线程在主线程中向主线程的Handler发送消息,如果使用得当,可以产生很精妙的效果。例如本例中可以实现如下的执行顺序:添加窗口->初次遍历->处理来自WMS的回调 */ //TODOrequestLayout();InputChannel inputChannel = null; /* 初始化mInputChannel。InputChannel是窗口接受来自InputDispatcher的输入事件的管道。注意,仅当窗口的属性inputFeatures不含有INPUT_FEATURE_NO_INPUT_CHANNEL时才会创建InputChannel,否则mInputChannel为空,从而导致此窗口无法接受任何输入事件 */if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {inputChannel = new InputChannel();}mForceDecorViewVisibility = (mWindowAttributes.privateFlags& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;try {mOrigWindowType = mWindowAttributes.type;mAttachInfo.mRecomputeGlobalAttributes = true;collectViewAttributes();adjustLayoutParamsForCompatibility(mWindowAttributes);/* 将窗口添加到WMS中。完成这个操作之后,mWindow已经被添加到指定的Display中?                    而且mInputChannel(如果不为空)已经准备好接收事件了。只是由于这个窗口没有进行过relayout(),因此它还没有有效的Surface可以进行绘制 */res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mDisplayCutout, inputChannel,mTempInsets, mTempControls);setFrame(mTmpFrame);} ....../* 如果mInputChannel不为空,则创建mInputEventReceiver,用于接收输入事件。注意第二个参数传递的是Looper.myLooper(),即mInputEventReceiver将在主线程上触发输入事件的读取与onInputEvent()。这是应用程序可以在onTouch()等事件响应中                直接进行UI操作等根本原因。*/if (inputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}mInputEventReceiver = new WindowInputEventReceiver(inputChannel,Looper.myLooper());}/* ViewRootImpl将作为参数view的parent。所以,ViewRootImpl可以从控件树中任何一个控件开始,通过回溯getParent()的方法得到 */view.assignParent(this);......}

ViewRootImpl.setView方法主要是执行这几个重要步骤:

1.requestLayout()---》请求遍历控件树

2.res = mWindowSession.addToDisplayAsUser ---》将窗口添加到WMS上面

3.mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper())---》接收输入事件

4.view.assignParent(this) --》ViewRootImpl将作为DecorView的parent。所以,ViewRootImpl可以从控件树中任何一个控件开始,通过回溯getParent()的方法得到。

mWindowSession.addToDisplayAsUser ()这个方法最后会调用WindowManagerService中的addWindow()方法,这样一来,DecorView就被添加到Window上了。

回顾一下整个添加的流程

ActivityThread#handleResumeActivity —>WindowManagerImpl#addView
—>WindowManagerGlobal#addView —> ViewRootImpl#setView(调用mWindowSession.addToDisplayAsUser)

ActivityThread.handleResumeActivity 
--> wm.addView(decor, l); (WindowManagerImpl.java)--> WindowManagerGlobal.addView--> root = new ViewRootImpl(view.getContext(), display);--> mViews.add(view); // DecorViewmRoots.add(root); // ViewRootImplmParams.add(wparams); // WindowManager.LayoutParams--> root.setView(view, wparams, panelParentView, userId);-->res = mWindowSession.addToDisplayAsUser

这篇关于View的绘制流程一 DecorView是什么时候添加到Window上的的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

如何使用Python实现一个简单的window任务管理器

《如何使用Python实现一个简单的window任务管理器》这篇文章主要为大家详细介绍了如何使用Python实现一个简单的window任务管理器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 任务管理器效果图完整代码import tkinter as tkfrom tkinter i

Spring AI ectorStore的使用流程

《SpringAIectorStore的使用流程》SpringAI中的VectorStore是一种用于存储和检索高维向量数据的数据库或存储解决方案,它在AI应用中发挥着至关重要的作用,本文给大家介... 目录一、VectorStore的基本概念二、VectorStore的核心接口三、VectorStore的

python之流程控制语句match-case详解

《python之流程控制语句match-case详解》:本文主要介绍python之流程控制语句match-case使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录match-case 语法详解与实战一、基础值匹配(类似 switch-case)二、数据结构解构匹

在VSCode中本地运行DeepSeek的流程步骤

《在VSCode中本地运行DeepSeek的流程步骤》本文详细介绍了如何在本地VSCode中安装和配置Ollama和CodeGPT,以使用DeepSeek进行AI编码辅助,无需依赖云服务,需要的朋友可... 目录步骤 1:在 VSCode 中安装 Ollama 和 CodeGPT安装Ollama下载Olla

linux环境openssl、openssh升级流程

《linux环境openssl、openssh升级流程》该文章详细介绍了在Ubuntu22.04系统上升级OpenSSL和OpenSSH的方法,首先,升级OpenSSL的步骤包括下载最新版本、安装编译... 目录一.升级openssl1.官网下载最新版openssl2.安装编译环境3.下载后解压安装4.备份

C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)

《C#集成DeepSeek模型实现AI私有化的流程步骤(本地部署与API调用教程)》本文主要介绍了C#集成DeepSeek模型实现AI私有化的方法,包括搭建基础环境,如安装Ollama和下载DeepS... 目录前言搭建基础环境1、安装 Ollama2、下载 DeepSeek R1 模型客户端 ChatBo

Linux流媒体服务器部署流程

《Linux流媒体服务器部署流程》文章详细介绍了流媒体服务器的部署步骤,包括更新系统、安装依赖组件、编译安装Nginx和RTMP模块、配置Nginx和FFmpeg,以及测试流媒体服务器的搭建... 目录流媒体服务器部署部署安装1.更新系统2.安装依赖组件3.解压4.编译安装(添加RTMP和openssl模块

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

springboot启动流程过程

《springboot启动流程过程》SpringBoot简化了Spring框架的使用,通过创建`SpringApplication`对象,判断应用类型并设置初始化器和监听器,在`run`方法中,读取配... 目录springboot启动流程springboot程序启动入口1.创建SpringApplicat

通过prometheus监控Tomcat运行状态的操作流程

《通过prometheus监控Tomcat运行状态的操作流程》文章介绍了如何安装和配置Tomcat,并使用Prometheus和TomcatExporter来监控Tomcat的运行状态,文章详细讲解了... 目录Tomcat安装配置以及prometheus监控Tomcat一. 安装并配置tomcat1、安装