十二、Android进程保活

Android系统会尽量长时间的保持应用的进程,但是为了新建进程或运行更重要的进程,在内存不足时会清理掉旧进程来释放内存;
为了确定保留或终止哪些进程,系统将进程进行了分类;主要分为五类:

按重要性从高到低排列:
  • 1.前台进程


    image.png
  • 2.可见进程

  • 3.服务进程

  • 4.后台进程

  • 5.空进程

在需要时,系统会清除重要性低的进程,以此类推,来回收系统资源;

系统出于体验和性能上的考虑,app退到后台时,系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也也越多,系统开始依据自身的一套进程回收机制来判断需要kill掉哪些进程,以腾出内存供给需要的app,Android系统这套杀进程回收内存的机制位:Low Memory Killer;

cat /sys/module/lowmemorykiller/parameters/minfree

这个命令会展示出一行数字,这些数字的单位是page,1page==4kb
内存阈值在不同的手机上不一样,一旦低于该值,Android系统便开始按顺序关闭进程,因此Android开始关闭优先级最低的空进程,即当内存小于xxxMB(46080page);

其实,进程的优先级可以通过一个oom_adj的值来反应出来,它是linux内核分配给每个系统进程的一个值,越小代表进程优先级越高;
可以通过命令:

cat /proc/进程id/oom_adj

看到当前进程的oom_adj的值;(这条命令需要room权限)


image.png

备注:adj越大,占用内存越多会被最先kill掉,所以保活就成了降低oom_adj的值,以及如何使得我们应用占的内存最少。

一、进程优先级提高方法一:Activity提权

就是监控手机锁屏和开屏广播,在手机锁屏时,启动一个1像素的透明的Activity;在用户解锁时,销毁这个Activity;从而达到提高进程优先级的作用;
弊端:存在一个无用的Activity,并且只有在锁屏的时候才能提权;
具体步骤:
1.创建一个activity

import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;

/**
 * Created by serenitynanian on 2018/10/23.
 */

public class KeepActivity extends AppCompatActivity {
    private static final String TAG = "KeepActivity";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Window window = getWindow();
        //放在左上角
        window.setGravity(Gravity.TOP|Gravity.LEFT);
        WindowManager.LayoutParams attributes = window.getAttributes();
        //宽高设置为1像素
        attributes.width = 1 ;
        attributes.height = 1 ;
        //起始坐标
        attributes.x = 0 ;
        attributes.y = 0 ;

        window.setAttributes(attributes);

    }

    public void registerKeep(){
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_SCREEN_ON);
        intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
        //注册广播
        BroadcastReceiver receiver = null;//声明成员属性
        registerReceiver(receiver, intentFilter);
    }

    public void unRegisterKeep(){
        //从成员属性中获得
        BroadcastReceiver receiver = null;
        if (null != receiver) {
            unregisterReceiver(receiver);
        }
    }
}

2.在AndroidManifest.xml中注册

<!-- android:excludeFromRecents="true"让它在最近任务列表里面没有展示-->
        <!--android:taskAffinity 让这个Activity在一个单独的栈里面-->
        <activity android:name=".KeepActivity"
            android:theme="@style/KeepTheme"
            android:excludeFromRecents="true"
            android:taskAffinity="com.serenity.daemon.keep"
            />

 //注册activity时设置的style样式
    <style name="KeepTheme" >
        <item name="android:windowBackground">@null</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>
二、进程优先级提高方法二:Service提权

使用startForeground(ID,Notification):使Service成为前台Service。 前台服务也是前台进程的一种形式:可以创建一个前台服务用于提高app在按下home键之后的进程优先级;
前台服务需要在通知栏显示一条:


image.png

API level < 18 :参数2 设置 new Notification()即可,图标不会显示;在大于18,参数1设置0后,图标不会显示,但是提权失败。
API level >= 18:在需要提优先级的service A启动一个InnerService。两个服务都startForeground,且绑定同样的 ID。Stop 掉InnerService ,通知栏图标被移除。也就是在第一个Service的onCreate中启动第二个InnerService,在InnerService也startForeground(id, notification),同时调用stopSelf()结束自己;
具体使用步骤:
1.创建两个service ,其中一个是内部service;

package com.serenity.daemon.daemon.service;

import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;

/**
 * Created by serenitynanian on 2018/10/24.
 * 前台服务
 */

public class ForegroundService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //startForeground让服务变成前台服务
        //两个参数的意思是:设置一个id为1的通知
        startForeground(1,new Notification());

        /**
         * 如果 18 以上的设备 启动一个service startForeground给相同的id
         *      然后结束这个服务  就不会显示通知栏了
         */
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR2){
            startService(new Intent(this, InnerSerivice.class));
        }
    }

    public static class InnerSerivice extends Service{

        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }

        @Override
        public void onCreate() {
            super.onCreate();
            startForeground(1,new Notification());
            //结束自己
            stopSelf();
        }
    }
}

