android启动后怎么查看其里面的进程和线程

android启动后怎么查看其里面的进程和线程,第1张

Android中进程与进程、线程与线程之间如何通信?

1)一个 Android 程序开始运行时,会单独启动一个Process。

默认情况下,所有这个程序中的Activity或者Service都会跑在这个Process。

默认情况下,一个Android程序也只有一个Process,但一个Process下却可以有许多个Thread。

2)一个 Android 程序开始运行时,就有一个主线程Main Thread被创建。该线程主要负责UI界面的显示、更新和控件交互,所以又叫UI Thread。

3)一个Android程序创建之初,一个Process呈现的是单线程模型--即MainThread,所有的任务都在一个线程中运行,所以,MainThread所调用的每一个函数,其耗时应该越短越好,而对于比较耗时的工作,应该交给子线程去做,以避免主线程(UI线程)被阻塞,导致程序出现ANR(Application not response)

一个Activity就运行在一个线程中吗?或者编码时,如果不是明确安排在不同线程中的两个Activity,其就都是在同一个线程中?那从一个Activity跳转到另一个Activity时,是不是跳出的那个Activity就处在睡眠状态了?

答 每个Activity都有一个Process属性,可以指定该Activity是属于哪个进程的。当然如果不明确指明,应该就是从属于默认进程(Application指定的,如其未指定,应该就是默认主进程)。

Android中有Task的概念,而同一个Task的各个Activity会形成一个栈,只有站定的Activity才有机会与用户交互。

原文地址:Android中的进程与线程 原文作者:江鹏

当应用程序的组件第一次运行时,Android将启动一个只有一个执行线程的Linux进程。默认,应用程序所有的组件运行在这个进程和线程中。然而,你可以安排组件运行在其他进程中,且你可以为进程衍生出其它线程。本文从下面几点来介绍Android的进程与线程:

1、进程

组件运行于哪个进程中由清单文件控制。组件元素——<activity>、<service>、<receiver>、<provider>,都有一个process属性可以指定组件运行在哪个进程中。这个属性可以设置为每个组件运行在自己的进程中,或者某些组件共享一个进程而其他的不共享。他们还可以设置为不同应用程序的组件运行在同一个进程中——假设这些应用程序共享同一个Linux用户ID且被分配了同样的权限。<application>元素也有process属性,为所有的组件设置一个默认值。

所有的组件都在特定进程的主线程中实例化,且系统调用组件是由主线程派遣。不会为每个实例创建单独的线程,因此,对应这些调用的方法——诸如ViewonKeyDown()报告用用户的行为和生命周期通知,总是运行在进程的主线程中。这意味着,没有组件当被系统调用时应该执行很长时间或阻塞 *** 作(如网络 *** 作或循环计算),因为这将阻塞进程中的其它组件。你可以为长 *** 作衍生独立的线程。

public boolean onKeyDown(int keyCode,KeyEvent event):默认实现KeyEventCallbackonKeyMultiple(),当按下视图的KEYCODE_DPAD_CENTER或KEYCODE_ENTER然后释放时执行,如果视图可用且可点击。

参数

keyCode-表示按钮被按下的键码,来自KeyEvent

event-定义了按钮动作的KeyEvent对象

返回值

如果你处理事件,返回true;如果你想下一个接收者处理事件,返回false。

当内存剩余较小且其它进程请求较大内存并需要立即分配,Android要回收某些进程,进程中的应用程序组件会被销毁。当他们再次运行时,会重新开始一个进程。

当决定终结哪个进程时,Android会权衡他们对用户重要性的相对权值。例如,与运行在屏幕可见的活动进程相比(前台进程),它更容易关闭一个进程,它的活动在屏幕是不可见(后台进程)。决定是否终结进程,取决于运行在进程中的组件状态。关于组件的状态,将在后面一篇——组件生命周期中介绍。

2、线程

虽然你可能会将你的应用程序限制在一个进程中,但有时候你会需要衍生一个线程做一些后台工作。因为用户界面必须很快地响应用户的 *** 作,所以活动寄宿的线程不应该做一些耗时的 *** 作如网络下载。任何不可能在短时间完成的 *** 作应该分配到别的线程。

