android进程常驻、保活研究


1、产品需求

一说到进程常驻,立马就有很多人开始吐槽,什么流氓软件啊,什么流氓技术啊之类的。但是技术不分好坏,只有看做产品的人怎么使用了(但一般情况是一个牛逼的程序拥有着一群牛逼的技术,却被一个流氓产品驱动着)。
我们经常会遇到一些必须保证进程常驻的需求,比如聊天软件要时刻监听着是否其他人发消息;跑步软件,总不能一直点亮屏幕;个性闹钟,也许一不小心用户就杀死了进程,第二天,睡到11点未响拿起手机,看看以为是晚上11点,继续睡了。所以进程常驻技术有时也是一个不可或缺的技术。常见代表,QQ,微信,360等等。

2、资源管理机制

Android 之所以采用特殊的资源管理机制,原因在于其设计之初就是面向移动终端,所有可用的内存仅限于系统 RAM,必须针对这种限制设计相应的优化方案。当 Android 应用程序退出时,并不清理其所占用的内存,Linux 内核进程也相应的继续存在,所谓“退出但不关闭”。从而使得用户调用程序时能够在第一时间得到响应。 在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app,这套杀进程回收内存的机制就叫 Low Memory Killer  ,它是基于Linux内核的  OOM Killer 机制诞生。

既然说道内存回收机制,我们再来说说进程的优先级。
Android 基于进程中运行的组件及其状态规定了默认的五个回收优先级:
1.前台进程 (Foreground process)
2.可见进程 (Visible process)
3.服务进程 (Service process)
4.后台进程 (Background process)
5.空进程 (Empty process)
前台进程的重要性最高,依次递减,空进程重要性最低,更容易被回收。参考文档如下:

