AndyZhuAZ

导航

Android 8.0 : 进程模型浅析(一)(仮)

前言

Android 8.0 / 8.1是目前 Android 最新的正式发行版智能手机操作系统,2017年3月21日 Google 为开发者推出了新的 Android O 首个开发者预览版,2017 Google I/O 开发者大会上发布了第二个Android O开发者预览。2017年8月22日,谷歌正式发布了Android 8.0的正式版,其正式名称为:Android Oreo(奥利奥)。2017年12月5日谷歌正式发布了Android 8.1的正式版。在最近的 Android 版本更新中,可以看得出来,Google 已经更加注重 Android 的流畅性和续航能力等性能,这个倾斜会让 Android 的用户体验变得越来越好。

据说谷歌重写的Android的底层源码,我在查看进程时就发现了一些显著的不同,当然我没有使用实机操作,不知道这个现象是否正常。以下为我日常使用的Sony Xperia Z5 Premium 搭载 Android 7.1.1 和AVD上的一个Android 8.0 的虚拟在adb shell中查看进程的不同之处。

可以看到AVD已经不再显示 root system 等USER的进程了,表头也发生了变化,sh进程NAME值为sh,而非7.0的/system/bin/sh。

Android系统架构

Android系统架构

Android 操作系统是一个软件组件的栈,在架构图中它大致可以分为五个部分和四个主要层。


Linux内核

在所有层的最底下是 Linux - 包括大约115个补丁的 Linux 3.6。它提供了基本的系统功能,比如进程管理,内存管理,设备管理(如摄像头,键盘,显示器)。同时,内核处理所有 Linux 所擅长的工作,如网络和大量的设备驱动,从而避免兼容大量外围硬件接口带来的不便。


程序库

在 Linux 内核层的上面是一系列程序库的集合,包括开源的 Web 浏览器引擎 Webkit ,知名的 libc 库,用于仓库存储和应用数据共享的 SQLite 数据库,用于播放、录制音视频的库,用于网络安全的 SSL 库等。


Android程序库

这个类别包括了专门为 Android 开发的基于 Java 的程序库。这个类别程序库的示例包括应用程序框架库,如用户界面构建,图形绘制和数据库访问。一些 Android 开发者可用的 Android 核心程序库总结如下:

  • android.app - 提供应用程序模型的访问,是所有 Android 应用程序的基石。
  • android.content - 方便应用程序之间,应用程序组件之间的内容访问,发布,消息传递。
  • android.database - 用于访问内容提供者发布的数据,包含 SQLite 数据库管理类。
  • android.opengl - OpenGL ES 3D 图片渲染 API 的 Java 接口。
  • android.os - 提供应用程序访问标注操作系统服务的能力,包括消息,系统服务和进程间通信。
  • android.text - 在设备显示上渲染和操作文本。
  • android.view - 应用程序用户界面的基础构建块。
  • android.widget - 丰富的预置用户界面组件集合,包括按钮,标签,列表,布局管理,单选按钮等。
  • android.webkit - 一系列类的集合,允许为应用程序提供内建的 Web 浏览能力。

看过了 Android 运行层内的基于 Java 的核心程序库,是时候关注一下 Android 软件栈中的基于 C/C++ 的程序库。


Android运行时

这是架构中的第三部分,自下而上的第二层。这个部分提供名为 Dalvik 虚拟机的关键组件,类似于 Java 虚拟机,但专门为 Android 设计和优化。

Dalvik 虚拟机使得可以在 Java 中使用 Linux 核心功能,如内存管理和多线程。Dalvik 虚拟机使得每一个 Android 应用程序运行在自己独立的虚拟机进程。

Android 运行时同时提供一系列核心的库来为 Android 应用程序开发者使用标准的 Java 语言来编写 Android 应用程序。


应用框架

应用框架层以 Java 类的形式为应用程序提供许多高级的服务。应用程序开发者被允许在应用中使用这些服务。

  • 活动管理者 - 控制应用程序生命周期和活动栈的所有方面。
  • 内容提供者 - 允许应用程序之间发布和分享数据。
  • 资源管理器 - 提供对非代码嵌入资源的访问,如字符串,颜色设置和用户界面布局。
  • 通知管理器 - 允许应用程序显示对话框或者通知给用户。
  • 视图系统 - 一个可扩展的视图集合,用于创建应用程序用户界面。

应用程序

顶层中有所有的 Android 应用程序。你写的应用程序也将被安装在这层。这些应用程序包括通讯录,浏览器,游戏等。

Android进程

查看Android进程

Android是基于Linux的,但是没有terminal所以我们查看Android进程需要使用adb中的adb shell进入手机,再使用ps指令来查看,如前言所示。

Android进程

当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。

决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上不再可见的 Activity 的进程。 因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。 

进程生命周期

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

Android系统共有五种等级的进程

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

  1. 前台进程

    用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

    通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

  2. 可见进程

    没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

    • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
    • 托管绑定到可见(或前台)Activity 的 Service

    可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

  3. 服务进程

    正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

  4. 后台进程

    包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

  5. 空进程

    不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。 