线程在代码中是用标准的Java线程对象创建的,Android提供了一些方便的类来管理线程——Looper用于在线程中运行消息循环、Handler用户处理消息、HandlerThread用户设置一个消息循环的线程。

Looper类

该类用户在线程中运行消息循环。线程默认没有消息循环,可以在线程中调用prepare()创建一个运行循环;然后调用loop()处理消息直到循环结束。大部分消息循环交互是通过Handler类。下面是一个典型的执行一个Looper线程的例子,分别使用prepare()和loop()创建一个初始的Handler与Looper交互:

1 Android中进程与进程、线程与线程之间如何通信?

1)一个 Android 程序开始运行时,会单独启动一个Process。

默认情况下,所有这个程序中的Activity或者Service都会跑在这个Process。

默认情况下,一个Android程序也只有一个Process,但一个Process下却可以有许多个Thread。

2)一个 Android 程序开始运行时,就有一个主线程Main Thread被创建。该线程主要负责UI界面的显示、更新和控件交互,所以又叫UI Thread。

3)一个Android程序创建之初,一个Process呈现的是单线程模型--即MainThread,所有的任务都在一个线程中运行,所以,MainThread所调用的每一个函数,其耗时应该越短越好,而对于比较耗时的工作,应该交给子线程去做,以避免主线程(UI线程)被阻塞,导致程序出现ANR(Application not response)

一个Activity就运行在一个线程中吗?或者编码时,如果不是明确安排在不同线程中的两个Activity,其就都是在同一个线程中?那从一个Activity跳转到另一个Activity时,是不是跳出的那个Activity就处在睡眠状态了?

答 每个Activity都有一个Process属性,可以指定该Activity是属于哪个进程的。当然如果不明确指明,应该就是从属于默认进程(Application指定的,如其未指定,应该就是默认主进程)。

Android中有Task的概念,而同一个Task的各个Activity会形成一个栈,只有站定的Activity才有机会与用户交互。

原文地址:Android中的进程与线程 原文作者:江鹏

当应用程序的组件第一次运行时,Android将启动一个只有一个执行线程的Linux进程。默认,应用程序所有的组件运行在这个进程和线程中。然而,你可以安排组件运行在其他进程中,且你可以为进程衍生出其它线程。本文从下面几点来介绍Android的进程与线程:

1、进程

组件运行于哪个进程中由清单文件控制。组件元素——<activity>、<service>、<receiver>、<provider>,都有一个process属性可以指定组件运行在哪个进程中。这个属性可以设置为每个组件运行在自己的进程中,或者某些组件共享一个进程而其他的不共享。他们还可以设置为不同应用程序的组件运行在同一个进程中——假设这些应用程序共享同一个Linux用户ID且被分配了同样的权限。<application>元素也有process属性,为所有的组件设置一个默认值。

所有的组件都在特定进程的主线程中实例化,且系统调用组件是由主线程派遣。不会为每个实例创建单独的线程,因此,对应这些调用的方法——诸如ViewonKeyDown()报告用用户的行为和生命周期通知,总是运行在进程的主线程中。这意味着,没有组件当被系统调用时应该执行很长时间或阻塞 *** 作(如网络 *** 作或循环计算),因为这将阻塞进程中的其它组件。你可以为长 *** 作衍生独立的线程。

public boolean onKeyDown(int keyCode,KeyEvent event):默认实现KeyEventCallbackonKeyMultiple(),当按下视图的KEYCODE_DPAD_CENTER或KEYCODE_ENTER然后释放时执行,如果视图可用且可点击。

参数

keyCode-表示按钮被按下的键码,来自KeyEvent

event-定义了按钮动作的KeyEvent对象

返回值

如果你处理事件,返回true;如果你想下一个接收者处理事件,返回false。

当内存剩余较小且其它进程请求较大内存并需要立即分配,Android要回收某些进程,进程中的应用程序组件会被销毁。当他们再次运行时,会重新开始一个进程。

当决定终结哪个进程时,Android会权衡他们对用户重要性的相对权值。例如,与运行在屏幕可见的活动进程相比(前台进程),它更容易关闭一个进程,它的活动在屏幕是不可见(后台进程)。决定是否终结进程,取决于运行在进程中的组件状态。关于组件的状态,将在后面一篇——组件生命周期中介绍。

2、线程

