阅读 2546

从Android进程优先级开始谈APP保活的意义

一、APP为什么保活?

        Android应用互相唤醒的情况是中国特色,国外因为有 Google Play 的评价系统和基本的审核机制,不会有国内这么疯狂的流氓式设计。这样做的好处一个是方便收集用户信息,了解用户习惯,优化产品,再者给用户推送消息需要APP退出前台时能够保活,对于IM应用来讲更是硬性需求。所以有些你没装过或者周围没人用的 APP ,随随便便都能日活上千万。

       

二、保活手段

      当前业界的Android进程保活手段主要分为 黑、白、灰 三种,其大致的实现思路如下:

黑色保活:

 不同的app进程,用广播相互唤醒(包括利用系统提供的广播进行唤醒)

场景1:开机,网络切换、拍照、拍视频时候,利用系统产生的广播唤醒app

场景2:接入第三方SDK也会唤醒相应的app进程,如微信sdk会唤醒微信,支付宝sdk会唤醒支付宝。由此发散开去,就会直接触发了下面的 场景3

场景3:假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。(只是拿阿里打个比方,其实BAT系都差不多)

没错,我们的Android手机就是一步一步的被上面这些场景给拖卡机的。

针对场景1,估计Google已经开始意识到这些问题,所以在最新的Android N取消了 ACTION_NEW_PICTURE(拍照),ACTION_NEW_VIDEO(拍视频),CONNECTIVITY_ACTION(网络切换)等三种广播。

白色保活:

       启动前台Service

白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。这样的目的也很容易理解,就是提高进程的优先级。让Linux在内存不足的时候不会优先被杀掉,从某种意义上来说这算是最不流氓的一种保活方式了。

灰色保活:

     利用系统的漏洞启动前台Service

  灰色保活,这种保活手段是应用范围最广泛。它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的

三、进程的优先级

在Android系统中,进程的优先级影响着以下三个因素:

  • 当内存紧张时,系统对于进程的回收策略
  • 系统对于进程的CPU调度策略
  • 虚拟机对于进程的内存分配和垃圾回收策略

系统对于进程的优先级有如下五个分类:

  1. 前台进程
  2. 可见进程
  3. 服务进程
  4. 后台进程
  5. 空进程

前台进程 

指正在与用户进行交互的应用进程,该进程数量较少,是最高优先级进程,系统一般不会终止该进程,而判断为前台进程的因素有以下这些 

 进程中包含处于前台的正与用户交互的activity; 

 进程中包含与前台activity绑定的service;

 进程中包含调用了startForeground()方法的service;

 进程中包含正在执行onCreate(), onStart(), 或onDestroy()方法的service;

 进程中包含正在执行onReceive()方法的BroadcastReceiver. 

可视进程

 能被用户看到,但不能根据根据用户的动作做出相应的反馈, 因素 进程中包含可见但不处于前台进程的activity(如:activity执行onPause()时处于可见状态,但并不处于前台进程中) 该进程有一个与可见/前台的activity绑定数据的service  

服务进程 

没有可见界面仍在不断的执行任务的进程,除非在可视进程和前台进程紧缺资源(如:内存资源)才会被终止 因素 包含除前台进程和可视进程的service外的service的进程 

后台进程

 通常系统中有大量的后台进程,终止后台进程不会影响用户体验,随时为优先级更高的进程腾出资源而被终止,优先回收长时间没用使用过的进程。 因素 包含不在前台或可视进程的activity的进程,也就是已经调用onStop()方法后的activity 

空进程 

为提高整体系统性能,系统会保存已经完成生命周期的应用程序 ,存在与内存当中,也就是缓存,为下次的启动更加迅速而设计。

四、内存阈值

上面是进程的分类,进程是怎么被杀的呢?系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app, 这套杀进程回收内存的机制就叫 Low Memory Killer。那这个不足怎么来规定呢,那就是内存阈值,我们可以使用cat /sys/module/lowmemorykiller/parameters/minfree来查看某个手机的内存阈值。


注意这些数字的单位是page。 1 page = 4 kb.上面的六个数字对应的就是(MB): 72,90,108,126,144,180,这些数字也就是对应的内存阀值,内存阈值在不同的手机上不一样,一旦低于该值,Android便开始按顺序关闭进程. 因此Android开始结束优先级最低的空进程,即当可用内存小于180MB(46080*4/1024)。

