Android 进程保活 的两种实现方式

前言

目前市场上主流的项目应用app,在其进程被杀掉之后,还是可以继续运行在后台(保活);比如,微信,淘宝,钉钉,QQ等。类似耍流氓,保证应用进程不被杀死。当然优雅的说法:常驻进程。不过现在各个手机厂商都有白名单,将应用加入到白名单,可100%解决进程保活的需求。

我是个小安卓.jpg

差强人意的方法

网上给一些常见的方法:

  1. 提高优先级
    这个办法对普通应用而言,
    应该只是降低了应用被杀死的概率,但是如果真的被系统回收了,还是无法让应用自动重新启动!

  2. 让service.onStartCommand返回START_STICKY,START_STICKY是service被kill掉后自动重启
    保活100%
    通过实验发现,如果在adb shell当中kill掉进程模拟应用被意外杀死的情况(或者用360手机卫士进行清理操作),
    如果服务的onStartCommand返回START_STICKY,
    在进程管理器中会发现过一小会后被杀死的进程的确又会出现在任务管理器中,貌似这是一个可行的办法。
    但是如果在系统设置的App管理中选择强行关闭应用,这时候会发现即使onStartCommand返回了START_STICKY,应用还是没能重新启动起来!

  3. android:persistent="true"
    网上还提出了设置这个属性的办法,通过实验发现即使设置了这个属性,应用程序被kill之后还是不能重新启动起来的!

  4. 让应用成为系统应用
    实验发现即使成为系统应用,被杀死之后也不能自动重新启动。
    但是如果对一个系统应用设置了persistent="true",情况就不一样了。
    实验表明对一个设置了persistent属性的系统应用,即使kill掉会立刻重启。
    一个设置了persistent="true"的系统应用,
    android中具有core service优先级,这种优先级的应用对系统的low memory killer是免疫的!

  5. 设置闹钟,定时唤醒
    这个效果是百分百的,但是不符合实际业务场景。

应用优先级

Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收
Android将进程分为5个等级,它们按优先级顺序由高到低依次是:

  • 空进程 Empty process
  • 可见进程 Visible process
  • 服务进程 Service process
  • 后台进程 Background process
  • 前台进程 Foreground process

如何在程序杀死的清下重启进程-----SIGLE信号

  • 思路
  1. 利用am命令,启动主进程的一个service
  2. SIGLE信号,通过SIGLE信号来判断程序是否被杀死
    在Linux系统下,如果使用sigaction将信号SIGCHLD的sa_flags中的SA_NOCLDSTOP选项打开,
    当子进程停止(STOP作业控制)时,
    不产生此信号(即SIGCHLD)。不过,当子进程终止时,仍旧产生此信号(即SIGCHLD)。
    僵尸

sigaction函数:
函数功能是:检查或修改与指定信号相关联的处理动作

sigaction(SIGCHLD, &sa, NULL);

wait()函数
函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

int status;
wait(&status);

查看Android进程


Android手机进程查看.png

uid Android用户id 号
pid 当前的进程号
ppid 当前进程的父进程号


敲代码.jpg

开始撸码

由于上面讲的内容都是在c++实现的,所以搞个jni工程

  • 创建native方法
  public native void watcher(String userId, int processId);
  • 主进程创建一个service,用来在主进程被杀的时候,通过am命令进行重启主进程
public class KeepProcessService extends Service {

  private static final String TAG = "BAO";
  private int i = 0;

  @Override
  public void onCreate() {
    super.onCreate();
    ProcessWatcher watcher = new ProcessWatcher();
    watcher.watcher(String.valueOf(Process.myUid()),  Process.myPid());

    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
      @Override
      public void run() {
        Log.i(TAG, "服务进程,运行中 i = "+i);
        i++;
      }
    }, 0,  3000);
  }

  ......省略其他代码
}
  • C++的实现
const char *_user_id;
int _process_id;

//子进程变成僵尸进程会调用这个方法
void sig_handler(int sino) {

    int status;
    //阻塞式函数
    LOGE("等待死亡信号");
    wait(&status);

    LOGE("创建进程");
    create_child_process();
}


extern "C"
JNIEXPORT void JNICALL
Java_com_jason_signal_process_ProcessWatcher_watcher(JNIEnv *env, jobject thiz, jstring user_id, jint process_id) {
    _process_id = process_id;
    _user_id = env->GetStringUTFChars(user_id, NULL);
    //为了防止子进程被弄成僵尸进程
    struct  sigaction sa;
    sa.sa_flags=0;

    sa.sa_handler = sig_handler;
    sigaction(SIGCHLD, &sa, NULL);
    create_child_process();
}


void create_child_process() {

    //创建一个子进程
    pid_t pid = fork();

    if(pid < 0) {
        LOGE("创建子进程失败!");
    } else if(pid > 0 && pid < getppid()) {
        LOGE("这个是父进程!");
    } else {  
        LOGE("创建子进程成功!");
        LOGE("进程PID是%d", getpid());
        LOGE("进程PPID是%d", getppid());
        LOGE("创建的子进程ID:%d", pid);
        create_process_monitor();
       
    }
}


void *thread_fun_signal(void *data) {
    //ppid 表示的是父进程号  pid表示当前进程号
    pid_t pid;
    while((pid = getppid()) != 1) {
        sleep(2);
        LOGE("循环 %d ",pid);
    }
    //当子进程的父进程号等于1 ,表示主进程被杀死了,子进程被init进程托管了
    LOGE("重启父进程");
    // 用am命令 启动KeepProcessService,来启动主进程
    execlp("am", "am", "startservice", "--user", _user_id,
           "com.jason.signal.process/com.jason.signal.process.KeepProcessService", (char*)NULL);
}

