LMK(Low Memory Killer)
進程被殺死無非就是由於系統內存過低,並且進程的優先級比較低,所以纔會被系統kill掉,想要進程保活必須提高進程的優先級。
爲什麼引入LMK?
進程的啓動分冷啓動和熱啓動,當用戶退出某一個進程的時候,並不會真正的將進程退出,而是將這個進程放到後臺,以便下次啓動的時候可以馬上啓動起來,這個過程名爲熱啓動,這也是Android的設計理念之一。這個機制會帶來一個問題,每個進程都有自己獨立的內存地址空間,隨着應用打開數量的增多,系統已使用的內存越來越大,就很有可能導致系統內存不足。爲了解決這個問題,系統引入LowmemoryKiller(簡稱lmk)管理所有進程,根據一定策略來kill某個進程並釋放佔用的內存,保證系統的正常運行
LMK基本原理:
所有應用進程都是從zygote孵化出來的,記錄在AMS中mLruProcesses列表中,由AMS進行統一管理,AMS中會根據進程的狀態更新進程對應的oom_adj值,這個值會通過文件傳遞到kernel中去,kernel有個低內存回收機制,在內存達到一定閥值時會觸發清理oom_adj值高的進程騰出更多的內存空間:
LMK殺進程標準:
內存閾值在不同的手機上不一樣,一旦低於該值,Android便開始按順序關閉進程. 因此Android開始結束優先級最低的空進程,即當可用內存小於180MB(46080)
查看進程的adj值:/proc/<pid>/oom_adj 或者 /proc/<pid>/oom_score_adj
內存閾值在不同的手機上不一樣,一旦低於閾值,Android就會殺死對應優先級的進程,例如:當內存小於315MB(80640),就會殺死空進程。我們可以通過adb shell後輸入`cat /sys/module/lowmemorykiller/parameters/minfree`來查看閾值,如下圖所示(閾值的單位是4KB):![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20190517151620712.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODIwMzY5Ng==,size_16,color_FFFFFF,t_70)
第一個值:18432(72MB)當閾值達到這個值時候,前臺進程就會被殺死;
第二個值:23040(90MB)當閾值達到這個值的時候,可見進程就會被殺死;
第三個值:27648(108MB)當閾值達到這個值的時候,服務進程會被殺死;
第四個值:32256(126MB)當閾值達到這個值的時候,後臺進程會被殺死;
第五個值:55296(216MB)當閾值達到這個值的時候,ContentProvider會被殺死;
第六個值:80640(315MB)當閾值達到這個值的時候,空進程會被殺死。
如何判斷進程優先級?
- 紅色部分是容易被回收的進程,屬於android進程
- 綠色部分是較難被回收的進程,屬於android進程
- 其他部分則不是android進程,也不會被系統回收,一般是ROM自帶的app和服務才能擁有
通過oom_adj的值,判斷進程的優先級,不同的手機的oom_adj的值可能不一樣。oom_adj值越小,優先級越高,也就也難被殺死,我們日常開發的APP最高能達到的值是0,即前臺進程。oom_adj的值我們可以通過 adb shell 直接輸入cat /proc/你的process ID/oom_adj來獲取(有一個前提,手機必須是root過的才能獲取到值)
完整項目地址:https://github.com/buder-cp/base_component_learn/tree/master/performanceOPT/buderdn14
方式一:activity提權保活
原理:
- 監控手機鎖屏解鎖事件,在屏幕鎖屏時啓動1個像素透明的 Activity,在用戶解鎖時將 Activity 銷燬掉,從而達到提高進程優先級的作用。
代碼實現:
- 創建1個像素的Activity:KeepActivity
public class KeepActivity extends Activity {
private static final String TAG = "KeepActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "啓動Keep");
Window window = getWindow();
//設置這個activity在左上角
window.setGravity(Gravity.START | Gravity.TOP);
WindowManager.LayoutParams attributes = window.getAttributes();
//寬高爲1
attributes.width = 1;
attributes.height = 1;
//起始位置左上角
attributes.x = 0;
attributes.y = 0;
window.setAttributes(attributes);
KeepManager.getInstance().setKeepActivity(this);
}
}
- 創建廣播接收者,當接收到亮屏或者息屏廣播時啓動我們的1像素activity:KeepReceiver
public class KeepReceiver extends BroadcastReceiver {
private static final String TAG = "KeepReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.e(TAG, "receive:" + action);
if (TextUtils.equals(action, Intent.ACTION_SCREEN_OFF)) {
//滅屏 開啓1px activity
KeepManager.getInstance().startKeep(context);
} else if (TextUtils.equals(action, Intent.ACTION_SCREEN_ON)) {
//亮屏 關閉
KeepManager.getInstance().finishKeep();
}
}
}
- 創建廣播註冊管理單例類來啓動或者關閉這個activity:KeepManager
/**
* 啓動1個像素的KeepActivity
*
* @param context
*/
public void startKeep(Context context) {
Intent intent = new Intent(context, KeepActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* finish1個像素的KeepActivity
*/
public void finishKeep() {
if (null != mKeepActivity) {
Activity activity = mKeepActivity.get();
if (null != activity) {
activity.finish();
}
mKeepActivity = null;
}
}
使用方式:在我們主應用打開時註冊這個廣播接收者,當應用在後臺並且用戶息屏後就會啓動我們的activity,以提高我們應用進程的優先級,防止在後臺被清除掉。
缺點:存在一個Activity不夠乾淨,同時需要在鎖屏才能監聽到,如果用戶一直處於亮屏狀態,oom_adj的值不會變小,如果內存過小,進程會被殺死。
方式二:Service提權保活
啓動一個前臺服務,從而拉高進程的優先級,提權是將應用的OMM提高,系統會認爲我們是一個較高優先級的進程而防止應用被後臺kill掉。
創建一個前臺服務用於提高app在按下home鍵之後的進程優先級,startForeground(ID,Notification):使Service成爲前臺Service。 前臺服務需要在通知欄顯示一條通知。
使用前臺服務在不同API上的差別:
- API leveL<18:參數2設置new Notification(),圖標不會顯示
- API level >=18 & API level < 26:在需要提優先級的serviceA啓動一個InnerService。兩個服務都startForeground,且綁定同樣的ID。stop掉InnerService,通知欄圖標被移除
- API level >= 26:必須手動創建通知欄,無法移除通知欄圖標startForegroundService()替代了startService()
在大於26版本後都有通知欄駐存,會提示到用戶,因此這個service提權並不好用。
我們需要對不同API做適配:
public class ForegroundService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("deamon", "deamon",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (manager == null)
return;
manager.createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this, "deamon").setAutoCancel(true).setCategory(
Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
NotificationManager.IMPORTANCE_LOW).build();
startForeground(10, notification);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果 18 以上的設備 啓動一個Service startForeground給相同的id
//然後結束那個Service
startForeground(10, new Notification());
startService(new Intent(this, InnnerService.class));
} else {
startForeground(10, new Notification());
}
}
public static class InnnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(10, new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
缺點:這種保活方式有一個缺點,就是在API > 26即8.0以上版本的通知欄隱藏不了。
OK,保活就講到這裏,下面我們將如何拉活進程。
方式三:廣播拉活:
Android在7.0之後對廣播增加了一些限制,在8.0以後就更加嚴格了,現在接收系統廣播的拉活方式基本上已經用不了了。
在發生特定系統事件時,系統會發出廣播,通過在 AndroidManifest 中靜態註冊對應的廣播監聽器,即可在發生響應事件時拉活。但是從android 7.0開始,對廣播進行了限制,而且在8.0更加嚴格。
目前可用的靜態廣播:https://developer.android.google.cn/guide/components/broadcast-exceptions.html
受 Android 8.0(API 級別 26)後臺執行限制的影響,以 API 級別 26 或更高級別爲目標的應用無法再在其清單中註冊用於隱式廣播的廣播接收器。不過,有幾種廣播目前不受這些限制的約束。無論應用以哪個 API 級別爲目標,都可以繼續爲以下廣播註冊監聽器。官方提示:注意:儘管這些隱式廣播仍在後臺運行,但您應避免爲其註冊監聽器。
列舉幾個被豁免的廣播:
ACTION_LOCKED_BOOT_COMPLETED
、ACTION_BOOT_COMPLETED:
豁免的原因這些廣播僅在首次啓動時發送一次,而且許多應用需要接收此廣播以調度作業、鬧鐘等。ACTION_LOCALE_CHANGED:僅在語言區域發生更改時發送,這種情況並不常見。
ACTION_PACKAGE_DATA_CLEARED:僅在用戶明確清除“設置”中的數據時,因此廣播接收器不太可能對用戶體驗造成顯著影響
ACTION_DEVICE_OWNER_CHANGED:此直播的發送頻率不高;某些應用需要接收它來了解設備的安全狀態已發生更改
可見,這些廣播一般我們也用不太到,對於應用保活拉活已經基本沒有什麼實際意義。
方式四:“全家桶”拉活
有多個app在用戶設備上安裝,只要開啓其中一個就可以將其他的app也拉活。比如手機裏裝了手Q、QQ空間、興趣部落等等,那麼打開任意一個app後,其他的app也都會被喚醒。
這種方式除了大廠外,對於一般開發者的應用是沒有參考價值的。
方式五:Service機制(Sticky)拉活
將 Service 設置爲 START_STICKY,利用系統機制在 Service 掛掉後自動拉活。
- START_STICKY:“粘性”。如果service進程被kill掉,保留service的狀態爲開始狀態,但不保留遞送的intent對象。隨後系統會嘗試重新創建service,由於服務狀態爲開始狀態,所以創建服務後一定會調用onStartCommand(Intent,int,int)方法。如果在此期間沒有任何啓動命令被傳遞到service,那麼參數Intent將爲null。
- START_NOT_STICKY:“非粘性的”。使用這個返回值時,如果在執行完onStartCommand後,服務被異常kill掉,系統不會自動重啓該服務。
- START_REDELIVER_INTENT:重傳Intent。使用這個返回值時,如果在執行完onStartCommand後,服務被異常kill掉,系統會自動重啓該服務,並將Intent的值傳入。
- START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保證服務被kill後一定能重啓。
只要 targetSdkVersion 不小於5,就默認是 START_STICKY。
但是某些ROM 系統不會拉活。並且經過測試,Service 第一次被異常殺死後很快被重啓,第二次會比第一次慢,第三次又會比前一次慢,一旦在短時間內 Service 被殺死4-5次,則系統不再拉起。
public class StickyService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
}
上面就是默認是Start_Sticky服務。
方式六:賬戶同步拉活
賬戶同步這篇文章講的比較全面:賬戶同步拉活在此我就不作多講了;
利用賬號同步機制拉活,不過貌似被改了,失效了
方式七:JobScheduler拉活
JobScheduler允許在特定狀態與特定時間間隔週期執行任務。可以利用它的這個特點完成保活的功能,效果即開啓一個定時器,與普通定時器不同的是其調度由系統完成。
創建一個JobService:注意setPeriodic方法在7.0以上如果設置小於15min不起作用,可以使用setMinimumLatency設置延時啓動,並且輪詢。
- 第一步:創建MyJobService實現JobService,如:
@SuppressLint("NewApi")
public class MyJobService extends JobService {
private static final String TAG = "MyJobService";
public static void startJob(Context context) {
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(
Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(10,
new ComponentName(context.getPackageName(),
MyJobService.class.getName())).setPersisted(true);
/**
* I was having this problem and after review some blogs and the official documentation,
* I realised that JobScheduler is having difference behavior on Android N(24 and 25).
* JobScheduler works with a minimum periodic of 15 mins.
*
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
//7.0以上延遲1s執行
builder.setMinimumLatency(1000);
}else{
//每隔1s執行一次job
builder.setPeriodic(1000);
}
jobScheduler.schedule(builder.build());
}
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.e(TAG,"開啓job");
//7.0以上輪詢
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
startJob(this);
}
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}
- 第二步:在清單中註冊MyJobService
<service
android:name=".jobSchedule.MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
- 第三步:手動開啓JobSchedule,在activity的oncreate方法中啓動Service
MyJobService.startJob(this);
方式八:推送拉活
根據終端不同,在小米手機(包括 MIUI)接入小米推送、華爲手機接入華爲推送。
方式九:Native拉活
Native fork子進程用於觀察當前app主進程的存亡狀態。對於5.0以上成功率極低。
方式十:雙進程守護
兩個進程共同運行,如果有其中一個進程被殺,那麼另外一個進程就會將被殺的進程重新拉起
雙進程守護本質上是開啓兩個服務,一個本地服務和一個遠程服務,當其中一個服務被殺死時,另一個服務會自動的把被殺死的那個服務拉活。
- 第一步:創建一個空的AIDL文件
interface IMyAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
- 2、創建本地LocalService和遠程RemoteService
LocalService.java:
public class LocalService extends Service {
private MyBinder myBinder;
class MyBinder extends IMyAidlInterface.Stub{
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString) throws RemoteException {
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
@Override
public void onCreate() {
super.onCreate();
myBinder = new MyBinder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("deamon", "deamon",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
if (manager == null)
return;
manager.createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this,
"deamon").setAutoCancel(true).setCategory(
Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
NotificationManager.IMPORTANCE_LOW).build();
startForeground(10, notification);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果 18 以上的設備 啓動一個Service startForeground給相同的id
//然後結束那個Service
startForeground(10, new Notification());
startService(new Intent(this, InnnerService.class));
} else {
startForeground(10, new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
bindService(new Intent(this, RemoteService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
return super.onStartCommand(intent, flags, startId);
}
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//回調
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
//
startService(new Intent(LocalService.this, RemoteService.class));
bindService(new Intent(LocalService.this, RemoteService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
}
}
public static class InnnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(10, new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
RemoteService.java:
public class RemoteService extends Service {
private MyBinder myBinder;
class MyBinder extends IMyAidlInterface.Stub {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString) throws RemoteException {
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
@Override
public void onCreate() {
super.onCreate();
myBinder = new MyBinder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("deamon", "deamon",
NotificationManager.IMPORTANCE_LOW);
NotificationManager manager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
if (manager == null)
return;
manager.createNotificationChannel(channel);
Notification notification = new NotificationCompat.Builder(this,
"deamon").setAutoCancel(true).setCategory(
Notification.CATEGORY_SERVICE).setOngoing(true).setPriority(
NotificationManager.IMPORTANCE_LOW).build();
startForeground(10, notification);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果 18 以上的設備 啓動一個Service startForeground給相同的id
//然後結束那個Service
startForeground(10, new Notification());
startService(new Intent(this, InnnerService.class));
} else {
startForeground(10, new Notification());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
bindService(new Intent(this, LocalService.class), new MyServiceConnection(),
BIND_AUTO_CREATE);
return super.onStartCommand(intent, flags, startId);
}
private class MyServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
startService(new Intent(RemoteService.this, LocalService.class));
bindService(new Intent(RemoteService.this, LocalService.class),
new MyServiceConnection(), BIND_AUTO_CREATE);
}
}
public static class InnnerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(10, new Notification());
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
}
- 第三步:在清單文件中註冊服務
<service android:name=".service.LocalService"
android:exported="true"
android:process=":local"/>
<service android:name=".service.LocalService$InnnerService"
android:exported="true"
android:process=":local"/>
<service android:name=".service.RemoteService"
android:exported="true"
android:process=":remote"/>
<service android:name=".service.RemoteService$InnnerService"
android:exported="true"
android:process=":remote"/>
<service android:name=".service.MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
- 第四步:在Activity中的onCreate方法開啓兩個服務
startService(new Intent(this, LocalService.class));
startService(new Intent(this, RemoteService.class));
我們還可以利用上面的JobSchedule 進一步保活我們的進程
public class MyJobService extends JobService {
private static final String TAG = "MyJobService";
@Override
public boolean onStartJob(JobParameters jobParameters) {
Log.e(TAG, "onStartJob");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
startJob(this);
}
boolean isLocalRun = isServiceRunning(this, LocalService.class.getName());
boolean isRemoteRun = isServiceRunning(this, RemoteService.class.getName());
if (!isLocalRun || !isRemoteRun) {
startService(new Intent(this, LocalService.class));
startService(new Intent(this, RemoteService.class));
}
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
public static void startJob(Context context) {
JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
// setPersisted 在設備重啓依然執行
JobInfo.Builder builder = new JobInfo.Builder(8, new ComponentName(context
.getPackageName(), MyJobService.class.getName())).setPersisted(true);
//小於7.0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
builder.setPeriodic(1000);
} else {
builder.setMinimumLatency(1000);
}
assert jobScheduler != null;
jobScheduler.schedule(builder.build());
}
/**
* 判斷服務是否開啓
*
* @return
*/
public static boolean isServiceRunning(Context context, String ServiceName) {
if (TextUtils.isEmpty(ServiceName)) {
return false;
}
ActivityManager myManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ArrayList<ActivityManager.RunningServiceInfo> runningService = (ArrayList<ActivityManager.RunningServiceInfo>) myManager.getRunningServices(1000);
for (int i = 0; i < runningService.size(); i++) {
Log.i("服務運行1:",""+runningService.get(i).service.getClassName().toString());
if (runningService.get(i).service.getClassName().toString().equals(ServiceName)) {
return true;
}
}
return false;
}
}
開倆個進程相互喚起,因爲系統的進程回收機制是一個個回收的,利用這個時間差來相互喚起,當一個進程被磨滅掉,另一個馬上重啓,缺點是現在大部分機型只要一鍵清理就玩完了,不過也沒有更好的辦法,而且8.0之後對這個做了限制,想要在一個後再服務中啓動另一個服務會報錯,可以用startForegroundService方法,但是會有一個通知在通知欄,這就有點不太友好了,不過介於8.0以下手機還有很多,可以考慮
總結:
進程保活和拉活的方案到這裏就結束了,隨着谷歌公司對Android的安全機制做的越發的好,想要永久保活是完全不可能的,除非我們的APP可以跟一些手機廠商合作添加到白名單中才能實現永久保活, 另外系統會優先殺死佔用內存多的應用,所以想讓自己的APP活的更久,還可以從性能的角度上去優化,讓其儘可能少的佔用內存。如果有說的不對的地方可以評論指出錯誤的地方,謝謝!