前台进程 —— Foreground process
用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。
A. 拥有用户正在交互的 Activity(已调用  onResume()
B. 拥有某个 Service,后者绑定到用户正在交互的 Activity
C. 拥有正在“前台”运行的 Service(服务已调用  startForeground()
D. 拥有正执行一个生命周期回调的 Service( onCreate() onStart()  或  onDestroy()
E. 拥有正执行其  onReceive()  方法的 BroadcastReceiver
可见进程 —— Visible process
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
A. 拥有不在前台、但仍对用户可见的 Activity(已调用  onPause() )。
B. 拥有绑定到可见(或前台)Activity 的 Service
服务进程 —— Service process
尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
A. 正在运行  startService()  方法启动的服务,且不属于上述两个更高类别进程的进程。
后台进程 —— Background process
后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU 列表中,以确保包含用户最近查看的  Activity  的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
A. 对用户不可见的 Activity 的进程(已调用  Activity的onStop()  方法)
空进程 —— Empty process
保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
A. 不含任何活动应用组件的进程

再科普一下 oom_adj 。什么是oom_adj?它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收。对于oom_adj的作用,你只需要记住以下几点即可:
  • 进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收
  • 普通app进程的oom_adj>=0,系统进程的oom_adj才可能<0
如何查看进程的oom_adj呢,我们可以使用如下的两个命令
ps | grep 包名 //获取你指定的进程信息主要是为了获取进程id
查看指定进程信息
cat /proc/进程ID/oom_adj
根据进程id查看oom_adj
com.tencent.mm是腾讯微信的包名,我们可以看到无论是微信还是他的push进程,oom_adj的值都是1,当然在我手机上,我以为它是关闭的。

oom_adj值的意义如下:
源码位置: / frameworks / base / services / core / java / com / android / server / am / ProcessList.java
/**
 * Activity manager code dealing with processes.
 */
final class ProcessList {
    ...
    // OOM adjustments for processes in various states:

    // 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 = 16;

    // 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 = 15;
    static final int CACHED_APP_MIN_ADJ = 9;

    // 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 = 8;

    // 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 = 7;

    // 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 = 6;

    // 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 = 5;

    // 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 = 4;

    // 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 = 3;

    // 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 = 2;

    // 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 = 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 = -11;

    // 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 = -12;

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

    // 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 = -17;
    ... 
}
借用腾讯张兴华同学的一张图:
oom_adj值
其中红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在 Lowmemorykiller 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

3、进程死亡情况分析

Android 手机中进程被杀死可能有如下情况:
进程死亡的情况
综上,可以得出减少进程被杀死概率无非就是想办法提高进程优先级,减少进程在内存不足等情况下被杀死的概率。

4、进程保活的手段

进程保活无非是两种方式:
  • 优化进程的oom_adj的值,确保进程更高的优先级,从而达到不会被Low Memory Killer 回收。
  • 进程被杀死之后,进行拉活。

4.1 提升进程优先级

4.1.1 利用Activity提升权限

设计思想
监听手机的锁屏事件,在屏幕锁屏的时候,启动一个透明的1像素的Activity,再监听用户解锁事件,在解锁的时候,销毁这个1像素的Activity。通过这个方案用户无感知,且能将进程在锁屏的期间的优先级由4或6提升到优先级0。

适用范围
适用场景:主要用在第三方应用以及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。
适用版本:所有版本

代码实现
首先要在MainActivity中注册黑屏和解锁事件的广播。这里注意了黑屏的广播只能在代码进行注册。清单文件注册的话,黑屏和亮屏的广播是接收不到的。底层增加了判断,Intent.ACTION_USER_PRESENT清单文件里注册时可以接受的。
MainActivity.java
MainActivity注册黑屏和解锁广播
在广播接收的地方判断,保活Activity的启动与销毁时机,代码如下:
广播接收判断Activity的启动和销毁时机
接下来就要看看我们的1像素的Activity定义了:
1像素的Activity定义
在清单文件里面设置其透明,且不在最近程序( RecentTask )中显示。
1像素Activity在清单文件里面的声明
styles定义透明没title的theme
到这里已经可以了,只有在应用开启的时候才能有作用哦。代码在KeepLiveActivity工程中,文章最后提供下载地址。

方案结果
锁屏的时候查看应用的oom_adj:
KeepLiveActivity的oom_adj的结果

4.1.2 利用前台Service提升权限

设计思想
在Android中Service的oom_adj的值为4,通过startForeground设置为前台Service,oom_adj的值就可以从4上升到1。仅低于正在交互的进程。
这个方案看上去很完美,但是却有一个严重的问题,在Android2.3之后,调用startForeground将后台进程设置为前台进程的,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。对于不需要常驻通知栏的应用来说,该方案虽好,但却是用户感知的,无法直接使用。

在这里我们可以通过一个灰色技术来解决这个问题,android的一个bug。 发送两个具有相同 ID 的 Notification。我们再结束后一个Service,这时Notification就会消失不见,但是我们通过查看进程中的Service,发现前一个Service依然存在,且也是前台Service。再查看oom_adj依然是1。

适用范围
目前所知所有版本。

代码实现
这个方案的主要实现代码都是在KeepLiveService里面,通过开启一个内部的Service并设置同一个id的Notification,并关闭内部service来隐藏通知栏,代码如下:
KeepLiveService内部实现
至此,这部分代码也差不多了,该部分代码在KeepLiveService工程中。

方案结果
最后隐藏的前台Service方案结果如何呢?截图如下:
KeepLiveService方案结果

更多
我们可以使用如下命令查看Services的使用细节 
adb shell dumpsys activity services 包名
查看Services命令
其中有一个isForeground = true代表这个Service是前台线程。我们再来看看别的一些应用哈。
qq的services

qq的service的信息

4.2 进程拉活
4.2.1 通过系统广播拉活进程
设计思想
当发生一些特定的系统事件时,系统会发出相应的广播,在清单文件中静态注册对应的广播接收者,即可在发生相应时间时进行拉活。
适用范围
1)在一些深度定制的厂商,手机在关闭情况下接受到系统广播。如小米。
2)在3.1之后,系统的package manager增加了对处于“stopped state”应用的管理。指的是安装后从来没有启动过和被用户手动强制停止的应用,与此同时系统增加了2个Flag:FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES ,来标识一个intent是否激活处于“stopped state”的应用。当2个Flag都不设置或者都进行设置的时候,采用的是FLAG_INCLUDE_STOPPED_PACKAGES的效果。
Android 3.1开始,系统底层向所有Intent的广播添加了FLAG_EXCLUDE_STOPPED_PACKAGES标志。
3)系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。
因此,该方案主要作为备用手段。

4.2.2 通过第三方应用的广播拉活
设计思想
通过反编译第三方知名的应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK。找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。
这和上一个方法的区别是,这里面的广播往往设置FLAG_INCLUDE_STOPPED_PACKAGES。
适用范围
除了和系统广播方案一样的一些制约以外,还有一些其他因素:
  • 反编译分析的第三方应用的多少,和用户是否安装。
  • 第三方应用的广播的频率,以及后续版本是否还继续存在等各种因素。

4.2.3 守护进程相互拉活
设计思想
当你的多款应用被使用的时候,你可以让两个进程不停的通过广播来相互拉起。这类方法在BAT类公司经常使用。
适用范围
当你一个进程被杀死后你可以拉起,但多个进程同时被杀死就没有办法。而且还受拉起方法约束,比如通过广播进行拉活,但是小米强制没有开启的进程不能收到广播。

4.2.4 利用Service的机制拉活
设计思想
将Service设置为START_STICKY,利用系统机制在Service挂掉后自动拉活的原理。
适用范围
如下情况无法拉活:
  • Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,则系统不再拉起。
  • 进程被取得Root权限的管理工具或者系统工具通过forestop停止掉,无法重启。
代码实现
只需将onStartCommand()函数的返回值设为Service.START_STICKY即可,如下:
利用START_STICKY来重启Service

