Android Jetpack组件架构:ViewModel的原理
Android Jetpack组件架构:ViewModel的原理
导言
本篇文章是关于介绍ViewModel的,由于ViewModel的使用还是挺简单的,这里就不再介绍其的基本应用,我们主要来分析ViewModel的原理。
ViewModel的生命周期
众所周知,一般使用ViewModel是用来解决两个问题的,第一个就是关于设备配置发生改变时Activity先前状态的保存,在ViewModel出来之前我们一般会使用saveInstanceState
这个Bundle来进行状态的保存,但是这样做能存储的数据是有限的,结构也不够明确,ViewModel作为一个生命周期大于Activity的组件就可以帮我们实现状态的存储,下面是ViewModel生命周期与Activity对比的图:
可以看到直到Activity被完全Destory时ViewModel中的数据才会被清除。
我们使用ViewModel的第二个原因就是用来实现MVVM架构,可以通过DataBinding组件和ViewModel组件以及LiveData组件一起实现MVVM架构,这样可以减轻Activity的职责,避免Activity过于臃肿。
获得ViewModel的提供者
我们从ViewModel的创建开始分析其原理,这里用我们上一篇文章的例子:
mViewModel = ViewModelProvider(this).get(SimpViewModel::class.java)
这里首先会通过ViewModelProvider
的构造方法获得一个ViewModelProvider的实例,其构造方法如下所示:
public constructor(owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))constructor(private val store: ViewModelStore,private val factory: Factory,private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
)
可以看到我们调用的是第一个构造方法,最终会调用到第二个构造方法中,也就是主构造方法中,这个构造来说就是指定了三个成员变量,分别是ViewModelStore
,Factory
和CreationExtras
三个类型的参数,我们先来分别介绍一下这三个类型。
ViewModelStore – ViewModel的拥有者
着整个类比较小,但是也是有注释的,我们先来看看注释:
这段注释中比较重要的信息就是ViewModel是真正用来存储ViewModel的类并且在configuration changes
就是配置发生改变时新的实例和旧的实例中的信息是一致的。只有当持有者不再会被recreated时里面的数据才会通过clear
清除。
接下来我们来看该类的源码:
public class ViewModelStore {private final HashMap<String, ViewModel> mMap = new HashMap<>();final void put(String key, ViewModel viewModel) {ViewModel oldViewModel = mMap.put(key, viewModel);if (oldViewModel != null) {oldViewModel.onCleared();}}final ViewModel get(String key) {return mMap.get(key);}Set<String> keys() {return new HashSet<>(mMap.keySet());}public final void clear() {for (ViewModel vm : mMap.values()) {vm.clear();}mMap.clear();}
}
可以看到具体是用一个哈希表来存储viewModel
的实例的,里面唯一比较大的方法就是put
方法,里面做的处理就是将替换出来的旧的viewModel实例给清理掉。
Factory – ViewModel的创建工厂
这个Factory是一个定义在ViewModelProvider的内部接口,它的主要职责是用来初始化ViewModel,说实话就是一个工厂。我们来看其定义:
public interface Factory {public fun <T : ViewModel> create(modelClass: Class<T>): T {throw UnsupportedOperationException("Factory.create(String) is unsupported. This Factory requires " +"`CreationExtras` to be passed into `create` method.")}public fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =create(modelClass)companion object {@JvmStaticfun from(vararg initializers: ViewModelInitializer<*>): Factory =InitializerViewModelFactory(*initializers)}}
不过这里是一个抽象的,我们来找一个具体的,也就是defaultFactory
方法获取的工厂:
public companion object {internal fun defaultFactory(owner: ViewModelStoreOwner): Factory =if (owner is HasDefaultViewModelProviderFactory)owner.defaultViewModelProviderFactory else instance......
}
这个方法的逻辑就是判断ViewModel的持有者是不是有默认的工厂方法,如果有的话就获取持有者的默认工厂,否则返回的是自身的instance
实例,至于这个instance
实例是在NewInstanceFactory
这个实现了Factory接口的工厂类中定义的伴生变量,具体逻辑是:
@JvmStatic
public val instance: NewInstanceFactory@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)get() {if (sInstance == null) {sInstance = NewInstanceFactory()}return sInstance!!}
可以看到instance
是一个静态的单例,他具体指向的还是这个NewInstanceFactory
类,至于它是如何初始化/创建ViewModel的实例的我们可以看一眼它的create
方法:
override fun <T : ViewModel> create(modelClass: Class<T>): T {return try {modelClass.newInstance()} catch (e: InstantiationException) {throw RuntimeException("Cannot create an instance of $modelClass", e)} catch (e: IllegalAccessException) {throw RuntimeException("Cannot create an instance of $modelClass", e)}
}
这个传入的modelClass
就是我们传入的.class文件:
ViewModelProvider(this).get(SimpViewModel::class.java)
所以可以看到,这个默认情况下的工厂就是直接用反射生成了对应ViewModel的实例。
CreationExtras – 构建ViewModel时的额外参数
首先我们来看一看注释的内容:
简单来说它就是为工厂生成实例的时候提供额外参数的,这些参数使用一个Map来存储的了,不过默认情况下我们并不需要实现额外的工厂,所以这个类型我们先略过。
获得ViewModel的实例
前面我们已经知道了通过构造ViewModelProvider我们可以获得其Factory了,接下来继续向下看:
ViewModelProvider(this).get(SimpViewModel::class.java)
我们看一看get
方法做了什么:
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {val canonicalName = modelClass.canonicalName?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")return get("$DEFAULT_KEY:$canonicalName", modelClass)}public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {val viewModel = store[key]if (modelClass.isInstance(viewModel)) {(factory as? OnRequeryFactory)?.onRequery(viewModel)return viewModel as T} else {@Suppress("ControlFlowWithEmptyBody")if (viewModel != null) {// TODO: log a warning.}}val extras = MutableCreationExtras(defaultCreationExtras)extras[VIEW_MODEL_KEY] = keyreturn try {factory.create(modelClass, extras)} catch (e: AbstractMethodError) {factory.create(modelClass)}.also { store.put(key, it) }}
可以看到第一个方法会调用第二个方法,其中第一个方法向第二个方法传入的的第一个String类型的参数是通过DEFAULT_KEY
和我们传入的类的类名拼接而成的。然后跳转到第二个方法之中去,首先会尝试从ViewModleStroe中获取对应Key对应的ViewModel,但是一般第一次创建时应该会为null
,所以之后跳转的应该是最后return块
中的factory.create
方法之中,这个方法我们在之前Factory的介绍中提到过了,具体就是通过反射实例化ViewModel的,并且最后将其放入到ViewModelStore对象之中去。
至于多次获取同一个ViewModel实例是会跳转到:
if (modelClass.isInstance(viewModel)) {(factory as? OnRequeryFactory)?.onRequery(viewModel)return viewModel as T}
这一段中去,这里onRequery
默认是无实现的,也就是说并不会对viewModel做任何的处理。
ViewModelStore在哪里被创建
既然ViewModel是被存储在ViewModelStore之中的,那ViewModelStore究竟是在哪里被创建出来的呢?我们可以在ComponentActivity
之中找到答案:
public ViewModelStore getViewModelStore() {if (getApplication() == null) {throw new IllegalStateException("Your activity is not yet attached to the "+ "Application instance. You can't request ViewModel before onCreate call.");}ensureViewModelStore();return mViewModelStore;
}void ensureViewModelStore() {if (mViewModelStore == null) {NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance(); //获得上次配置更改的相关参数if (nc != null) { //当存在上次的参数时// Restore the ViewModelStore from NonConfigurationInstancesmViewModelStore = nc.viewModelStore; //恢复上次的参数}if (mViewModelStore == null) {mViewModelStore = new ViewModelStore(); //创建一个新的ViewModelStore}}
}
可以看到这里在获得ViewModelStore时主要是通过一个NonConfigurationInstances
,而该参数是一个静态的对象,也就是说,它是一个单例,这样就保证了Activity在整个生命周期之中只有一个ViewModelStore实例,从而实现配置改变时也可以恢复数据的作用。
Activity的默认工厂
在看ComponentActivity的源码时,发现了原来Activity也是有默认工厂的,它的具体实现如下:
constructor(application: Application?, owner: SavedStateRegistryOwner, defaultArgs: Bundle?) {savedStateRegistry = owner.savedStateRegistrylifecycle = owner.lifecyclethis.defaultArgs = defaultArgsthis.application = applicationfactory = if (application != null) getInstance(application)else ViewModelProvider.AndroidViewModelFactory()}
这个方法最终设置到的工厂类都是一个名为AndroidViewModelFactory
的工厂类:
public open class AndroidViewModelFactoryprivate constructor(private val application: Application?,// parameter to avoid clash between constructors with nullable and non-nullable// Application@Suppress("UNUSED_PARAMETER") unused: Int,) : NewInstanceFactory() {@Suppress("SingletonConstructor")public constructor() : this(null, 0)@Suppress("SingletonConstructor")public constructor(application: Application) : this(application, 0)@Suppress("DocumentExceptions")override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {return if (application != null) {create(modelClass)} else {val application = extras[APPLICATION_KEY]if (application != null) {create(modelClass, application)} else {// For AndroidViewModels, CreationExtras must have an application setif (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {throw IllegalArgumentException("CreationExtras must have an application by `APPLICATION_KEY`")}super.create(modelClass)}}}@Suppress("DocumentExceptions")override fun <T : ViewModel> create(modelClass: Class<T>): T {return if (application == null) {throw UnsupportedOperationException("AndroidViewModelFactory constructed " +"with empty constructor works only with " +"create(modelClass: Class<T>, extras: CreationExtras).")} else {create(modelClass, application)}}@Suppress("DocumentExceptions")private fun <T : ViewModel> create(modelClass: Class<T>, app: Application): T {return if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {//如果传入的Class类型实现的接口和AndroidViewModel::class一致,及说明也有生命周期,调用构造犯法并且传入application对象try {modelClass.getConstructor(Application::class.java).newInstance(app)} catch (e: NoSuchMethodException) {throw RuntimeException("Cannot create an instance of $modelClass", e)} catch (e: IllegalAccessException) {throw RuntimeException("Cannot create an instance of $modelClass", e)} catch (e: InstantiationException) {throw RuntimeException("Cannot create an instance of $modelClass", e)} catch (e: InvocationTargetException) {throw RuntimeException("Cannot create an instance of $modelClass", e)}} else super.create(modelClass)}public companion object {internal fun defaultFactory(owner: ViewModelStoreOwner): Factory =if (owner is HasDefaultViewModelProviderFactory)owner.defaultViewModelProviderFactory else instanceinternal const val DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"private var sInstance: AndroidViewModelFactory? = null@JvmStaticpublic fun getInstance(application: Application): AndroidViewModelFactory {if (sInstance == null) {sInstance = AndroidViewModelFactory(application)}return sInstance!!}private object ApplicationKeyImpl : Key<Application>@JvmFieldval APPLICATION_KEY: Key<Application> = ApplicationKeyImpl}}
具体通过getInstance方法
就跳转到了这里,可以发现似乎工厂类都是一个单例的模式,这个工厂的特殊之处就是他的create
方法涉及到了Application对象的传入,比如说这里的newInstance
方法:
modelClass.getConstructor(Application::class.java).newInstance(app)public T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException
{if (serializationClass == null) {return newInstance0(initargs);} else {return (T) newInstanceFromSerialization(serializationCtor, serializationClass);}
}
也就是说整个被传入Application的生命周期内都只有一个实例,这样由于创建的实例在生命周期范围内的单例性和ViewModelStore的单例性,整个ViewModel就可以实现在整个Activity的生命周期内(发生意外,比如说配置改变时)数据不变更的作用。
相关文章:
Android Jetpack组件架构:ViewModel的原理
Android Jetpack组件架构:ViewModel的原理 导言 本篇文章是关于介绍ViewModel的,由于ViewModel的使用还是挺简单的,这里就不再介绍其的基本应用,我们主要来分析ViewModel的原理。 ViewModel的生命周期 众所周知,一般…...
数据分析(python)学习笔记1.0
《利用Python进行数据分析》(原书第2版) 《利用Python进行数据分析》(原书第2版) 《利用Python进行数据分析》(原书第2版) 社区和会议 除了网络搜索,科学、数据相关的Python邮件列表对于解决问题也非常有帮助。可以看看下列邮件列表: pydata:与数据分析和pandas相…...
SW免安装的toolbox只读问题
把SOLIDWORKSDATA 整体复制到另外的目录,然后这里设置目录位置。不然原始位置有只读属性...
nodejs在pdf中绘制表格
需求 之前我已经了解过如何在pdf模板中填写字段了 nodejs根据pdf模板填入中文数据并生成新的pdf文件https://blog.csdn.net/ArmadaDK/article/details/132456324 但是当我具体使用的时候,我发现我的模板里面有表格,表格的长度是不固定的,所…...
使用不同尺寸的传感器拍照时,怎么保证拍出同样视场范围的照片?
1、问题背景 使用竞品机做图像效果对比时,我们通常都会要求拍摄的照片要视场范围一致,这样才具有可比性。之前我会考虑用同样焦距、同样分辨率的设备去拍照对比就可以了,觉得相机的视场范围只由镜头焦距来决定。 但如果对于不同尺寸的传感器…...
01-工具篇-windows与linux文件共享
一般来说绝大部分PC上装的系统均是windows,为了开发linux程序,会在PC上安装一个Vmware的虚拟机,在虚拟机上安装ubuntu18.04,由于windows上的代码查看软件、浏览器,通信软件更全,我们想只用ubuntu进行编译&a…...
医疗实施-住院流程详解
住院就诊流程详解 1.病人入院登记2.病人进入病区3.医生操作病人4.医嘱录入与审核执行5. 医嘱收费前在对应业务系统的操作5.1.药物医嘱5.2.检查检验医嘱5.3.手术医嘱 6.住院医嘱费用的产生7. 医嘱收费后在对应业务系统的操作8. 病人出院 这篇文章是基于我的文章《医疗实施-住院就…...
本地连接服务器 jupyter notebook
本地连接服务器 jupyter notebook 一、前提工作二、服务器操作三、Windows 操作 一、前提工作 准备一台Linux云服务器新建一个用户,并切换到此用户安装 Anaconda 二、服务器操作 远程服务器上安装和配置 Jupyter Notebook: pip3 install jupyter接着…...
Android 使用Kotlin封装RecyclerView
文章目录 1.概述2.运行效果图3.代码实现3.1 扩展RecyclerView 3.2 扩展Adapter3.3 RecyclerView装饰绘制3.3.1 以图片实现分割线3.3.2 画网格线3.3.3空白的分割线3.3.4 不同方向上的分割线 3.4 使用方法 1.概述 在一个开源项目上看到了一个Android Kotlin版的RecyclerView封装…...
WPF 实现点击按钮跳转页面功能
方法1. 配置环境 首先添加prism依赖项,配置好所有文件。需要配置的有两个文件:App.xaml.cs和App.xaml App.xaml.cs using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows;namespace PrismDemo {/// <summa…...
关于http网络通信数据包封装的过程
当我们谈论网络通信时,数据在从源到目的地传输的过程中会通过多层网络协议。在每一层,都会添加一些头信息(和有时尾信息)来帮助处理和传输数据。这个过程被称为"封装"(Encapsulation)。简单来说&…...
关于RabbitMQ你了解多少?
关于RabbitMQ你了解多少? 文章目录 关于RabbitMQ你了解多少?基础篇同步和异步MQ技术选型介绍和安装数据隔离SpringAMQP快速入门Work queues交换机Fanout交换机Direct交换机Topic交换机 声明队列和交换机MQ消息转换器 高级篇消息可靠性问题发送者的可靠性…...
Vulkan-着色器及编译SPIR-V
1.着色器模块介绍 Vulkan着色器代码一定要用字节码格式,而不是人类可读的语法如GLSL和HLSL。这个字节码就是SPIR-V,设计用于Vulkan和OpenCL。这是一个可以用于编写图形和计算着色器的格式,但是我们主要关注的是Vulkan的图形管线。使用字节码格…...
从MVC到DDD,该如何下手重构?
作者:付政委 博客:bugstack.cn 沉淀、分享、成长,让自己和他人都能有所收获!😄 大家好,我是技术UP主小傅哥。多年的 DDD 应用,使我开了技术的眼界! MVC 旧工程腐化严重,…...
论文阅读:基于隐马尔可夫模型的蛋白质多序列比对方法研究
本文来自chatpaper Basic Information: • Title: Research on Protein Multiple Sequence Alignment Method Based on Hidden Markov Model (基于隐马尔可夫模型的蛋白质多序列比对方法研究) • Authors: Zhan Qing • Affiliation: Harbin Institute of Technology (哈尔滨工…...
Vim同时打开多个文件
分屏模式 在 Vim 中,可以同时打开多个文件并使用分屏模式来查看它们。以下是一些常见的方法和命令: 在启动 Vim 时打开多个文件 使用 -o 选项打开文件并水平分屏: vim -o file1.txt file2.txt使用 -O 选项打开文件并垂直分屏: v…...
SpringCloudStreamkafka接收jsonarray字符串失败
文章目录 场景现象问题处理 场景现象 kafka作为消息队列,作为前端设备数据到后端消费的渠道,也被多个不同微服务消费一个服务与前端边缘计算设备建立socket消息,接收实时交通事件推送,再将事件发送到kafka里面。此处使用的是Spri…...
面向对象特性分析大全集
面向对象特性分析 先进行专栏介绍 面向对象总析前提小知识分类浅析封装浅析继承浅析多态面向对象编程优点abc 核心思想实际应用总结 封装概念详解关键主要目的核心思想优点12 缺点12 Java代码实现封装特性 继承概念详解语法示例关键主要目的核心思想优点12 缺点12 Java代码实现…...
【数据结构】队列和栈
大家中秋节快乐,玩了好几天没有学习,今天分享的是栈以及队列的相关知识,以及栈和队列相关的面试题 1.栈 1.1栈的概念及结构 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作…...
WordPress主题开发( 十)之—— 条件标签函数(上)
这里写目录标题 什么是条件标签函数?条件标签函数的使用场景使用条件标签函数的注意事项常用的条件标签函数主页示例:is_front_page() 示例:管理后台is_admin() 示例:单个文章页面is_single() 示例:is_single(17) 示例:is_single(Hello World) 示例:is_single(hello…...
vue学习-10vue整合SpringBoot跨域请求
在Vue.js应用整合Spring Boot后端时,需要处理跨域请求。跨域请求通常发生在前端应用运行在不同的域名或端口上时,而后端服务运行在不同的域名或端口上。以下是一种处理跨域请求的常见方式: 后端(Spring Boot)配置 在…...
ElasticSearch - 基于 JavaRestClient 查询文档(match、精确、复合查询,以及排序、分页、高亮)
目录 一、基于 JavaRestClient 查询文档 1.1、查询 API 演示 1.1.1、查询基本框架 DSL 请求的对应格式 响应的解析 1.1.2、全文检索查询 1.1.3、精确查询 1.1.4、复合查询 1.1.5、排序和分页 1.1.6、高亮 一、基于 JavaRestClient 查询文档 1.1、查询 API 演示 1.1.…...
简易实现通讯录(2.0)
这篇文章是在上期实现的通讯录基础上,增加了自动增容的功能,也解决了一开始通讯录自动开辟一个空间,可能会浪费空间,或者是信息过多无法增容的痛点,由于我们使用的是malloc这类函数来开辟空间,我们也需要来…...
Jasypt 实现自定义加解密
如下文章已经讲解了, Jasypt 是什么,怎么集成 Jasypt,怎么使用 Jasypt。 Jasypt 开源加密库使用教程_jasyptstringencryptor-CSDN博客Jasypt 加密框架概述1、Jasypt Spring Boot 为 spring boot 应用程序中的属性源提供加密支持,…...
Leetcode 554. 砖墙
文章目录 题目代码(9.25 首刷自解) 题目 Leetcode 554. 砖墙 代码(9.25 首刷自解) class Solution { public:int leastBricks(vector<vector<int>>& wall) {unordered_map<int, int> mp;int count 0;for…...
Python 内置函数详解 (3) 进制转换
近期在外旅游,本篇是出发前定时发布的,不完整,旅游回来后再补充。 Python 内置函数 Python3.11共有75个内置函数,其来历和分类请参考:Python 新版本有75个内置函数,你不会不知道吧_Hann Yang的博客-CSDN博客 函数列表 abs aiter all …...
SPSS列联表分析
前言: 本专栏参考教材为《SPSS22.0从入门到精通》,由于软件版本原因,部分内容有所改变,为适应软件版本的变化,特此创作此专栏便于大家学习。本专栏使用软件为:SPSS25.0 本专栏所有的数据文件可在个人主页—…...
聊聊并发编程——并发容器和阻塞队列
目录 一.ConcurrentHashMap 1.为什么要使用ConcurrentHashMap? 2.ConcurrentHashMap的类图 3.ConcurrentHashMap的结构图 二.阻塞队列 Java中的7个阻塞队列 ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。 LinkedBlockingQueue…...
我庄严承诺终生不去承德旅游
虽然人微言轻,但也要尽一份力。 在此,我庄严承诺: 如果承德相关机构不返还那名"灵活就业人员"105.82万元的财产,并进行公开道歉。 我将终生不去承德旅游, 我将终生不买承德出产的任何产品。 我还将劝诫我…...
【python】python实现杨辉三角的三种方法
文章目录 1.杨辉三角介绍:2.方法一:迭代3.方法二:生成器4.方法三:递归 1.杨辉三角介绍: 杨辉三角是一种数学图形,由数字排列成类似三角形的形状。它的每个数值等于它上方两个数值之和。这个三角形的形状可以…...
h5网站怎么做的/百度一下官方网
如果我们将镜像推送到了Docker注册中心,可以从任何想要的机器上使用 docker run 命令来恢复并运行该容器。如果将镜像打包成tar包备份到了本地,只需要拷贝或移动该镜像到我们想要的机器上,加载该镜像并运行需要的容器即可。1.备份容器首先&am…...
如何重装wordpress/关键词列表
智能名片识别系统源码 一、名片识别产品介绍 1、精准识别名片信息,准确率高; 2、创建电子名片随时随地快速交换名片; 3、关联通讯录识别出来的名片信息,可以直接保存到手机本地的通讯里面; 4、关联地图识别出名片中的地…...
广西南宁网站推广/网络推广方案模板
(转载)http://hi.baidu.com/dujiaopeng/item/b87de8a952956e9d151073b6 在数据库中,UNION和UNION ALL关键字都是将两个结果集合并为一个,但这两者从使用和效率上来说都有所不同。 (1)MySQL中的UNION UNION在…...
小型企业网站排名前十/2345软件为什么没人管
一.PDU解析 1.Signal command and response headers PDU ID1).AVDTP_DISCOVER 2).AVDTP_GET_CAPABILITIES 3).AVDTP_SET_CONFIGURATION 4).AVDTP_OPEN 5).AVDTP_START 2.media packet format 相应的空中包解析:二.流程...
网络平台贷款逾期不还的后果/优化seo教程
OpenGL实现视频的剪裁、旋转、水印、滤镜?? 将 ffmpeg 库移植到 Android 平台? 在播放网络上的视频流时,Android原生的MediaPlayer支持两种协议,HTTP和RTSP,这两种协议最大的不同是,RTSP协议支…...
如何删除在凡科上做的网站/网站排名英文
概述 tcpdump语法图解 常用过滤规则 其实就是ip,端口过滤。 基于ip地址过滤(host/dst/src) 案例1:捕获特定主机192.168.0.108的主机收到的和发出的所有的数据包。 tcpdump host 192.168.0.108只捕获源ip、目的ip为host的报文。 如果你非常明确哪个主…...