2.在AndroidManifest.xml中注册这两个service

        <service android:name=".service.ForegroundService"/>
        <service android:name=".service.ForegroundService$InnerSerivice"/>
三、 拉活进程:广播拉活

在系统发出特定事件时,会发出系统广播,在AndroidManifest.xml中静态注册对应的广播监听器,即可在发生响应事件时拉活;
但是从android 7.0开始,对广播进行了限制,而且在8.0更加严格https://developer.android.google.cn/about/versions/oreo/background.html#broadcasts
可静态注册广播列表:
https://developer.android.google.cn/guide/components/broadcast-exceptions.html

现在可监听的系统广播没有合适的实现进程拉活,这种情况暂时不在阐述;

三、 拉活进程:系统service拉活

将 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次,则系统不再拉起。

四、 拉活进程:账户同步拉活

手机系统设置里面都会有“账户”功能,任何第三方的app都可以通过此功能添加一个账户,然后触发同步,将数据在一定的时间内同步到服务器中去;系统在将app账户同步时,会将未启动app的进程拉活;
https://github.com/googlesamples/android-BasicSyncAdapter

优点:由系统主动拉起进程,生态稳定
缺点:同步账户周期过长;由系统自己计算时间,来调用,不能人为控制;

  • 1.第一步,创建一个APP账户service,通过binder告知系统存在这样一个APP账户
package com.serenity.daemon.daemon.account;

import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;

/**
 * Created by serenitynanian on 2018/10/24.
 * 这个service通过binder来告知系统,存在这样一个APP账户
 * 这个APP的账号信息在authenticator.xml中配置的
 * 这个服务不需要我们来启动,是由系统自动检测 自动启动
 */

public class AuthenticationService extends Service {

    private AccountAuthenticator accountAuthenticator;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return accountAuthenticator.getIBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        accountAuthenticator = new AccountAuthenticator(this);
    }

    static class AccountAuthenticator extends AbstractAccountAuthenticator {

        public AccountAuthenticator(Context context) {
            super(context);
        }

        @Override
        public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
            return null;
        }

        @Override
        public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public String getAuthTokenLabel(String authTokenType) {
            return null;
        }

        @Override
        public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
            return null;
        }

        @Override
        public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
            return null;
        }
    }
}
  • 2.在AndroidManifest.xml中注册
 <!--用与告知系统存在这样一个app账户的service-->
        <service android:name=".account.AuthenticationService">
            <!--需要配置android.accounts.AccountAuthenticator 让系统能够找到这个账户服务-->
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <!--指定设置  账户中显示的信息-->
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator"
                />
        </service>
  • 3.需要添加一个meta-data resource文件,来指定这个app账户信息
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.serenity.daemon.account"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    />
<!--accountType 这个是账户类型,是个标识-->
  • 4.通过以上三步骤,系统已经知道存在这样一个APP账户,下面是添加一个这样的账户
   public static final String ACCOUNT_TYPE = "com.serenity.daemon.account";


    /**
     * 添加一个系统中存在账户类型的账户
     * @param context
     */
    public static void addAccount(Context context){
        AccountManager am = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
        //获得此类型的账户
        Account[] accountsByType = am.getAccountsByType(ACCOUNT_TYPE);
        if (accountsByType.length > 0) {
            Log.e(TAG, "账户已存在" );
            return;
        }
        //给这个账户类型添加一个账户
        Account serenity = new Account("serenity", ACCOUNT_TYPE);
        //明确指定一个账户和密码,及所传参数
        am.addAccountExplicitly(serenity, "123", new Bundle());
    }
  • 5.添加账户后,需要系统进行同步账户信息,需要创建一个同步数据的service
package com.serenity.daemon.daemon.account;

import android.accounts.Account;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;


/**
 * Created by serenitynanian on 2018/10/24.
 * 系统通过调用此服务  用于同步账户数据的service
 * 并且在onBind返回AbstractThreadedSyncAdapter的getSyncAdapterBinder
 *  当系统通过binder同步数据时,会调用到SyncAdapter的onPerformSync方法,执行同步
 *  
 *  这个服务也是由系统bind此service调用 通过binder来同步数据
 */