虽然你可能会将你的应用程序限制在一个进程中,但有时候你会需要衍生一个线程做一些后台工作。因为用户界面必须很快地响应用户的 *** 作,所以活动寄宿的线程不应该做一些耗时的 *** 作如网络下载。任何不可能在短时间完成的 *** 作应该分配到别的线程。

线程在代码中是用标准的Java线程对象创建的,Android提供了一些方便的类来管理线程——Looper用于在线程中运行消息循环、Handler用户处理消息、HandlerThread用户设置一个消息循环的线程。

Looper类

该类用户在线程中运行消息循环。线程默认没有消息循环,可以在线程中调用prepare()创建一个运行循环;然后调用loop()处理消息直到循环结束。大部分消息循环交互是通过Handler类。下面是一个典型的执行一个Looper线程的例子,分别使用prepare()和loop()创建一个初始的Handler与Looper交互:

21、远程过程调用(Remote procedure calls,RPCs)

Android有一个轻量级的远程过程调用机制——方法在本地调用却在远程(另外一个进程中)执行,结果返回给调用者。这需要将方法调用和它伴随的数据分解为 *** 作系统能够理解的层次,从本地进程和地址空间传输到远程进程和地址空间,并重新组装调用。返回值以相反方向传输。Android提供了做这些工作的所有代码,这样我们可以专注于定义和执行RPC接口本身。

一个RPC接口仅包含方法。所有的方法同步地执行(本地方法阻塞直到远程方法执行完成),即使是没有返回值。简言之,该机制工作原理如下:首先,你用简单的IDL(interface definition language,接口定义语言)声明一个你想实现的RPC接口。从这个声明中,aidl工具生成一个Java接口定义,提供给本地和远程进程。它包含两个内部类,如下图所示:

内部类有管理你用IDL定义的接口的远程过程调用所需要的所有代码。这两个内部类都实现了IBinder接口。其中之一就是在本地由系统内部使用,你写代码可以忽略它。另外一个是Stub,扩展自Binder类。除了用于有效地IPC(interprocess communication)调用的内部代码,内部类在RPC接口声明中还包含方法声明。你可以定义Stub的子类实现这些方法,如图中所示。

通常情况下,远程过程有一个服务管理(因为服务能通知系统关于进程和它连接的其它进程的信息)。它有由aidl工具生成的接口文件和Stub子类实现的RPC方法。服务的客户端仅有由aidl工具生成的接口文件。

下面介绍服务如何与它的客户端建立连接:

· 服务的客户端(在本地端的)应该实现onServiceConnected() 和onServiceDisconnected() 方法,因此当与远程服务建立连接成功和断开连接是会通知它。然后调用bindService() 建立连接。

· 服务的onBind()方法将实现为接受或拒绝连接,者取决于它接受到的意图(该意图传送到binServive())。如果连接被接受,它返回一个Stub子类的实例。

· 如果服务接受连接,Android调用客户端的onServiceConnected()方法且传递给它一个IBinder对象,返回由服务管理的Stub子类的一个代理。通过代理,客户端可以调用远程服务。

这里只是简单地描述,省略了一些RPC机制的细节。你可以查阅相关资料或继续关注Android开发之旅,后面将为你奉上。

22、线程安全方法

在一些情况下,你实现的方法可能会被不止一个线程调用,因此必须写成线程安全的。这对远程调用方法是正确的——如上一节讨论的RPC机制。当从IBinder进程中调用一个IBinder对象中实现的一个方法,这个方法在调用者的线程中执行。然而,当从别的进程中调用,方法将在Android维护的IBinder进程中的线程池中选择一个执行,它不在进程的主线程中执行。例如,一个服务的onBind()方法在服务进程的主线程中被调用,在onBind()返回的对象中执行的方法(例如,实现RPC方法的Stub子类)将在线程池中被调用。由于服务可以有一个以上的客户端,所以同时可以有一个以上的线程在执行同一个IBinder方法。因此,IBinder的方法必须是线程安全的。

同样,一个内容提供者可以接受其它进程产生的数据请求。虽然ContentResolver 和 ContentProvider 类隐藏进程通信如何管理的,对应哪些请求的ContentResolver 方法——query()、insert()、delete()、update()、getType(),在内容提供者的进程的线程池中被调用,而不是在这一进程的主线程中。因为这些方法可以同时从任意数量的线程中调用,他们也必须实现为线程安全的。

