【Android】源码解析Activity的结构分析

源码解析Activity的结构分析

目录

  • 1、Activity、View、Window有什么关联?
  • 2、Activity的结构构建流程
  • 3 源码解析Activity的构成
    • 3.1 Activity的Attach方法
    • 3.2 Activity的OnCreate
  • 4、WindowManager与View的关系
  • 总结
    • 1、一个Activity对应几个WindowManage,对应几个Window?
    • 2、DecorView在哪被创建?
    • 3、PhoneWindow和Window有什么关系?
    • 4、在Activity的onResume方法调用Handler的post可以获取View的宽高吗?View的post方法能拿到View的宽高?

参考文献:
1、Android进阶之光第二版
2、Android 源码分析 - Activity的结构分析
3、反思|Android LayoutInflater机制的设计与实现

1、Activity、View、Window有什么关联?

用一个简单的例子理解它们,假设现在正在装修一个新房子:

📌Activity相当于一个房子。
Window相当于房子的窗户,可以通过窗户观察到房子。
WindowManage 相当于管家,控制窗户的开关。
View 相当于各种各样的家具。
layoutInflater 相当于室内装修师 将家具(View)正确的摆放在房子(Activity)中。
XML文件 就像是装修图纸,将不同的家具(View)排列组合

通过一个图理解它们之间的层级关系:

关于Activity和Window,DecorView怎么关联起来参考:View事件的分发机制

2、Activity的结构构建流程

首先简单介绍一下各个部分的作用:

ActivityThread:每个流程调用起始地点
Activity:相当于是一个管理者,负责创建WindowManagerWindow
Window:承载着View,同时代Activity处理一切View的事务。
WindowManager:从字面意思来理解是Window的管理,其实是管理Window上的View,包括addViewremove

3 源码解析Activity的构成

3.1 Activity的Attach方法

在Activity的Attach方法中主要做了两件事:

  1. 初始化mWindow,通过new PhoneWindow调用它的构造方法。
  2. 初始化WindowManage,并且将它set到Window中

接下来具体看看源码在干啥:

@UnsupportedAppUsage
private WindowManager mWindowManager;

final 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) {
    // ······
   mWindow = new PhoneWindow(this, window, activityConfigCallback);  //1
   // ······
   //  2
   mWindow.setWindowManager(
           (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
           mToken, mComponent.flattenToString(),
           (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
   // ······
     mWindowManager = mWindow.getWindowManager();  //3
}

这里的context.getSystemService方法就是用来返回一个WindowManage对象

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
                "System services not available to Activities before onCreate()");
    }

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

一个小疑问,为什么先setWindowManager接下来又通过getWindowManager获取mWindowManager,但是getSystemService返回的也是这个mWindowManager,这是在做什么?

实际上Android在这里做了一个缓存,在第一次创建时super.getSystemService(name);调用系统级别的管理器WindowManager,再之后的创建每一次都是同一个WindowManager

当我们调用 context.getSystemService(Context.WINDOW_SERVICE) 时,实际上返回的是 WindowManagerGlobal唯一的那个 WindowManagerImpl 实例的一个代理对象。这种设计使得整个系统只存在一个真正的 WindowManagerImpl 实例,所有视图都是由它来管理和调度的。

3.2 Activity的OnCreate

OnCreate主要通过setContentView方法给当前页面设置一个布局,实际上 Activity的setContentView并没有做什么工作,主要是Window的setContentView方法实现了这个功能。

当一个事件点击后首先传递给Activity,在我们写Activity时会调用setContentView方法来加载布局,我们看一下setContenView方法在做什么:

//frameworks/base/core/java/android/app/Activity.java

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

发现它首先调用了getWindowsetContentview方法,那么getWindow是什么呢?它返回了一个mWindow对象,查看代码后再Activity的attach方法中发现了它。

mWindow = new PhoneWindow(this, window, activityConfigCallback);

它原来是一个PhoneWindow,接下来我们看看它的setContentView方法在做什么。

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();   //1
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

FEATURE_CONTENT_TRANSITIONS是一个用于启用内容转换特性的标志,作用时提供一种动画效果过渡的切换视图。

我们重点看一下mContentParent为null时installDecor()方法做了什么。这个方法比较长,看一下重点地方:

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);  //1
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);  //2

看一下注释1的代码做了什么事情,发现这个generateDecor创建了一个DecorView