public class SyncService extends Service {
    private static final String TAG = "SyncService";
    private SyncAdapter syncAdapter;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return syncAdapter.getSyncAdapterBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        syncAdapter = new SyncAdapter(this, true);
    }

    static class SyncAdapter extends AbstractThreadedSyncAdapter{

        public SyncAdapter(Context context, boolean autoInitialize) {
            super(context, autoInitialize);
        }

        @Override
        public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
            Log.e(TAG, "onPerformSync: 同步账户");
            //与互联网 或者 本地数据库同步账户
        }
    }
}
  • 6.在AndroidManifest.xml中注册
 <!--用与同步的service-->
        <service android:name=".account.SyncService">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/sync_adapter"
                />
        </service>

上面注册同步数据service时,需要添加一个同步数据账户配置meta-data resource

<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"

    android:accountType="com.serenity.daemon.account"
    android:contentAuthority = "com.serenity.daemon.provider"
    android:allowParallelSyncs = "false"
    android:isAlwaysSyncable = "true"
    android:userVisible = "false"
    />
<!--accountType 必须与authenticator.xml中的accountType 一致-->

<!--contentAuthority系统在进行账户同步时 会查找此auth的ContentProvider-->

<!--allowParallelSyncs 允许多个同步-->

<!--isAlwaysSyncable 一直同步-->

<!--userVisible 是否在账户旁边展示一个开关-->
  • 7.配置同步参数----告知系统应该怎样进行同步
/**
     * 配置同步参数----告知系统应该怎样进行同步
     * 设置账户自动同步
     * 
     * 这个方法要在添加完账户后,主动调用
     */
    public static void autoAccount(){
        Account serenity = new Account("serenity", ACCOUNT_TYPE);
        //设置同步
        //第二个参数就是在sync_adapter.xml设置的contentAuthority的值
        ContentResolver.setIsSyncable(serenity,"com.serenity.daemon.provider",1);
        //设置自动同步
        ContentResolver.setSyncAutomatically(serenity,"com.serenity.daemon.provider",true);
        //设置同步周期
        ContentResolver.addPeriodicSync(serenity,"com.serenity.daemon.provider",new Bundle(),1);

        //设置立即同步,但是我们需要的是由系统主动拉活进程
//        ContentResolver.requestSync();
    }

在上面的配置同步账户时,需要指定一个contentProvider,

package com.serenity.daemon.daemon.account;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

/**
 * Created by serenitynanian on 2018/10/24.
 */

public class SyncProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

在AndroidManifest.xml中配置

<provider
            android:authorities="com.serenity.daemon.provider"
            android:name=".account.SyncProvider"/>

其中authorities是一个标识,必须与同步设置的authorities一致

五、 拉活进程:JobScheduler

JobScheduler允许在特定状态和特定时间间隔周期执行任务;可以利用它的这个特点来保活进程,其效果就是开启一个定时器;但是与普通定时器不同的是:其调度是由系统来完成的;

缺点:在某些ROM手机上无效果(小米)

package com.serenity.daemon.daemon.account.jobscheduler;

import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.util.Log;

/**
 * Created by serenitynanian on 2018/10/24.
 * 使用jobService来进行拉活
 */

public class MyJobService extends JobService {

    private static final String TAG = "MyJobService";
    public static void startJobServcie(Context context){
        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
//        setPersisted 用来设置  在设备重启依然执行 设置这个属性需要重启权限
//        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
        JobInfo.Builder builder = new JobInfo.Builder(10, new ComponentName(context.getPackageName(), MyJobService.class.getName()))
                .setPersisted(true);
        //小于7.0
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            //设置没隔1秒执行一次job
            //7.0以上设置无效
            builder.setPeriodic(1000);
        }else{
            //7.0以上  设置最小等待时间(时延)  延迟执行
            builder.setMinimumLatency(1000);
        }
        JobInfo jobInfo = builder.build();
        jobScheduler.schedule(jobInfo);

    }
    @Override
    public boolean onStartJob(JobParameters params) {
        Log.e(TAG, "onStartJob:-----开启jobService");
        //如果大于7.0 进行 轮循执行
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            startJobServcie(this);
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.e(TAG, "onStopJob:-----停止jobService");
        return false;
    }
}

在AndroidManifest.xml注册

 <!--注册jobService  需要给权限-->
        <service android:name=".account.jobscheduler.MyJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            />

请点击具体Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 这几天,翻看头条新闻,无意间周杰伦-蔡依林-林俊杰-罗志祥-王力宏等实力派歌手,出现在我的视线里。在翻看有关他们演...
    棉絮儿阅读 144评论 0 0
  • 初见,你笑如阳光,把我照进温暖,还好我们都曾不断挥手说再见。 才能敞开心扉,不卑不亢,终遇知心人。 我的任性,懒惰...
    求知欲道阅读 416评论 0 2
  • 很多事儿想不明白 因为很爱一个人所以很想和对方结婚,似乎不对 有矛盾的时候想自己消化不想再生事端,他让我说出心里话...
    阿花他夫人阅读 258评论 0 0