一般我们想要执行耗时 *** 作都会想开启Thread子线程去处理,但是多次创建和销毁线程是很耗系统资源的。为了解决这一问题,我们可以自己构建一个循环线程,在线程当中创建一个Looper轮循器,来进行消息的轮循, 当有耗时任务需要投放到该循环线程时,线程就会执行耗时任务,任务执行完成之后,线程又会处于阻塞等待状态,不会马上销毁掉,直到下一个耗时任务被投放进来;即通过阻塞和等待来保证性能最优,为此Google为我们封装好了HandlerThread框架。

HandlerThread本质上就是一个Thread子线程,不同的是在Thread内部有一个Looper开启了轮循器;

HandlerThread = Handler + Thread + Looper

由于一般的Thread没有开启Looper轮循器,所以不能在子线程中创建handler,因为没有对应的MessageQueue与handler相关联,而MessageQueue是由Looper进行维护的;如果想在子线程中创建handler,必须先通过Looperprepare()创建一个Looper,再通过Looperloop()开启循环,才能使用Handler。

1、HandlerThread本质上是一个线程类,继承了Thread;

2、HandlerThread有自己的内部Looper对象,可以进行loop循环;所以在HandlerThread中可以创建handler来发送和处理消息;

3、通过获取HandlerThread的Looper对象传递给Handler,可以在handleMessage()中执行异步 *** 作;

4、handler中的looper默认绑定了UI线程的MessageQueue,对于非UI线程想使用MessageQueue机制的,HandlerThread的内部Looper最适合,它不会干扰和阻塞UI线程,减少了对性能的消耗,但是处理效率低;

5、HandlerThread是一个串行队列,会比较稳定;

使用Looper判断,方法为: LoopermyLooper() != LoopergetMainLooper()

使用线程句柄判断,将主线程的ThreadcurrentThread()获取到主线程当前句柄,保存起来,在需要判断的时候调用ThreadcurrentThread()来与之比较,即可判断当前线程是否是主线程了

[Android]安卓在代码中实时获取控件尺寸的几种方法

本篇博客已收录到我的安卓开发小结中——点击安卓开发小结

1、已知控件大小,想知道他在不同设备上的px值。

使用TypedValueapplyDimension方法。

如果这个控件你知道了大小,但不知道在各个设备上的大小,可以使用TypedValueapplyDimension方法来获取实际大小。getResources()getDisplayMetrics()方法Activity类和View中都有提供,因此可以在Activity和自定义View中都可以使用这个方法。

int ballRadius = (int) TypedValueapplyDimension(TypedValueCOMPLEX_UNIT_DIP, BALL_RADIUS, getResources()getDisplayMetrics());

1

2

1

2

applyDimension是androidutilTypedValue类下的一个静态方法,可以看到这个方法的作用就是将value的值换算为dp、sp等对应的px值,因为在实时运行过程中很多方法接受的是px值,而不是xml中的dp值。

public static float applyDimension(int unit, float value,DisplayMetrics metrics)