protected DecorView generateDecor(int featureId) {
    // System process doesn't have application context and in that case we need to directly use
    // the context we have. Otherwise we want the application context, so we don't cling to the
    // activity.
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, this);
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

查看DecorView源码发现,它继承了Fragment。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks 

接下来我们再回到installDecor() 方法,看一下注释2 中的generateLayout(mDecor)做了什么事。

这段代码很长,具体就不展示了。其中最重要的一点就是根据不同的情况给LayoutResource加载不同的布局。我们查看其中的一个布局文件R.layout.screen_title。这个文件在:frameworks/base/core/res/res/layout/screen_title.xml中,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

上面的ViewStub是用来显示Actionbar的,下面的两个Fragment一个是Title用于显示标题,另一个是Conten,用来显示内容。

刚刚通过这段源码分析可以知道一个Activity包含了一个Window对象,这个对象是PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,这个DecorView将屏幕分成两个区域,一个区域是TitleView,另一个区域是ContenView。而我们平常写的布局都是展示在ContenView中。如图:

4、WindowManager与View的关系

众所周知,DecrorViewViewParentViewRootImpl,而View最重要的三大流程就是由ViewRootImpl触发的。

结合上面的流程我们知道了DecroView的创建过程,那么它是如何被绑定到Window上的呢?ViewRootImpl又是怎么和WindowDecroView建立联系的?

我们先看一下ActivityThreadhandleResumeActivity方法在干什么:

代码较长这里截取关键地方

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
        //........
        // TODO 将resumeArgs推送到活动中以供考虑
        // 对于双恢复和 r.mFinish = true 的情况,跳过以下步骤。
        // 1
        if (!performResumeActivity(r, finalStateRequest, reason)) {
            return;
        }
        
        //........
        if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();  //2
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager(); //3
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        if (r.mPreserveWindow) {
            a.mWindowAdded = true;
            r.mPreserveWindow = false;
            // Normally the ViewRoot sets up callbacks with the Activity
            // in addView->ViewRootImpl#setView. If we are instead reusing
            // the decor view we have to notify the view root that the
            // callbacks may have changed.
            ViewRootImpl impl = decor.getViewRootImpl();
            if (impl != null) {
                impl.notifyChildRebuilt();
            }
        }
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l); //4
            } 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);
            }
        }
        
        //..........
     }

handleResumeActivity主要做了两件事件,第一件事情在注释1处,通过performResumeActivity进而回调ActivityonResume方法。

第二件事是注释2,3,4共同完成,它将一个DecorView添加到了WindowManage中。

我们详细看一下这个addView的过程,通过查找发现这个addView实际上是WindowManageImpladdView

//WindowManageImpl

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyTokens(params);
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

在这个方法中调用了mGlobaladdView方法,继续查找源码发现mGlobal居然是一个WindowManagerGlobal。看一下它的addView在干什么,同样的代码过长,我们在这选出重点代码。

//WindowManagerGlobal

private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();

public void addView(View view, ViewGroup.LayoutParams params,
      Display display, Window parentWindow, int userId) {
      
      //.....
       // 1
         if (windowlessSession == null) {
            root = new ViewRootImpl(view.getContext(), display);
        } else {
            root = new ViewRootImpl(view.getContext(), display,
                    windowlessSession, new WindowlessWindowLayout());
        }

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView, userId); //2
        } catch (RuntimeException e) {
            final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
            // BadTokenException or InvalidDisplayException, clean up.
            if (viewIndex >= 0) {
                removeViewLocked(viewIndex, true);
            }
            throw e;
        }
      
      //.....
      }

这个方法也主要干了两件事,在注释1处初始化了ViewRootImpl,在注释2处通过这个set方法将DecorView绑定到了ViewRootImpl中,并且触发了View的三大流程1

通过上面的分析我们知道,每个Window都对应着一个DecorView,而从这里我们可以发现,每个DecorView都对应着一个ViewRootImpl

📌从而得知,如果是一个Dialog或者其他新Window的界面,必定有一个新的ViewRootImpl来触发View的三大流程,而不是由宿主WindowViwRootImpl触发的。

总结

1、一个Activity对应几个WindowManage,对应几个Window?

通过3.1源码分析可知,一个Activity对应一个WindowManage,而一个WindowManage对应一个Window。并且一个Window对应一个DecorView,而每个DecorView着对应一个ViewRootImpl