读到这里,你或许有一个疑问,假设现在内存不足,空进程都被杀光了,现在要杀后台进程,但是手机中后台进程很多,难道要一次性全部都清理掉?当然不是的,进程是有它的优先级的,这个优先级通过进程的adj值来反映,它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收,adj值定义在com.android.server.am.ProcessList类中,这个类路径是${android-sdk-path}\sources\android-23\com\android\server\am\ProcessList.java。

oom_score_adj

对于每一个运行中的进程,Linux内核都通过proc文件系统暴露这样一个文件来允许其他程序修改指定进程的优先级:

/proc/[pid]/oom_score_adj。(修改这个文件需要root权限)

这个文件允许的值的范围是:-1000 ~ +1000之间。值越小,表示进程越重要。

当内存非常紧张时,系统便会遍历所有进程,以确定哪个进程需要被杀死以回收内存,此时便会读取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这个文件。oom_adj的值越小,进程的优先级越高,普通进程oom_adj值是大于等于0的,而系统进程oom_adj的值是小于0的,我们可以通过cat /proc/进程id/oom_adj可以看到当前进程的adj值。



看到adj值是0,0就代表这个进程是属于前台进程,我们按下Back键,将应用至于后台,再次查看



adj值变成了8,8代表这个进程是属于不活跃的进程,你可以尝试其他情况下,oom_adj值是多少,但是每个手机的厂商可能不一样,oom_adj值主要有这么几个,可以参考一下。




lowmemorykiller机制

OOM全称Out Of Memory,是Linux当中,内存保护机制的一种。该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核将该进程杀掉。

当Kernel遇到OOM的时候,可以有2种选择:

1) 产生kernelpanic(死机)

2) 启动OOM killer,选择一个或多个“合适”的进程,干掉那些选择中的进程,从而释放内存。

在Linux中,系统就是通过算分去杀死进程的。至于分数的值,就是这3个参数的值。简单来说,系统是这样进行算分的:算分主要分2部分,一部分是系统打分,主要根据该进程的内存使用情况(oom_score),另一部分是用户大份额,就是oom_score_adj。每个进程的实际得分是综合这2个参数的值的。而oom_adj只是一个旧的接口参数,在普通的linux系统中和oom_score_adj是差不多的(但是在android中是很有用的)。

在Android中,及时用户退出当前应用程序后,应用程序还是会存在于系统当中,这是为了方便程序的再次启动。但是这样的话,随着打开的程序的数量的增加,系统的内存就会不足,从而需要杀掉一些进程来释放内存空间。至于是否需要杀进程以及杀什么进程,这个就是由Android的内部机制LowMemoryKiller机制来进行的。

Andorid的Low Memory Killer是在标准的linux lernel的OOM基础上修改而来的一种内存管理机制。当系统内存不足时,杀死不必要的进程释放其内存。不必要的进程的选择根据有2个:oom_adj和占用的内存的大小。oom_adj代表进程的优先级,数值越高,优先级月低,越容易被杀死;对应每个oom_adj都可以有一个空闲进程的阀值。Android Kernel每隔一段时间会检测当前空闲内存是否低于某个阀值。假如是,则杀死oom_adj最大的不必要的进程,如果有多个,就根据oom_score_adj去杀死进程,,直到内存恢复低于阀值的状态。

LowMemoryKiller的值的设定,主要保存在2个文件之中,分别是/sys/module/lowmemorykiller/parameters/adj与/sys/module/lowmemorykiller/parameters/minfree。adj保存着当前系统杀进程的等级,minfree则是保存着对应的阀值。他们的对应关系如下:



举个例子说明一下上表,当当前系统内存少于55296×4K(即216MB)时,Android就会找出当前oom_adj≥9的进程,根据进程的等级,先把oom_adj数值最大的进程给杀掉,释放他的内存,当他们的oom_adj相等时,就对比他们的oom_score_adj,然后oom_score_adj越大,也越容易杀掉。


在这里,也许有人会问,为什么采用LowMemoryKiller而不用OOM呢?我们来对比一下两者,就可以得出答案了。



使用LowMemoryKiller可以使系统内存较低时,调出进程管理器结束不必要的人进程释放空间。在安卓中,如果等到真正的OOM时,也许进程管理器就已经没法启动了。

lowMemorykiller的运行原理

上面其实已经说过,LowMemoryKiller是对多个内存阀值的控制来选择杀进程的。但是,这些阀值是怎样联系在一起的呢?下面就我的理解,简单说一下其运行的原理。