//创建一个线程
void create_process_monitor() {
    pthread_t  pt_t;
    pthread_create(&pt_t, NULL, thread_fun_signal,  NULL);
}

以上就是利用Android的linux内核的signal信号来,重启被杀掉的进程。

如何在程序杀死的清下重启进程-----socket方式 进程间通信

  • 思路
  1. 创建一个子进程作为socket的的服务端
  2. 将主进程作为客户端,通过socket进行连接,当主进程被杀死之后,子进程服务端会受到一个主进程被杀的消息,这个时候通过am命令启动service重新启动主进程。
  • 介绍函数
  1. int socket()函数
int  socket(int protofamily, int type, int protocol);//返回sockfd

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

参数 说明
protofamily 即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型
type 指定socket类型, 常用的socket类型SOCK_STREAM IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应 流协议,TCP传输协议、UDP传输协议、 STCP传输协议、TIPC传输协议
protocol socket支持哪些协议,https://www.cnblogs.com/liyuanhong/articles/10591069.html
  1. bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数介绍:

参数 说明
sockfd socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个socket
addr 一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同
addrlen 对应的是地址的长度
醒醒改不bug.jpg

开始撸码

  • 创建native方法
  public native void watch(String userId);
  public native void connect();
  • 同上方法创建service,在service的oncreate,进行socket的创建和连接
watcher.watch(String.valueOf(Process.myUid()));
watcher.connect();
  • C++的实现:子进程创建socket的服务单,主进程进行连接
int m_child;

const char *userId;
const char *PATH = "/data/data/com.jason.socket.process/my.sock";

extern "C"
JNIEXPORT void JNICALL
Java_com_jason_socket_process_Watcher_watch(JNIEnv *env, jobject thiz, jstring user_id) {

    userId = env->GetStringUTFChars(user_id, NULL);
    create_child_process();
}

void create_child_process() {
    pid_t pid = fork();
    if(pid < 0) {
    } else if (pid > 0) {
    } else {
        do_child_work();
    }
}

void do_child_work() {
    //1 在子进程建立socket服务,作为服务端,等待父进程连接
    //2 读取消息来自父进程的消息:这边唯一的消息是父进程被杀掉
    if(create_socket_server()) {
        child_listen_msg();
    }
}

int create_socket_server() {
    //1 创建socket对象
    int listenId = socket(AF_LOCAL, SOCK_STREAM, 0);
    //2 断开之前的连接
    unlink(PATH);
    struct sockaddr_un addr;
    //3 清空内存
    memset(&addr, 0, sizeof(sockaddr_un));
    addr.sun_family = AF_LOCAL;

    strcpy(addr.sun_path, PATH);
    int connfd = 0;
    LOGE("绑定端口号");
    if(bind(listenId, (const sockaddr *) &addr, sizeof(addr))<0) {
        LOGE("绑定错误");
        return 0;
    }
    //设置最大的连接数
    listen(listenId, 5);
    while (1) {
        LOGE("子进程循环等待连接  %d ",m_child);
        // 不断接受客户端请求的数据
        // 等待 客户端连接  accept阻塞式函数
        if ((connfd = accept(listenId, NULL, NULL)) < 0) {
            if (errno == EINTR) {
                continue;
            } else{
                LOGE("读取错误");
                return 0;
            }
        }
        //apk 进程连接上了
        m_child = connfd;
        LOGE("apk 父进程连接上了  %d ",m_child);
        break;
    }
    LOGE("返回成功");
    return 1;
}

void child_listen_msg() {

    fd_set rfds;
    while (1) {
        // 清空端口号
        FD_ZERO(&rfds);
        // 设置新的端口号
        FD_SET(m_child,&rfds);
        // 设置超时时间
        struct timeval timeout={3,0};
        int r = select(m_child + 1, &rfds, NULL, NULL, &timeout);
        LOGE("读取消息前  %d  ",r);
        if (r > 0) {
            char pkg[256] = {0};
            // 确保读到的内容是制定的端口号
            if (FD_ISSET(m_child, &rfds)) {
                // 阻塞式函数  客户端写到内容
                int result = read(m_child, pkg, sizeof(pkg));
                // 读到内容的唯一方式 是客户端断开
                LOGE("重启父进程  %d ",result);
                LOGE("读到信息  %d    userid  %d ",result, userId);
                execlp("am", "am", "startservice", "--user", userId,
                       "com.jason.socket.process/com.jason.socket.process.KeepProcessService", (char*)NULL);
                break;
            }
        }
    }
}


extern "C"
JNIEXPORT void JNICALL
Java_com_jason_socket_process_Watcher_connect(JNIEnv *env, jobject thiz) {

    //主进程socket连接父进程
    int sockfd;
    struct sockaddr_un  addr;
    while (1) {
        LOGE("客户端  父进程开始连接");
        sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
        if (sockfd < 0) {
            return;
        }
        memset(&addr, 0, sizeof(sockaddr_un));
        addr.sun_family = AF_LOCAL;
        strcpy(addr.sun_path, PATH);
        if (connect(sockfd, (const sockaddr *) &addr, sizeof(addr)) < 0) {
            LOGE("连接失败  休眠");
            // 连接失败
            close(sockfd);
            sleep(1);
            // 再来继续下一次尝试
            continue;
        }
        // 连接成功
        m_parent = sockfd;
        LOGE("连接成功  父进程跳出循环");
        break;
    }
}

以上就是通过socket进行进程间通信,来实现进程保活。

结语

上面两种进程被杀重启的方式,只能实现支持大部分的手机,有部分厂商进行底层修改。这两种只是提供了两种思路方案。

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