有一些特殊情况下可能会存在多个 DecorView,比如系统弹出对话框或者悬浮窗口等。但是这些额外的 DecorView 通常不是直接与 Activity 关联的,而是由系统创建和管理的。在这些情况下,虽然存在多个 DecorView,但它们不是在同一个 Window 中,并且与主 ActivityDecorView 是独立的。

2、DecorView在哪被创建?

DecorView是在Window被创建的时候同步创建的,具体来说,DecorViewPhoneWindowsetContentView() 方法中被创建。Window会通过LayoutInflater将选定的DecorView布局加载并实例化成View对象。这个View对象就是DecorView

最后,DecorView会被设置为Window的顶级View,所有的UI界面都是附加到这个DecorView的子View上ContentView。

3、PhoneWindow和Window有什么关系?

它们是继承关系,PhoneWindow继承了Window,并针对手机平台的特性进行了具体实现和扩展。

4、在Activity的onResume方法调用Handler的post可以获取View的宽高吗?View的post方法能拿到View的宽高?

通过4部分的代码分析,我们知道ActivityonResume方法的执行是在ViewRootImpl触发测量过程之前,同时ViewRootImpl是通过如下的方式来触发测量过程的:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

这里使用了一个Handler.post了一个异步消息来进行测量。尽管post的是异步消息,但在onResume方法中无法保证中立即获取到正确的视图宽高,在Activity的onResume方法调用Handler.post不能获取View的宽高。

View.post方法可以获取View的宽高,View.post 方法添加的消息会在主线程空闲时被处理,这时候通常是在视图的测量和布局过程完成之后。


  1. View 的三大流程通常指的是 View 的绘制流程、布局流程和事件分发流程。 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/604522.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【论文阅读笔记】关于“二进制函数相似性检测”的调研(Security 22)

个人博客链接 注&#xff1a;部分内容参考自GPT生成的内容 [Security 22] 关于”二进制函数相似性检测“的调研&#xff08;个人阅读笔记&#xff09; 论文&#xff1a;《How Machine Learning Is Solving the Binary Function Similarity Problem》&#xff08;Usenix Securi…

C++ 模拟实现 priority_queue(优先队列)

目录 一&#xff0c;优先队列简介 二&#xff0c;priority_queue 的内部实现原理 三&#xff0c;模拟实现 priority_queue 1&#xff0c;模板参数与数据结构 2&#xff0c;构造 3&#xff0c;辅助功能&#xff08;堆的有序化&#xff0c;建立堆&#xff09; 4&#xff0…

嵌入式学习69-C++(Opencv)

知识零碎&#xff1a; QT的两种编译模式 1.debug 调试模式 …

springboot整合rabbitmq的不同工作模式详解

前提是已经安装并启动了rabbitmq&#xff0c;并且项目已经引入rabbitmq&#xff0c;完成了配置。 不同模式所需参数不同&#xff0c;生产者可以根据参数不同使用重载的convertAndSend方法。而消费者均是直接监听某个队列。 不同的交换机是实现不同工作模式的关键组件.每种交换…

泛微E9开发 选择项目类型,自动带出该类项目的预计金额(即下拉框联动浮点型数据)

1、功能背景 在用户进行项目类型选择时&#xff0c;自动带出其余的标准数据&#xff08;样例中的预计金额&#xff09;&#xff0c;如对员工进行表彰奖励时&#xff0c;不同的表彰有不同的奖励金额&#xff0c;那么我们就可以使用以下的方式来进行操作。 2、展示效果 3、实现…

WiFine通信与Wi-sun通信对比

调制速率 WiFine通信&#xff1a;(G)FSK 50Kbps~500Kbps &#xff1b;LoRa 5Kbps~37.5Kbps Wi-Sun通信&#xff1a;(G)FSK 50Kbps~300Kbps &#xff1b;QPSK/OFDM 计划中… 2、协议简介 WiFine通信&#xff1a;为低成本、低功耗、移动设备倾力打造 的轻量级、分布式无线移动…

英语新概念2-回译法-lesson13

The Greenwood Boys 绿林少年是一组流行歌手们。现在他们正在参观城市里的所有公园&#xff0c;他们明天就要到这。他们将坐火车到并且大多数小镇上的年轻人将要欢迎他们&#xff0c;明天晚上他们将要在工人俱乐部唱歌。绿林少年将在这待五天&#xff0c;在这期间&#xff0c;…

我独自升级崛起加速器推荐 我独自升级免费加速器