4.2.5 利用JobScheduler机制拉活
设计思想
系统在Android5.0以上版本提供了JobScheduler接口,系统会定时调用该进程以使应用进行一些逻辑操作。
适用范围
适用于Android5.0版本以上,7.0以下的手机。且不受forestop影响,被强制停止的应用依然可以拉活。在该这些版本范围内,暂只发现小米手机出现无法拉活。
代码实现
首先定义一个类KeepLiveService继承JobService,在代码onStartJob()函数中执行你的操作:
JobScheduler中service实现
在清单文件里注册,注意要添加属性权限:
声明即添加权限
KeepLiveService定义好,接下来就是要看怎么调用JobService了,首先和其他Service一样开启Service,然后再启动JobScheduler拉活。
JobScheduler中mainActivity的实现

方案结果
经各种机型测试结果如下:
三星Galaxy s6 android6.0.1系统 拉活成功
LG nexus5x android7.0系统 拉活失败
华为 G7-TL00 android4.4.4系统 api不支持 拉活失败(可以自己封装JobScheduler兼容5.0以下版本)
华为 nexus6P android6.0.1系统 拉活成功
小米4 android6.0.1系统 不成功

4.2.6 利用账号同步机制拉活
设计思想
android系统的账号同步机制会定期进行同步账号,利用这个机制进行拉活。
适用范围
  • 据说使用于Android的所有版本,但是本人小米手机不可以进行拉活。(小米手机:只有在应用开启的时候,才会执行同步的函数。)
  • 各大厂商的修改,导致有些手机账号同步功能关闭的,需要手动打开。(小米只有腾讯产品是默认打开的。)
  • SyncAdapter时间进度不高,往往手机处于休眠状态,时间往后调整,同步间隔最低为1分钟。还有一些产品为半小时。
代码实现
账号同步api如何使用,这里就不赘述了。大家可以自行百度一下,或参考之后的代码。这里主要说一下部分重要的代码。
在Authenticator.java的addAccount()中直接添加账号,会导致手机卡主,不知道是否是没有调用底层的onActivityForResult()还是什么原因。所以我还是按照官方的示例,在addAccount()跳转到登陆界面。如下:
addAccount代码
这里将跳转的intent包含在bundle传递底层。接下来我在AuthenticatorActivity中看一下添加账号和设置同步周期的代码。
AuthenticatorActivity
最后执行同步会执行SyncAdapter.java里面的OnPerformSync()函数里面的代码。如下:
OnPerformSync
最后再放上在清单文件里面注册的代码:
清单文件注册
在清单文件里面注意添加对应的权限,之前就是参考其他人blog而没注意权限,导致耽误了很长时间没有调试出来。
至此账户同步拉活也说完了,代码在文章后面链接的KeepLiveAccount工程中。

4.2.7 利用Native进程拉活(网上,暂没实现)
设计思想
主要思想:
利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。

主要原理:
在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。而且,通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 的管理。


方案实现挑战
挑战一:在 Native 进程中如何感知主进程死亡。
要在 Native 进程中感知主进程是否存活有两种实现方式:
  • 在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,档主进程不存活时进行拉活。该方案的很大缺点是不停的轮询执行判断逻辑,非常耗电。
  • 在主进程中创建一个监控文件,并且在主进程中持有文件锁。在拉活进程启动后申请文件锁将会被堵塞,一旦可以成功获取到锁,说明主进程挂掉,即可进行拉活。由于 Android 中的应用都运行于虚拟机之上,Java 层的文件锁与 Linux 层的文件锁是不同的,要实现该功能需要封装 Linux 层的文件锁供上层调用。
封装 Linux 文件锁的代码如下:
封装Linux文件锁
Native 层中堵塞申请文件锁的部分代码:
堵塞申请文件锁的部分代码

挑战二:在 Native 进程中如何拉活主进程。
通过 Native 进程拉活主进程的部分代码如下,即通过 am 命令进行拉活。通过指定“–include-stopped-packages”参数来拉活主进程处于 forestop 状态的情况。
通过am命令进行拉活

挑战三:如何保证 Native 进程的唯一。
从可扩展性和进程唯一等多方面考虑,将 Native 进程设计层 C/S 结构模式,主进程与 Native 进程通过 Localsocket 进行通信。在Native进程中利用 Localsocket 保证 Native 进程的唯一性,不至于出现创建多个 Native 进程以及 Native 进程变成僵尸进程等问题。
保证native进程唯一

适用范围
该方案主要适用于 Android5.0 以下版本手机。该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。
对于Android5.0以上手机,系统虽然会将native进程内的所有进程都杀死,这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,如果可以跑的比系统逻辑快,依然可以有效拉起。记得网上有人做过实验,该结论是成立的,在某些 Android 5.0 以上机型有效。

5、其他的拉活方式

还可以通过应用内的push来拉活应用:
  • 国外版本:接入Google的GCM。
  • 国内版本:根据手机不通,小米手机接入小米推送,华为手机接入华为推送;其他手机可以尝试JPush。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值