进程优先级的定义

OOM adjustments与 Process Importance决定了进程在调用中的优先级。

ADJ值定义在frameworks\base\services\core\java\com\android\server\am\ProcessList.java中

    // OOM adjustments for processes in various states:

    // Uninitialized value for any major or minor adj fields
    static final int INVALID_ADJ = -10000;

    // Adjustment used in certain places where we don't know it yet.
    // (Generally this is something that is going to be cached, but we
    // don't know the exact value in the cached range to assign yet.)
    static final int UNKNOWN_ADJ = 1001;

    // This is a process only hosting activities that are not visible,
    // so it can be killed without any disruption.
    static final int CACHED_APP_MAX_ADJ = 906;
    static final int CACHED_APP_MIN_ADJ = 900;

    // The B list of SERVICE_ADJ -- these are the old and decrepit
    // services that aren't as shiny and interesting as the ones in the A list.
    static final int SERVICE_B_ADJ = 800;

    // This is the process of the previous application that the user was in.
    // This process is kept above other things, because it is very common to
    // switch back to the previous app.  This is important both for recent
    // task switch (toggling between the two top recent apps) as well as normal
    // UI flow such as clicking on a URI in the e-mail app to view in the browser,
    // and then pressing back to return to e-mail.
    static final int PREVIOUS_APP_ADJ = 700;

    // This is a process holding the home application -- we want to try
    // avoiding killing it, even if it would normally be in the background,
    // because the user interacts with it so much.
    static final int HOME_APP_ADJ = 600;

    // This is a process holding an application service -- killing it will not
    // have much of an impact as far as the user is concerned.
    static final int SERVICE_ADJ = 500;

    // This is a process with a heavy-weight application.  It is in the
    // background, but we want to try to avoid killing it.  Value set in
    // system/rootdir/init.rc on startup.
    static final int HEAVY_WEIGHT_APP_ADJ = 400;

    // This is a process currently hosting a backup operation.  Killing it
    // is not entirely fatal but is generally a bad idea.
    static final int BACKUP_APP_ADJ = 300;

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 200;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 100;
    static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

    // This is the process running the current foreground app.  We'd really
    // rather not kill it!
    static final int FOREGROUND_APP_ADJ = 0;

    // This is a process that the system or a persistent process has bound to,
    // and indicated it is important.
    static final int PERSISTENT_SERVICE_ADJ = -700;

    // This is a system persistent process, such as telephony.  Definitely
    // don't want to kill it, but doing so is not completely fatal.
    static final int PERSISTENT_PROC_ADJ = -800;

    // The system process runs at the default adjustment.
    static final int SYSTEM_ADJ = -900;

    // Special code for native processes that are not being managed by the system (so
    // don't have an oom adj assigned by the system).
    static final int NATIVE_ADJ = -1000;

 

ADJ值越小则优先度越高,例如INVALID和NATIVE的ADJ值在Android 8.0中为-10000和-1000,优先度最高,系统不会去关闭它。紧随其后的是SYSTEM,ADJ值为-900。

ADJ值

类型

-10000

INVALID 未定义

-1000

NATIVE 本机持有的特殊代码
-900
SYSTEM
-800
PERSISTENT_PROC 系统持续进程(电话)
-700
PERSISTENT_SERVICE 系统服务进程
0
FOREGROUND_APP 前台应用
100
VISIBLE_APP 可见应用
200
PERCEPTIBLE_APP 后台应用
300
BACKUP_APP 备份进程
400
HEAVY_WEIGHT_APP 重量级后台进程
500
SERVICE 服务进程
600
HOME_APP 主进程
700
PREVIOUS_APP 上一个进程
800
SERVICE_B 旧的服务进程B列表
900
CACHED_APP_MIN
906
CACHED_APP_MAX
1001
UNKNOWN
99(PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1)
VISIBLE_APP_LAYER_MAX

 

Android 进程级别 和 oom_adj对应关系

 一 : 前台进程 (Active Process): oom_adj为0。

         前台进程包括 : 1 : 活动 正在前台接收用户输入  

                               2:活动、服务与广播接收器正在执行一个onReceive事件的处理函数

                               3: 服务正在运行 onStart、onCreate或onDestroy事件处理函数。

二 : 已启动服务的进程(Started Service Process) :oom_adj值为0,这类进程包含一个已启动的服务。 服务并不直接与用户输入交互,因此服务的优先级

        低于可见活动的优先级,但是,已启动服务的进程任被认为是前台进程,只有在活动以及可见活动需要资源时,已启动服务的进程才会被杀死。

三 :可见进程 (Visible Process): oom_adj 为 100。活动是可见的,但并不在前台,或者不响应用户的输入。例如,活动被非全屏或者透明的活动所遮挡。

//以下为旧版本