近期&#xff0c;《我独自升级》这部动画凭借爆棚的人气&#xff0c;在各大平台上掀起了一阵观看热潮&#xff0c;其影响力不容小觑。借此时机&#xff0c;韩国游戏巨头网石集团敏捷响应&#xff0c;顺势推出了同名游戏《我独自升级&#xff1a;ARISE》&#xff0c;为粉丝们搭建…

如何让vim支持python3

首先删除旧的vim。 sudo apt-get remove vim //输入re按下tab直接显示remove sudo apt-get remove vim-runtime sudo apt-get remove vim -tiny sudo apt-get remove vim-common 然后下载vim8源码&#xff1a; git clone https://github.com/vim/vim.git 进行编译安装…

鸿蒙开发全攻略:华为应用系统如何携手嵌入式技术开启新篇章~

鸿蒙操作系统是华为自主创新的成果&#xff0c;打破了传统操作系统的局限。通过结合嵌入式技术&#xff0c;鸿蒙实现了跨平台、跨设备的高度融合&#xff0c;提供了流畅、智能的体验。华为应用系统与嵌入式技术的结合&#xff0c;提升了性能&#xff0c;丰富了用户体验。鸿蒙与…

【stm-4】PWM驱动LED呼吸灯 PWM驱动舵机PWM驱动直流电机

1.PWM驱动LED呼吸灯 void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); //结构体初始化输出比较单元 void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef*…

RabbitMQ的五种模式

一、简单模式 简单模式&#xff08;Simple&#xff09;&#xff1a;一个生产者&#xff0c;一个消费者 package com.qiangesoft.rabbitmq.mode.simple;import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframe…

mysql集群cluster引擎在写入数据时报错 (1114, “The table ‘ads‘ is full“)

问题描述&#xff1a;mysql集群在写入数据时&#xff0c;出现上述报错 问题原因&#xff1a;表数据已满&#xff0c;一般是在集群的管理节点设置里面datamemory的值太小&#xff0c;当数据量超过该值时就会出现该问题 解决方案&#xff1a; 修改集群管理节点的config.ini里面…

JUC下的ScheduledThreadPoolExecutor详解

ScheduledThreadPoolExecutor是Java并发编程框架中一个强大且灵活的线程池实现&#xff0c;专为定时与周期性任务而设计。作为ThreadPoolExecutor的子类&#xff0c;它不仅继承了线程池管理的高效与灵活性&#xff0c;还内置了基于优先级队列的延迟任务调度机制&#xff0c;支持…

商务分析方法与工具(五):Python的趣味快捷-文件和文件夹操作自动化

Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢我的博客的话&#xff0c;记得…

pytest教程-41-钩子函数-pytest_runtest_teardown

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了pytest_runtest_call钩子函数的使用方法&#xff0c;本小节我们讲解一下pytest_runtest_teardown钩子函数的使用方法。 pytest_runtest_teardown 钩子函数在每个测试用例执行完成后被调用&…

游戏辅助 -- 三种分析角色坐标方法(CE、xdbg、龙龙遍历工具)

所用工具下载地址&#xff1a; https://pan.quark.cn/s/d54e7cdc55e6 在上次课程中&#xff0c;我们成功获取了人物对象的基址&#xff1a;[[[0xd75db8]1C]28]&#xff0c;而人物血量的地址则是基址再加上偏移量278。 接下来&#xff0c;我们需要执行以下步骤来进一步操作&a…

JSP技术讲解

目录 1、JSP简介 2、JSP体验 3、JSP运行原理 4、JSP基本语法 5、JSP指令 6、JSP内置九大对象 7、JSP标签 8、JSP配置 9、JSP排错 10、总结 在前面的Servlet学习中发现Servlet本质是一个java程序&#xff0c;因此Servlet更加擅长编写程序的业务逻辑&#xff0c;而如果要…

BACnet到OPC UA的楼宇自动化系统与生产执行系统(MES)整合

在智能制造的浪潮下&#xff0c;一家位于深圳的精密电子制造企业面临着前所未有的挑战&#xff1a;如何高效地将楼宇自动化系统与生产执行系统&#xff08;MES&#xff09;整合&#xff0c;实现能源管理与生产流程的精细化控制。这家企业的楼宇控制系统使用的是BACnet协议&…

牛客NC97 字符串出现次数的TopK问题【中等 哈希+优先级队列 Java/Go】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/fd711bdfa0e840b381d7e1b82183b3ee 核心 哈希&#xff0c;优先级队列Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返…
最新文章