須要引用請註明出處: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的值爲0。 當按返回鍵後,程序退到後臺,這時候adj變爲1通常而言,進程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, 只有手機收到通知都會監聽到,若是應用收到的消息比較多的話能夠採用該辦法去處理,而且即便進程被殺死也能夠監聽到,這是何等的牛逼啊。