四 :后台进程 (Backgroud Process): oom_adj 值为 2,这类进程不包含任何可见的活动与启动的服务。通常大量后台进程存在时,系统会采用(last-seen-first-kill)后见先杀的方式,释放资源为前台进程使用。

五 :主界面 (home process): oom_adj 为 4

六 :隐藏进程 (hidden process): oom_adj为 7

七 :内容提供者 (content provider):oom_adj 为 14

八 :空进程 (Empty process):oom_adj为 15


Process Importance定义在frameworks/base/core/java/android/app/ActivityManager.java类中:

        /**
         * Constant for {@link #importance}: This process is running the
         * foreground UI; that is, it is the thing currently at the top of the screen
         * that the user is interacting with.
         */
        public static final int IMPORTANCE_FOREGROUND = 100;

        /**
         * Constant for {@link #importance}: This process is running a foreground
         * service, for example to perform music playback even while the user is
         * not immediately in the app.  This generally indicates that the process
         * is doing something the user actively cares about.
         */
        public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;

        /**
         * Constant for {@link #importance}: This process is running the foreground
         * UI, but the device is asleep so it is not visible to the user.  This means
         * the user is not really aware of the process, because they can not see or
         * interact with it, but it is quite important because it what they expect to
         * return to once unlocking the device.
         */
        public static final int IMPORTANCE_TOP_SLEEPING = 150;

        /**
         * Constant for {@link #importance}: This process is running something
         * that is actively visible to the user, though not in the immediate
         * foreground.  This may be running a window that is behind the current
         * foreground (so paused and with its state saved, not interacting with
         * the user, but visible to them to some degree); it may also be running
         * other services under the system's control that it inconsiders important.
         */
        public static final int IMPORTANCE_VISIBLE = 200;

        /**
         * Constant for {@link #importance}: {@link #IMPORTANCE_PERCEPTIBLE} had this wrong value
         * before {@link Build.VERSION_CODES#O}.  Since the {@link Build.VERSION_CODES#O} SDK,
         * the value of {@link #IMPORTANCE_PERCEPTIBLE} has been fixed.
         *
         * <p>The system will return this value instead of {@link #IMPORTANCE_PERCEPTIBLE}
         * on Android versions below {@link Build.VERSION_CODES#O}.
         *
         * <p>On Android version {@link Build.VERSION_CODES#O} and later, this value will still be
         * returned for apps with the target API level below {@link Build.VERSION_CODES#O}.
         * For apps targeting version {@link Build.VERSION_CODES#O} and later,
         * the correct value {@link #IMPORTANCE_PERCEPTIBLE} will be returned.
         */
        public static final int IMPORTANCE_PERCEPTIBLE_PRE_26 = 130;

        /**
         * Constant for {@link #importance}: This process is not something the user
         * is directly aware of, but is otherwise perceptible to them to some degree.
         */
        public static final int IMPORTANCE_PERCEPTIBLE = 230;

        /**
         * Constant for {@link #importance}: {@link #IMPORTANCE_CANT_SAVE_STATE} had
         * this wrong value
         * before {@link Build.VERSION_CODES#O}.  Since the {@link Build.VERSION_CODES#O} SDK,
         * the value of {@link #IMPORTANCE_CANT_SAVE_STATE} has been fixed.
         *
         * <p>The system will return this value instead of {@link #IMPORTANCE_CANT_SAVE_STATE}
         * on Android versions below {@link Build.VERSION_CODES#O}.
         *
         * <p>On Android version {@link Build.VERSION_CODES#O} after, this value will still be
         * returned for apps with the target API level below {@link Build.VERSION_CODES#O}.
         * For apps targeting version {@link Build.VERSION_CODES#O} and later,
         * the correct value {@link #IMPORTANCE_CANT_SAVE_STATE} will be returned.
         *
         * @hide
         */
        public static final int IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170;

        /**
         * Constant for {@link #importance}: This process is running an
         * application that can not save its state, and thus can't be killed
         * while in the background.
         * @hide
         */
        public static final int IMPORTANCE_CANT_SAVE_STATE= 270;

        /**
         * Constant for {@link #importance}: This process is contains services
         * that should remain running.  These are background services apps have
         * started, not something the user is aware of, so they may be killed by
         * the system relatively freely (though it is generally desired that they
         * stay running as long as they want to).
         */
        public static final int IMPORTANCE_SERVICE = 300;

        /**
         * Constant for {@link #importance}: This process process contains
         * cached code that is expendable, not actively running any app components
         * we care about.
         */
        public static final int IMPORTANCE_CACHED = 400;

        /**
         * @deprecated Renamed to {@link #IMPORTANCE_CACHED}.
         */
        public static final int IMPORTANCE_BACKGROUND = IMPORTANCE_CACHED;

        /**
         * Constant for {@link #importance}: This process is empty of any
         * actively running code.
         * @deprecated This value is no longer reported, use {@link #IMPORTANCE_CACHED} instead.
         */
        @Deprecated
        public static final int IMPORTANCE_EMPTY = 500;

        /**
         * Constant for {@link #importance}: This process does not exist.
         */
        public static final int IMPORTANCE_GONE = 1000;