{

switch (unit) {

case COMPLEX_UNIT_PX://获得value作为px值对应的px值

return value;

case COMPLEX_UNIT_DIP:

return value metricsdensity;//获得value作为dp值对应的px值

case COMPLEX_UNIT_SP:

return value metricsscaledDensity;//获得value作为sp值对应的px值

case COMPLEX_UNIT_PT:

return value metricsxdpi (10f/72);

1,先要建立一个线程获取json数据

2接着解析json数据

3,设置textview

例如:json数据,strjson= {"key": ["a","b"]}

JSONObject object = new JSONObject (strjson)

JSONArray arr= objectgetJSONArray("key") ;

String text=arrgetString(0);

Android 中线程可分为 主线程 和 子线程 两类,其中主线程也就是 UI线程 ,它的主要这作用就是运行四大组件、处理界面交互。子线程则主要是处理耗时任务,也是我们要重点分析的。

首先 Java 中的各种线程在 Android 里是通用的,Android 特有的线程形态也是基于 Java 的实现的,所以有必要先简单的了解下 Java 中的线程,本文主要包括以下内容:

在 Java 中要创建子线程可以直接继承 Thread 类,重写 run() 方法:

或者实现 Runnable 接口,然后用Thread执行Runnable,这种方式比较常用:

简单的总结下:

Callable 和 Runnable 类似,都可以用来处理具体的耗时任务逻辑的,但是但具体的差别在哪里呢?看一个小例子:

定义 MyCallable 实现了 Callable 接口,和之前 Runnable 的 run() 方法对比下, call() 方法是有返回值的哦,泛型就是返回值的类型:

一般会通过线程池来执行 Callable (线程池相关内容后边会讲到),执行结果就是一个 Future 对象:

可以看到,通过线程池执行 MyCallable 对象返回了一个 Future 对象,取出执行结果。

Future 是一个接口,从其内部的方法可以看出它提供了取消任务(有坑!!!)、判断任务是否完成、获取任务结果的功能:

Future 接口有一个 FutureTask 实现类,同时 FutureTask 也实现了 Runnable 接口,并提供了两个构造函数:

用 FutureTask 一个参数的构造函数来改造下上边的例子:

FutureTask 内部有一个 done() 方法,代表 Callable 中的任务已经结束,可以用来获取执行结果:

所以 Future + Callable 的组合可以更方便的获取子线程任务的执行结果,更好的控制任务的执行,主要的用法先说这么多了,其实 AsyncTask 内部也是类似的实现!

注意, Future 并不能取消掉运行中的任务,这点在后边的 AsyncTask 解析中有提到。

Java 中线程池的具体的实现类是 ThreadPoolExecutor ,继承了 Executor 接口,这些线程池在 Android 中也是通用的。使用线程池的好处:

常用的构造函数如下:

一个常规线程池可以按照如下方式来实现:

执行任务:

基于 ThreadPoolExecutor ,系统扩展了几类具有新特性的线程池:

线程池可以通过 execute() 、 submit() 方法开始执行任务,主要差别从方法的声明就可以看出,由于 submit() 有返回值,可以方便得到任务的执行结果:

要关闭线程池可以使用如下方法:

IntentService 是 Android 中一种特殊的 Service,可用于执行后台耗时任务,任务结束时会自动停止,由于属于系统的四大组件之一,相比一般线程具有较高的优先级,不容易被杀死。用法和普通 Service 基本一致,只需要在 onHandleIntent() 中处理耗时任务即可:

至于 HandlerThread,它是 IntentService 内部实现的重要部分,细节内容会在 IntentService 源码中说到。

IntentService 首次创建被启动的时候其生命周期方法 onCreate() 会先被调用,所以我们从这个方法开始分析:

这里出现了 HandlerThread 和 ServiceHandler 两个类,先搞明白它们的作用,以便后续的分析。

首先看 HandlerThread 的核心实现:

首先它继承了 Thread 类,可以当做子线程来使用,并在 run() 方法中创建了一个消息循环系统、开启消息循环。

ServiceHandler 是 IntentService 的内部类,继承了 Handler,具体内容后续分析:

现在回过头来看 onCreate() 方法主要是一些初始化的 *** 作, 首先创建了一个 thread 对象,并启动线程,然后用其内部的 Looper 对象 创建一个 mServiceHandler 对象,将子线程的 Looper 和 ServiceHandler 建立了绑定关系,这样就可以使用 mServiceHandler 将消息发送到子线程去处理了。

生命周期方法 onStartCommand() 方法会在 IntentService 每次被启动时调用,一般会这里处理启动 IntentService 传递 Intent 解析携带的数据:

又调用了 start() 方法:

就是用 mServiceHandler 发送了一条包含 startId 和 intent 的消息,消息的发送还是在主线程进行的,接下来消息的接收、处理就是在子线程进行的:

当接收到消息时,通过 onHandleIntent() 方法在子线程处理 intent 对象, onHandleIntent() 方法执行结束后,通过 stopSelf(msgarg1) 等待所有消息处理完毕后终止服务。

为什么消息的处理是在子线程呢?这里涉及到 Handler 的内部消息机制,简单的说,因为 ServiceHandler 使用的 Looper 对象就是在 HandlerThread 这个子线程类里创建的,并通过 Looperloop() 开启消息循环,不断从消息队列(单链表)中取出消息,并执行,截取 loop() 的部分源码:

dispatchMessage() 方法间接会调用 handleMessage() 方法,所以最终 onHandleIntent() 就在子线程中划线执行了,即 HandlerThread 的 run() 方法。

这就是 IntentService 实现的核心,通过 HandlerThread + Hanlder 把启动 IntentService 的 Intent 从主线程切换到子线程,实现让 Service 可以处理耗时任务的功能!

AsyncTask 是 Android 中轻量级的异步任务抽象类,它的内部主要由线程池以及 Handler 实现,在线程池中执行耗时任务并把结果通过 Handler 机制中转到主线程以实现UI *** 作。典型的用法如下:

从 Android30 开始,AsyncTask 默认是串行执行的:

如果需要并行执行可以这么做:

AsyncTask 的源码不多,还是比较容易理解的。根据上边的用法,可以从 execute() 方法开始我们的分析:

看到 @MainThread 注解了吗?所以 execute() 方法需要在主线程执行哦!

进而又调用了 executeOnExecutor() :

可以看到,当任务正在执行或者已经完成,如果又被执行会抛出异常!回调方法 onPreExecute() 最先被执行了。

传入的 sDefaultExecutor 参数,是一个自定义的串行线程池对象,所有任务在该线程池中排队执行:

可以看到 SerialExecutor 线程池仅用于任务的排队, THREAD_POOL_EXECUTOR 线程池才是用于执行真正的任务,就是我们线程池部分讲到的 ThreadPoolExecutor :

再回到 executeOnExecutor() 方法中,那么 execexecute(mFuture) 就是触发线程池开始执行任务的 *** 作了。

那 executeOnExecutor() 方法中的 mWorker 是什么? mFuture 是什么?答案在 AsyncTask 的构造函数中:

原来 mWorker 是一个 Callable 对象, mFuture 是一个 FutureTask 对象,继承了 Runnable 接口。所以 mWorker 的 call() 方法会在 mFuture 的 run() 方法中执行,所以 mWorker 的 call() 方法在线程池得到执行!

同时 doInBackground() 方法就在 call() 中方法,所以我们自定义的耗时任务逻辑得到执行,不就是我们第二部分讲的那一套吗!

doInBackground() 的返回值会传递给 postResult() 方法:

就是通过 Handler 将最终的耗时任务结果从子线程发送到主线程,具体的过程是这样的, getHandler() 得到的就是 AsyncTask 构造函数中初始化的 mHandler , mHander 又是通过 getMainHandler() 赋值的:

可以在看到 sHandler 是一个 InternalHandler 类对象:

所以 getHandler() 就是在得到在主线程创建的 InternalHandler 对象,所以

就可以完成耗时任务结果从子线程到主线程的切换,进而可以进行相关UI *** 作了。

当消息是 MESSAGE_POST_RESULT 时,代表任务执行完成, finish() 方法被调用:

如果任务没有被取消的话执行 onPostExecute() ,否则执行 onCancelled() 。

如果消息是 MESSAGE_POST_PROGRESS , onProgressUpdate() 方法被执行,根据之前的用法可以 onProgressUpdate() 的执行需要我们手动调用 publishProgress() 方法,就是通过 Handler 来发送进度数据:

进行中的任务如何取消呢?AsyncTask 提供了一个 cancel(boolean mayInterruptIfRunning) ,参数代表是否中断正在执行的线程任务,但是呢并不靠谱, cancel() 的方法注释中有这么一段:

大致意思就是调用 cancel() 方法后, onCancelled(Object) 回调方法会在 doInBackground() 之后被执行而 onPostExecute() 将不会被执行,同时你应该 doInBackground() 回调方法中通过 isCancelled() 来检查任务是否已取消,进而去终止任务的执行!

所以只能自己动手了:

AsyncTask 整体的实现流程就这些了,源码是最好的老师,自己跟着源码走一遍有些问题可能就豁然开朗了!

以上就是关于android启动后怎么查看其里面的进程和线程全部的内容,包括:android启动后怎么查看其里面的进程和线程、Android中的特殊线程——HandlerThread、Android中的线程怎么获取主线程等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9648020.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-30
下一篇 2023-04-30

发表评论

登录后才能评论

评论列表(0条)

保存