首先,LowMemoryKiller是随着系统的启动而启动的。当前主要的LowMemoryKiller的代码主要在\system\core\lmkd的目录下,之前的代码\kernel\drivers\staging\android\lowmemorykiller.c已经不再使用。

LowMemoryKiller在系统启动的时候就已经由init进程一并启动了。LowMemoryKiller启动就是,就会不断监测系统的运行情况和内存情况,当内存少于minfree限定的阀值的时候,lowMemoryKiller遍历当前进程的oom_score_adj,把大于对应阀值的进程进行kill操作。例如,在当前设置中,当系统内存少于315M时,系统就会自动把进程中oom_score_adj的值少于1000的杀掉,当系统内存少于216时,系统就会自动把进程中oom_score_adj的值少于529的杀掉,如此类推。

至于oom_adj和oom_score_adj是由谁去控制并写入的呢?

在系统当中,oom_adj和oom_score_adj是由ActivityManagerService去控制的,上层应用的启动都离不开AcitivityManagerService的调用与分配资源。有关oom_adj与oom_score_adj会在以后分析ActivityManagerService的时候加入相对详细的论述。在这里就不详细说明。

有关Minfree的值的写入,其实可以找到很多个地方,但是在最开始(还在用lowmemorykiller.c)的时候,是可以在lowmemorykiller.c中设置的。但是现在已经不用lowmemorykiller.c了,所以相对设置的地方也不一样了。经查找验证,Minfree的阀值控制,是由ActivictyManagerService和lowmemorykiller一并控制写入的。

五、常用的保活套路


前台服务

这种大部分人都了解,据说这个微信也用过的进程保活方案,这方案实际利用了Android前台service的漏洞。
原理如下
对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除

相互唤醒 

相互唤醒的意思就是,假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。这个完全有可能的。此外,开机,网络切换、拍照、拍视频时候,利用系统产生的广播也能唤醒app,不过Android N已经将这三种广播取消了。


粘性服务&与系统服务捆绑

这个是系统自带的,onStartCommand方法必须具有一个整形的返回值,这个整形的返回值用来告诉系统在服务启动完毕后,如果被Kill,系统将如何操作,这种方案虽然可以,但是在某些情况or某些定制ROM上可能失效

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return START_REDELIVER_INTENT;
}
复制代码
  • START_STICKY
    如果系统在onStartCommand返回后被销毁,系统将会重新创建服务并依次调用onCreate和onStartCommand(注意:根据测试Android2.3.3以下版本只会调用onCreate根本不会调用onStartCommand,Android4.0可以办到),这种相当于服务又重新启动恢复到之前的状态了)。

  • START_NOT_STICKY
    如果系统在onStartCommand返回后被销毁,如果返回该值,则在执行完onStartCommand方法后如果Service被杀掉系统将不会重启该服务。

  • START_REDELIVER_INTENT
    START_STICKY的兼容版本,不同的是其不保证服务被杀后一定能重启。               


  • JobSheduler

    是作为进程死后复活的一种手段,native进程方式最大缺点是费电, Native 进程费电的原因是感知主进程是否存活有两种实现方式,在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。其次5.0以上系统不支持。 但是JobSheduler可以替代在Android5.0以上native进程方式,这种方式即使用户强制关闭,也能被拉起来,代码如下:

      JobSheduler@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public class MyJobService extends JobService {
        @Override
        public void onCreate() {
            super.onCreate();
            startJobSheduler();
        }
    
        public void startJobSheduler() {
            try {
                JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), MyJobService.class.getName()));
                builder.setPeriodic(5);
                builder.setPersisted(true);
                JobScheduler jobScheduler = (JobScheduler) this.getSystemService(Context.JOB_SCHEDULER_SERVICE);
                jobScheduler.schedule(builder.build());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    
        @Override
        public boolean onStartJob(JobParameters jobParameters) {
            return false;
        }
    
        @Override
        public boolean onStopJob(JobParameters jobParameters) {
            return false;
        }
    }复制代码

    开启一个像素的Activity

    据说这个是手Q的进程保活方案,基本思想,系统一般是不会杀死前台进程的。所以要使得进程常驻,我们只需要在锁屏的时候在本进程开启一个Activity,为了欺骗用户,让这个Activity的大小是1像素,并且透明无切换动画,在开屏幕的时候,把这个Activity关闭掉,所以这个就需要监听系统锁屏广播. 



    文章分类
    Android
    文章标签