IMPORTANCE值也是越小优先度越高。

IMPORTANCE_FOREGROUND 意味着这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程,即之前提到的前台进程。

IMPORTANCE_FOREGROUND_SERVICE前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情。

在这里我们发现了一个Android 8.0不同以往的地方新增了IMPORTANCE_CACHED = 400的定义,而后台进程IMPORTANCE_BACKGROUND值与其相同。

IMPORTANCE值 属性
100  
FOREGROUND
125  
FOREGROUND_SERVICE
130  
PERCEPTIBLE_PRE_26
170  
CANT_SAVE_STATE_PRE_26
150  
TOP_SLEEPING
200  
VISIBLE
230  
PERCEPTIBLE
270  
CANT_SAVE_STATE
300  
SERVICE
400
CACHED & BACKGROUND
500
EMPTY
1000
GONE

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

进程回收机制

这里参考自穿裤衩闯天下 的文章《Android 守护进程的实现方式》

AndroidLow Memory Killer基于LinuxOOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:

alloc_pages -> out_of_memory() -> select_bad_process() -> badness()

Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态

// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java 
// 进程不存在。 
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。 
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。 
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。 
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。 
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;

在Android 8.0 中是这样的,增加了17、18两个值四个STATE:PROCESS_STATE_CACHED_EMPTY、PROCESS_STATE_NONEXISTENT、MIN_PROCESS_STATE、MAX_PROCESS_STATE。

    /** @hide Not a real process state. */
    public static final int PROCESS_STATE_UNKNOWN = -1;

    /** @hide Process is a persistent system process. */
    public static final int PROCESS_STATE_PERSISTENT = 0;

    /** @hide Process is a persistent system process and is doing UI. */
    public static final int PROCESS_STATE_PERSISTENT_UI = 1;

    /** @hide Process is hosting the current top activities.  Note that this covers
     * all activities that are visible to the user. */
    public static final int PROCESS_STATE_TOP = 2;

    /** @hide Process is hosting a foreground service due to a system binding. */
    public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;

    /** @hide Process is hosting a foreground service. */
    public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;

    /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
    public static final int PROCESS_STATE_TOP_SLEEPING = 5;

    /** @hide Process is important to the user, and something they are aware of. */
    public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;

    /** @hide Process is important to the user, but not something they are aware of. */
    public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;

    /** @hide Process is in the background transient so we will try to keep running. */
    public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 8;

    /** @hide Process is in the background running a backup/restore operation. */
    public static final int PROCESS_STATE_BACKUP = 9;

    /** @hide Process is in the background, but it can't restore its state so we want
     * to try to avoid killing it. */
    public static final int PROCESS_STATE_HEAVY_WEIGHT = 10;

    /** @hide Process is in the background running a service.  Unlike oom_adj, this level
     * is used for both the normal running in background state and the executing
     * operations state. */
    public static final int PROCESS_STATE_SERVICE = 11;

    /** @hide Process is in the background running a receiver.   Note that from the
     * perspective of oom_adj receivers run at a higher foreground level, but for our
     * prioritization here that is not necessary and putting them below services means
     * many fewer changes in some process states as they receive broadcasts. */
    public static final int PROCESS_STATE_RECEIVER = 12;

    /** @hide Process is in the background but hosts the home activity. */
    public static final int PROCESS_STATE_HOME = 13;

    /** @hide Process is in the background but hosts the last shown activity. */
    public static final int PROCESS_STATE_LAST_ACTIVITY = 14;

    /** @hide Process is being cached for later use and contains activities. */
    public static final int PROCESS_STATE_CACHED_ACTIVITY = 15;

    /** @hide Process is being cached for later use and is a client of another cached
     * process that contains activities. */
    public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 16;

    /** @hide Process is being cached for later use and is empty. */
    public static final int PROCESS_STATE_CACHED_EMPTY = 17;

    /** @hide Process does not exist. */
    public static final int PROCESS_STATE_NONEXISTENT = 18;

    /** @hide The lowest process state number */
    public static final int MIN_PROCESS_STATE = PROCESS_STATE_PERSISTENT;

    /** @hide The highest process state number */
    public static final int MAX_PROCESS_STATE = PROCESS_STATE_NONEXISTENT;

 

Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考

/** 
 * Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
 * 这里在Android8.0中没有发生变化
 * 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
 */
public static int procStateToImportance(int procState) {
    if (procState == PROCESS_STATE_NONEXISTENT) {
        return IMPORTANCE_GONE;
    } else if (procState >= PROCESS_STATE_HOME) {
        return IMPORTANCE_BACKGROUND;
    } else if (procState >= PROCESS_STATE_SERVICE) {
        return IMPORTANCE_SERVICE;
    } else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
        return IMPORTANCE_CANT_SAVE_STATE;
    } else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
        return IMPORTANCE_PERCEPTIBLE;
    } else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
        return IMPORTANCE_VISIBLE;
    } else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
        return IMPORTANCE_TOP_SLEEPING;
    } else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
        return IMPORTANCE_FOREGROUND_SERVICE;
    } else {
        return IMPORTANCE_FOREGROUND;
    }
}

