须要引用请注明出处:juejin.im/post/5ca5f9…android
如今不少公司和开发者但愿本身的app可以长期运行在手机内存中,由于只要该app的进程一直存在,那么咱们就能够干不少事,尽管不少不是很光彩的事,好比偷流量,费电,偷偷安装应用和推送广告信息等等。庆幸的事,道高一尺魔高一丈,android原生系统对如今手机系统作了不少的保护,如今很难保证哪个应用的进程能够一直不被杀死,咱们可以作的就是尽可能保活进程,接下来咱们就对进程保活作个总结。程序员
在正式开始介绍进程保活的知识以前,简单了解一下进程相关的一些东西。首先什么是进程,这些我相信是程序员都会清楚,进程是系统进行分配资源和调度的最小单位,很简单,每一个进程就像一个app运行在手机系统里。shell
咱们能够经过adb shell ps来查看进程的信息:缓存
id | 说明 |
---|---|
u0_a344 | 当前用户 |
9153 | pid 进程名 |
201 | ppid 父进程名 |
1597348 | VSIZE进程的虚拟内存大小 |
com.sunland.staffapp | 进程名 |
根据进程当前所处的状态,咱们能够将进程分为5类:前台进程、可见进程、服务进程、后台进程、空进程。每一种进程解释以下:bash
进程只要处在上述任意一种状态,那么该进程就是前台进程,前台进程的优先级最高,系统通常不会直接杀死前台进程,除非手机系统内存彻底耗尽。微信
可见进程也是系统中及其重要的进程,不到万不得已的状况下,系统也是不会杀死可见进程的。app
某个进程中运行着service,而且该service是经过startService启动的,与用户界面不存在交互的那种service,当内存不足以维持前台进程和可见进程的状况下,会优先杀死服务进程。ide
在程序退到后台,好比用户按了back键或者home,界面看不到了,可是程序仍在运行中,此时的activity处于onpause状态,在任务管理器中能够看到,当系统内存不足的状况下会有限杀死后台进程。布局
空进程就是不含有任何active的进程,系统保留的缘由主要是高速缓存,方便下次访问速度很快,若是系统内存不足,首先杀的就是空进程。post
进程有个参数,oom_adj,通常而言,这个参数的值越小,优先级则越高,处于前台进程的adj为0,固然各个手机厂商的可能会有一点差别,查看adj的方法以下:
通常而言,进程adj值越大,占用系统内存值越大,优先被杀死。咱们作进程保护就是从这两个方面下手。接下来就是正题了。
因为前台进程不容易被杀死,因此咱们能够试着去开启一个前台进程,而且开启前台进程不为用户所感知,1个像素点的activity就能够知足要求,咱们能够在锁屏的时候启动一个activity,这个activity只有一个像素,在开屏的时候finish掉这个activity,考虑到内存的问题,咱们能够在service中去启动这个activity,而且这个service在独立的进程中,以下实例:
/**
* foreground service for keeping alive
*/
public class KeepAliveService extends Service {
private static final String TAG = Constants.LOG_TAG;
public static final int NOTICE_ID = 100;
// 动态注册锁屏等广播
private ScreenReceiverUtil mScreenUtil;
// 1像素Activity管理类
private ScreenManager mScreenManager;
private View toucherLayout;
private WindowManager.LayoutParams params;
private WindowManager windowManager;
private ScreenReceiverUtil.ScreenStateListener mScreenStateListenerer = new ScreenReceiverUtil.ScreenStateListener() {
@Override
public void onSreenOn() {
L.d(TAG, "KeepAliveService-->finsh 1 pixel activity");
mScreenManager.finishActivity();
}
@Override
public void onSreenOff() {
L.d(TAG, "KeepAliveService-->start 1 pixel activity");
mScreenManager.startActivity();
}
@Override
public void onUserPresent() {
}
};
//不与Activity进行绑定.
@Override
public IBinder onBind(Intent intent)
{
return null;
}
@Override
public void onCreate()
{
super.onCreate();
//若是API大于18,须要弹出一个可见通知,这个可见通知在大于API 25 版本以前能够经过Cancel隐藏
// 因此在API 18 ~ API 24之间启动前台service,并隐藏Notification
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N){
startForeground(NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 若是以为常驻通知栏体验很差
// 能够经过启动CancelNoticeService,将通知移除,oom_adj值不变
Intent intent = new Intent(this,CancelNoticeService.class);
startService(intent);
}else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTICE_ID, new Notification());
}
mScreenUtil = new ScreenReceiverUtil(this);
mScreenManager = ScreenManager.getInstance(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
createFloatingWindow();
L.d(TAG, "KeepAliveService-->KeepAliveService created");
// If app process is killed system, all task scheduled by AlarmManager is canceled.
// We need reschedule all task when KeepAliveService is revived.
TaskUtil.dispatchAllTask(this);
}
@Override
public void onDestroy() {
super.onDestroy();
// 若是Service被杀死,干掉通知
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mManager.cancel(NOTICE_ID);
}
if (toucherLayout != null) {
ensureWindowManager();
try {
L.d(TAG, "KeepAliveService-->remove floating window");
windowManager.removeView(toucherLayout);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
mScreenUtil.stopScreenReceiverListener();
}
public static void startKeepAliveServiceIfNeed(Context context) {
boolean isKeepAliveServiceEnabled = SystemUtils.isComponentEnabled(context.getApplicationContext(), KeepAliveService.class);
if (isKeepAliveServiceEnabled) {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
if (!LifecycleHandler.getInstance().isForeground()) {
return;
}
}
Intent intentAlive = new Intent(context.getApplicationContext(), KeepAliveService.class);
context.startService(intentAlive);
L.d(TAG, "KeepAliveService-->start KeepAliveService");
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
}
private void createFloatingWindow()
{
// MIUI用TYPE_TOAST没法在后台显示悬浮窗,必须获取draw_over_other_app权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& OSJudgementUtil.isMIUI()
&& !Settings.canDrawOverlays(this)) {
L.d(TAG, "KeepAliveService-->MIUI needs draw overlay permission");
return;
}
//赋值WindowManager&LayoutParam.
params = new WindowManager.LayoutParams();
//设置type.系统提示型窗口,通常都在应用程序窗口之上.
params.type = WindowManager.LayoutParams.TYPE_TOAST;
// 有限使用SYSTEM_ALERT,优先级更高
if (OSJudgementUtil.isMIUI() || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& Settings.canDrawOverlays(this))) {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
//设置效果为背景透明.
params.format = PixelFormat.RGBA_8888;
//设置flags.不可聚焦及不可以使用按钮对悬浮窗进行操控.
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//设置窗口初始停靠位置.
params.gravity = Gravity.LEFT | Gravity.TOP;
params.x = 0;
params.y = 0;
//设置悬浮窗口长宽数据.
params.width = 1;
params.height = 1;
//获取浮动窗口视图所在布局.
toucherLayout = new View(this);
//toucherLayout.setBackgroundColor(0x55ffffff);
//添加toucherlayout
ensureWindowManager();
try {
L.d(TAG, "KeepAliveService-->create floating window");
windowManager.addView(toucherLayout,params);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
private void ensureWindowManager() {
if (windowManager == null) {
windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
}
}
}
复制代码
public class SinglePixelActivity extends AppCompatActivity {
private static final String TAG = Constants.LOG_TAG;
private ScreenReceiverUtil.ScreenStateListener mScreenStateListenerer = new ScreenReceiverUtil.ScreenStateListener() {
@Override
public void onSreenOn() {
if (!isFinishing()) {
finish();
}
}
@Override
public void onSreenOff() {
}
@Override
public void onUserPresent() {
if (!isFinishing()) {
finish();
}
}
};
private ScreenReceiverUtil mScreenUtil;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
L.d(TAG,"SinglePixelActivity--->onCreate");
Window mWindow = getWindow();
mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams attrParams = mWindow.getAttributes();
attrParams.x = 0;
attrParams.y = 0;
attrParams.height = 1;
attrParams.width = 1;
mWindow.setAttributes(attrParams);
// 绑定SinglePixelActivity到ScreenManager
ScreenManager.getInstance(this).setSingleActivity(this);
mScreenUtil = new ScreenReceiverUtil(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
L.d(TAG, "SinglePixelActivity onTouchEvent-->finsih()");
if (!isFinishing()) {
finish();
}
return false;
}
@Override
protected void onDestroy() {
super.onDestroy();
L.d(TAG,"SinglePixelActivity-->onDestroy()");
if (mScreenUtil != null) {
mScreenUtil.stopScreenReceiverListener();
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
if (!LifecycleHandler.getInstance().isForeground()) {
return;
}
}
try {
Intent intentAlive = new Intent(this, KeepAliveService.class);
startService(intentAlive);
} catch (Exception e) {
L.e(TAG, e == null ? "" : e.getMessage());
}
}
}
复制代码
监听系统的锁屏广播,在锁屏的时候开启activity,开屏的关掉便可。
<service
android:name=".plantask.KeepAliveService"
android:enabled="false"
android:exported="true"
android:process=":keepAlive" />
复制代码
service在独立的进程中,因为系统考虑到省电,在锁屏一段时间后会杀掉后台进程,采用这种方式就能够避免了。
局限性: 在Android 5.0系统之后,系统在杀死某个进程的时候同时会杀死该进程群组里面的进程。
Process.killProcessQuiet(app.pid);
Process.killProcessGroup(app.info.uid, app.pid);
复制代码
因此在5.0之后,这个方法也不是很靠谱了,咱们能够须要另寻他法了。
该方法能够说是很靠谱的,主要原理以下:
对于 API level < 18 :调用startForeground(ID, ewNotification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在须要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定一样的 ID。Stop掉InnerService ,这样通知栏图标即被移除。 这里我也给出了实际例子:
public void onCreate()
{
super.onCreate();
//若是API大于18,须要弹出一个可见通知,这个可见通知在大于API 25 版本以前能够经过Cancel隐藏
// 因此在API 18 ~ API 24之间启动前台service,并隐藏Notification
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N){
startForeground(NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 若是以为常驻通知栏体验很差
// 能够经过启动CancelNoticeService,将通知移除,oom_adj值不变
Intent intent = new Intent(this,CancelNoticeService.class);
startService(intent);
}else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
startForeground(NOTICE_ID, new Notification());
}
mScreenUtil = new ScreenReceiverUtil(this);
mScreenManager = ScreenManager.getInstance(this);
mScreenUtil.setScreenReceiverListener(mScreenStateListenerer);
createFloatingWindow();
L.d(TAG, "KeepAliveService-->KeepAliveService created");
// If app process is killed system, all task scheduled by AlarmManager is canceled.
// We need reschedule all task when KeepAliveService is revived.
TaskUtil.dispatchAllTask(this);
}
复制代码
继续看CancelNoticeService
public class CancelNoticeService extends Service {
private static final String TAG = Constants.LOG_TAG;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
L.d(TAG, "CancelNoticeService-->onStartCommand() begin");
//Notification.Builder builder = new Notification.Builder(this);
//builder.setSmallIcon(R.drawable.earth);
startForeground(KeepAliveService.NOTICE_ID, new NotificationUtils(this).getNotification("Sunlands", "打卡提醒进程正在运行"));
// 开启一条线程,去移除DaemonService弹出的通知
new Thread(new Runnable() {
@Override
public void run() {
// 延迟1s
SystemClock.sleep(50);
// 取消CancelNoticeService的前台
stopForeground(true);
// 移除DaemonService弹出的通知
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.cancel(KeepAliveService.NOTICE_ID);
// 任务完成,终止本身
stopSelf();
L.d(TAG, "CancelNoticeService-->onStartCommand() end");
}
}).start();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
复制代码
这种方式实质上是利用了Android系统service的服务漏洞,微信也是采用此法达到保活目的的。
故名思议,当本进程处于后台优先级很低或者被杀死了,有另一个进程能够把你唤醒或者拉活,这里有几种方案。
监听一些系统的广播,好比重启,开启相机等,监听到广播后就能够拉活进程,可是Android N已经取消部分系统广播了。
事实上,QQ,微信这种app在手机中使用频率是很是高的,咱们能够去反编译这些app,获取它们能够发出的广播,而后去监听这些广播,再进行进程的拉活。
像信鸽、极光推送,都有唤醒拉活app的功能。
这种方式不怎么靠谱,可是能够算是多一种保险吧,系统自带的service中有onStartCommand这个方法
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_REDELIVER_INTENT;
}
复制代码
咱们能够对返回值作特殊处理,处理参数以下:
若是系统在onStartCommand返回后被销毁,系统将会从新建立服务并依次调用onCreate和onStartCommand(注意:根据测试Android2.3.3如下版本只会调用onCreate根本不会调用onStartCommand,Android4.0能够办到),这种至关于服务又从新启动恢复到以前的状态了)。
若是系统在onStartCommand返回后被销毁,若是返回该值,则在执行完onStartCommand方法后若是Service被杀掉系统将不会重启该服务。
START_STICKY的兼容版本,不一样的是其不保证服务被杀后必定能重启。
系统服务捆绑,使用NotificationListenerService, 只有手机收到通知都会监听到,若是应用收到的消息比较多的话能够采用该办法去处理,而且即便进程被杀死也能够监听到,这是何等的牛逼啊。