一般情况下,设备端进程被干掉有一下几种情况

进程结束场景结束方式影响范围
Android 系统自身内存回收机制 Low Memory Killer Process State 数值从大到小
第三方管理程序清理进程 无 Root 权限 killBackgroundProcess Process State 数值大于6进程
第三方管理程序清理进程 有 Root 权限 force-stop or Kill 除当前前台进程外所有非系统进程
Rom 清除进程(用户手动清理) force-stop or Kill 所有非系统进程
用户手动强制结束 force-stop 第三方应用以及非 System 进程

 

 

 

 

Android进程状态转换

这里参考了一篇CSDN的文章

 

Android应用进程启动

当我们在Launcher桌面上点击一个应用的图标时,当然Launcher启动之后,已经把桌面上每个图标对应的信息都封装好了,用户点击之后,Launcher进程就会通过Binder进程间通信机制调用startActivity的方式去打开目标进程的入口Activity,指令传达到ActivityManagerService当中时,AMS会去检测当有的应用进程是否已经启动,如果没有启动,那么就会先将当前的目标进程启动起来,启动目标进程是通过调用startProcessLocked方法来完成的。

方法的实现在ActivityManagerService类中,目录路径为frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java,startProcessLocked方法的源码如下:

private final void startProcessLocked(ProcessRecord app,  
                                      String hostingType, String hostingNameStr) {  
    startProcessLocked(app, hostingType, hostingNameStr, null /* abiOverride */,  
            null /* entryPoint */, null /* entryPointArgs */);  
}  

该方法就是直接调用另一个重载方法来实现在,该重载方法的源码如下:

private final void startProcessLocked(ProcessRecord app, String hostingType,  
                                          String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {  
        long startTime = SystemClock.elapsedRealtime();  
        if (app.pid > 0 && app.pid != MY_PID) {  
            checkTime(startTime, "startProcess: removing from pids map");  
            synchronized (mPidsSelfLocked) {  
                mPidsSelfLocked.remove(app.pid);  
                mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);  
            }  
            checkTime(startTime, "startProcess: done removing from pids map");  
            app.setPid(0);  
        }  
  
        if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES,  
                "startProcessLocked removing on hold: " + app);  
        mProcessesOnHold.remove(app);  
  
        checkTime(startTime, "startProcess: starting to update cpu stats");  
        updateCpuStats();  
        checkTime(startTime, "startProcess: done updating cpu stats");  
  
        try {  
            try {  
                final int userId = UserHandle.getUserId(app.uid);  
                AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);  
            } catch (RemoteException e) {  
                throw e.rethrowAsRuntimeException();  
            }  
  
            int uid = app.uid;  
            int[] gids = null;  
            int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;  
            if (!app.isolated) {  
                int[] permGids = null;  
                try {  
                    checkTime(startTime, "startProcess: getting gids from package manager");  
                    final IPackageManager pm = AppGlobals.getPackageManager();  
                    permGids = pm.getPackageGids(app.info.packageName,  
                            MATCH_DEBUG_TRIAGED_MISSING, app.userId);  
                    StorageManagerInternal storageManagerInternal = LocalServices.getService(  
                            StorageManagerInternal.class);  
                    mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,  
                            app.info.packageName);  
                } catch (RemoteException e) {  
                    throw e.rethrowAsRuntimeException();  
                }  
  
                /* 
                 * Add shared application and profile GIDs so applications can share some 
                 * resources like shared libraries and access user-wide resources 
                 */  
                if (ArrayUtils.isEmpty(permGids)) {  
                    gids = new int[3];  
                } else {  
                    gids = new int[permGids.length + 3];  
                    System.arraycopy(permGids, 0, gids, 3, permGids.length);  
                }  
                gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));  
                gids[1] = UserHandle.getCacheAppGid(UserHandle.getAppId(uid));  
                gids[2] = UserHandle.getUserGid(UserHandle.getUserId(uid));  
            }  
            checkTime(startTime, "startProcess: building args");  
            if (mFactoryTest != FactoryTest.FACTORY_TEST_OFF) {  
                if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL  
                        && mTopComponent != null  
                        && app.processName.equals(mTopComponent.getPackageName())) {  
                    uid = 0;  
                }  
                if (mFactoryTest == FactoryTest.FACTORY_TEST_HIGH_LEVEL  
                        && (app.info.flags & ApplicationInfo.FLAG_FACTORY_TEST) != 0) {  
                    uid = 0;  
                }  
            }  
            int debugFlags = 0;  
            if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {  
                debugFlags |= Zygote.DEBUG_ENABLE_JDWP;  
                debugFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;  
                // Also turn on CheckJNI for debuggable apps. It's quite  
                // awkward to turn on otherwise.  
                debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;  
            }  
            // Run the app in safe mode if its manifest requests so or the  
            // system is booted in safe mode.  
            if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 ||  
                    mSafeMode == true) {  
                debugFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;  
            }  
            if ("1".equals(SystemProperties.get("debug.checkjni"))) {  
                debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;  
            }  
            String genDebugInfoProperty = SystemProperties.get("debug.generate-debug-info");  
            if ("true".equals(genDebugInfoProperty)) {  
                debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;  
            }  
            if ("1".equals(SystemProperties.get("debug.jni.logging"))) {  
                debugFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;  
            }  
            if ("1".equals(SystemProperties.get("debug.assert"))) {  
                debugFlags |= Zygote.DEBUG_ENABLE_ASSERT;  
            }  
            if (mNativeDebuggingApp != null && mNativeDebuggingApp.equals(app.processName)) {  
                // Enable all debug flags required by the native debugger.  
                debugFlags |= Zygote.DEBUG_ALWAYS_JIT;          // Don't interpret anything  
                debugFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO; // Generate debug info  
                debugFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;   // Disbale optimizations  
                mNativeDebuggingApp = null;  
            }  
  
            String invokeWith = null;  
            if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {  
                // Debuggable apps may include a wrapper script with their library directory.  
                String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";  
                StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();  
                try {  
                    if (new File(wrapperFileName).exists()) {  
                        invokeWith = "/system/bin/logwrapper " + wrapperFileName;  
                    }  
                } finally {  
                    StrictMode.setThreadPolicy(oldPolicy);  
                }  
            }  
  
            String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;  
            if (requiredAbi == null) {  
                requiredAbi = Build.SUPPORTED_ABIS[0];  
            }  
  
            String instructionSet = null;  
            if (app.info.primaryCpuAbi != null) {  
                instructionSet = VMRuntime.getInstructionSet(app.info.primaryCpuAbi);  
            }  
  
            app.gids = gids;  
            app.requiredAbi = requiredAbi;  
            app.instructionSet = instructionSet;  
  
            // the per-user SELinux context must be set  
            if (TextUtils.isEmpty(app.info.seInfoUser)) {  
                Slog.wtf(TAG, "SELinux tag not defined",  
                        new IllegalStateException("SELinux tag not defined for "  
                                + app.info.packageName + " (uid " + app.uid + ")"));  
            }  
            final String seInfo = app.info.seInfo  
                    + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);  
            // Start the process.  It will either succeed and return a result containing  
            // the PID of the new process, or else throw a RuntimeException.  
            boolean isActivityProcess = (entryPoint == null);  
            if (entryPoint == null) entryPoint = "android.app.ActivityThread";  
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +  
                    app.processName);  
            checkTime(startTime, "startProcess: asking zygote to start proc");  
            ProcessStartResult startResult;  
            if (hostingType.equals("webview_service")) {  
                startResult = startWebView(entryPoint,  
                        app.processName, uid, uid, gids, debugFlags, mountExternal,  
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,  
                        app.info.dataDir, null, entryPointArgs);  
            } else {  
                startResult = Process.start(entryPoint,  
                        app.processName, uid, uid, gids, debugFlags, mountExternal,  
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,  
                        app.info.dataDir, invokeWith, entryPointArgs);  
            }  
            checkTime(startTime, "startProcess: returned from zygote!");  
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
  
            mBatteryStatsService.noteProcessStart(app.processName, app.info.uid);  
            checkTime(startTime, "startProcess: done updating battery stats");  
  
            EventLog.writeEvent(EventLogTags.AM_PROC_START,  
                    UserHandle.getUserId(uid), startResult.pid, uid,  
                    app.processName, hostingType,  
                    hostingNameStr != null ? hostingNameStr : "");  
  
            try {  
                AppGlobals.getPackageManager().logAppProcessStartIfNeeded(app.processName, app.uid,  
                        seInfo, app.info.sourceDir, startResult.pid);  
            } catch (RemoteException ex) {  
                // Ignore  
            }  
  
            if (app.persistent) {  
                Watchdog.getInstance().processStarted(app.processName, startResult.pid);  
            }  
  
            checkTime(startTime, "startProcess: building log message");  
            StringBuilder buf = mStringBuilder;  
            buf.setLength(0);  
            buf.append("Start proc ");  
            buf.append(startResult.pid);  
            buf.append(':');  
            buf.append(app.processName);  
            buf.append('/');  
            UserHandle.formatUid(buf, uid);  
            if (!isActivityProcess) {  
                buf.append(" [");  
                buf.append(entryPoint);  
                buf.append("]");  
            }  
            buf.append(" for ");  
            buf.append(hostingType);  
            if (hostingNameStr != null) {  
                buf.append(" ");  
                buf.append(hostingNameStr);  
            }  
            Slog.i(TAG, buf.toString());  
            app.setPid(startResult.pid);  
            app.usingWrapper = startResult.usingWrapper;  
            app.removed = false;  
            app.killed = false;  
            app.killedByAm = false;  
            checkTime(startTime, "startProcess: starting to update pids map");  
            ProcessRecord oldApp;  
            synchronized (mPidsSelfLocked) {  
                oldApp = mPidsSelfLocked.get(startResult.pid);  
            }  
            // If there is already an app occupying that pid that hasn't been cleaned up  
            if (oldApp != null && !app.isolated) {  
                // Clean up anything relating to this pid first  
                Slog.w(TAG, "Reusing pid " + startResult.pid  
                        + " while app is still mapped to it");  
                cleanUpApplicationRecordLocked(oldApp, false, false, -1,  
                        true /*replacingPid*/);  
            }  
            synchronized (mPidsSelfLocked) {  
                this.mPidsSelfLocked.put(startResult.pid, app);  
                if (isActivityProcess) {  
                    Message msg = mHandler.obtainMessage(PROC_START_TIMEOUT_MSG);  
                    msg.obj = app;  
                    mHandler.sendMessageDelayed(msg, startResult.usingWrapper  
                            ? PROC_START_TIMEOUT_WITH_WRAPPER : PROC_START_TIMEOUT);  
                }  
            }  
            checkTime(startTime, "startProcess: done updating pids map");  
        } catch (RuntimeException e) {  
            Slog.e(TAG, "Failure starting process " + app.processName, e);  
  
            // Something went very wrong while trying to start this process; one  
            // common case is when the package is frozen due to an active  
            // upgrade. To recover, clean up any active bookkeeping related to  
            // starting this process. (We already invoked this method once when  
            // the package was initially frozen through KILL_APPLICATION_MSG, so  
            // it doesn't hurt to use it again.)  
            forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), false,  
                    false, true, false, false, UserHandle.getUserId(app.userId), "start failure");  
        }  
    }  

 该方法中int uid = app.uid可以获取到目标进程的uid,uid是标识目标进程身份的一个整数,它最原始的分配是在应用进程安装时,由PackageManagerService分配好

关于Android进程模型

在安装Android应用程序的时候,Android会为每个程序分配一个Linux用户id,并设置相应的权限,这样其它应用程序就不能访问此应用程序所拥有的数据和资源了。

默认情况下,每个apk运行在它自己的Linux进程中。当需要执行应用程序中的代码时,Android会启动一个jvm,即一个新的进程来执行,因此不同的apk运行在相互隔离的环境中。

同时,开发者可以给两个应用程序分配相同的linux用户id,这样他们就能访问对方所拥有的资源。为了保留系统资源,拥有相同用户id的应用程序可以运行在同一个进程中,共享同一个jvm。

课堂演示

 

众所周知,Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。Android是基于Linux的操作系统,所以init也是Android系统中用户空间的第一个进程,它的进程号是1。下面先简单的看一下init进程的启动过程。 

 

 

Android 的线程调度

Android 进程生命周期与ADJ

Android 开发者应该都知道在系统中进程重要性的划分:

 

  • 前台进程(Foreground process)
  • 可见进程(Visible process)
  • 服务进程(Service process)
  • 后台进程(Background process)
  • 空进程(Empty process)

相信大家都很清楚,这里就不做过多的介绍了,不过对于进程重要性是通过哪些操作发生变更的,以及和我们前面讲的 Linux 进程分组又是怎么关联和映射上的,是下面要讲述的重点。

Android 进程优先级的相关概念

oom_score_adj 级别

对于每一个运行中的进程,Linux 内核都通过 proc 文件系统暴露 /proc/[pid]/oom_score_adj这样一个文件来允许其他程序修改指定进程的优先级,这个文件允许的值的范围是:-1000 ~ +1001之间。值越小,表示进程越重要。当内存非常紧张时,系统便会遍历所有进程,以确定哪个进程需要被杀死以回收内存,此时便会读取 oom_score_adj 这个文件的值。

PS:在Linux 2.6.36之前的版本中,Linux 提供调整优先级的文件是 /proc/[pid]/oom_adj 。这个文件允许的值的范围是-17 ~ +15之间。数值越小表示进程越重要。 这个文件在新版的 Linux 中已经废弃。但你仍然可以使用这个文件,当你修改这个文件的时候,内核会直接进行换算,将结果反映到 oom_score_adj 这个文件上。 

Android早期版本的实现中也是依赖 oom_adj 这个文件。但是在新版本中,已经切换到使用 oom_score_adj 这个文件。

为了便于管理,ProcessList.java中预定义了 oom_score_adj 的可能取值,这里的预定义值也是对应用进程的一种分类。

 

 

Lowmemorykiller 根据当前可用内存情况来进行进程释放,总设计了6个级别,即上表中“解释列”加粗的行,即 Lowmemorykiller 的杀进程的6档,如下:

 

  • CACHED_APP_MAX_ADJ
  • CACHED_APP_MIN_ADJ
  • BACKUP_APP_ADJ
  • PERCEPTIBLE_APP_ADJ
  • VISIBLE_APP_ADJ
  • FOREGROUND_APP_ADJ

系统内存从很宽裕到不足,Lowmemorykiller 也会相应地从 CACHED_APP_MAX_ADJ (第1档)开始杀进程,如果内存还不足,那么会杀 CACHED_APP_MIN_ADJ (第2档),不断深入,直到满足内存阈值条件。

ProcessRecord中下面这些属性反应了 oom_score_adj 的值:

int maxAdj;                 // Maximum OOM adjustment for this process
int curRawAdj;              // Current OOM unlimited adjustment for this process
int setRawAdj;              // Last set OOM unlimited adjustment for this process
int curAdj;                 // Current OOM adjustment for this process
int setAdj;                 // Last set OOM adjustment for this process
int verifiedAdj;            // The last adjustment that was verified as actually being set

Process State

对应的在 ActivityManager 重定义了 process_state 级别的划分,Android 系统会在修改进程状态的同时更新 oom_score_adj 的分级:

 

 

在 ProcessRecord 中,记录了和进程状态相关的属性:

int curProcState = PROCESS_STATE_NONEXISTENT; // Currently computed process state
int repProcState = PROCESS_STATE_NONEXISTENT; // Last reported process state
int setProcState = PROCESS_STATE_NONEXISTENT; // Last set process state in process tracker
int pssProcState = PROCESS_STATE_NONEXISTENT; // Currently requesting pss for

Schedule Group

对应到底层进程分组,除了上面提到的 Process.java 定义的不同线程组的定义,同时还为 Activity manager 定义了一套类似的调度分组,和之前的线程分组定义也存在对应关系:

// Activity manager's version of Process.THREAD_GROUP_BG_NONINTERACTIVE
static final int SCHED_GROUP_BACKGROUND = 0;
// Activity manager's version of Process.THREAD_GROUP_DEFAULT
static final int SCHED_GROUP_DEFAULT = 1;
// Activity manager's version of Process.THREAD_GROUP_TOP_APP
static final int SCHED_GROUP_TOP_APP = 2;
// Activity manager's version of Process.THREAD_GROUP_TOP_APP
// Disambiguate between actual top app and processes bound to the top app
static final int SCHED_GROUP_TOP_APP_BOUND = 3;

在 ProcessRecord 中,也记录了和调度组相关的属性:

int curSchedGroup;          // Currently desired scheduling class
int setSchedGroup;          // Last set to background scheduling class

Android 进程优先级的变化

我们知道影响 Android 应用进程优先级变化的是根据 Android 
应用组件的生命周期变化相关。Android进程调度之adj算法 里面罗列了所有会触发进程状态发生变化的事件,主要包括:

Actvity

 

  • ActivityStackSupervisor.realStartActivityLocked: 启动Activity
  • ActivityStack.resumeTopActivityInnerLocked: 恢复栈顶Activity
  • ActivityStack.finishCurrentActivityLocked: 结束当前Activity
  • ActivityStack.destroyActivityLocked: 摧毁当前Activity

Service

位于ActiveServices.java

 

  • realStartServiceLocked: 启动服务
  • bindServiceLocked: 绑定服务(只更新当前app)
  • unbindServiceLocked: 解绑服务 (只更新当前app)
  • bringDownServiceLocked: 结束服务 (只更新当前app)
  • sendServiceArgsLocked: 在bringup或则cleanup服务过程调用 (只更新当前app)

broadcast

 

  • BQ.processNextBroadcast: 处理下一个广播
  • BQ.processCurBroadcastLocked: 处理当前广播
  • BQ.deliverToRegisteredReceiverLocked: 分发已注册的广播 (只更新当前app)

ContentProvider

 

  • AMS.removeContentProvider: 移除provider
  • AMS.publishContentProviders: 发布provider (只更新当前app)
  • AMS.getContentProviderImpl: 获取provider (只更新当前app)

Process

位于 ActivityManagerService.java

 

  • setSystemProcess: 创建并设置系统进程
  • addAppLocked: 创建persistent进程
  • attachApplicationLocked: 进程创建后attach到system_server的过程;
  • trimApplications: 清除没有使用app
  • appDiedLocked: 进程死亡
  • killAllBackgroundProcesses: 杀死所有后台进程.即(ADJ>9或removed=true的普通进程)
  • killPackageProcessesLocked: 以包名的形式 杀掉相关进程;

这些事件都会直接或间接调用到 ActivityManagerService.java 中的 updateOomAdjLocked 方法来更新进程的优先级,updateOomAdjLocked 先通过 computeOomAdjLocked 方法负责计算进程的优先级,再通过调用 applyOomAdjLocked 应用进程的优先级。

computeOomAdjLocked

computeOomAdjLocked 方法负责计算进程的优先级,总计约700行,执行流程比较清晰,步骤如下,由于代码有点多这里就不贴了,想仔细研究的可以比着系统源码看:

 

posted on 2018-05-01 21:17  AndyZhuAZ  阅读(3394)  评论(0编辑  收藏  举报