专栏首页包子的书架Android 进程保活 的两种实现方式

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

前言

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

差强人意的方法

网上给一些常见的方法:

  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 当前进程的父进程号

开始撸码

由于上面讲的内容都是在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

对应的是地址的长度

开始撸码

  • 创建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进行进程间通信,来实现进程保活。

结语

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • FFmpeg 内容介绍 音视频解码和播放

    FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案...

    包子388321
  • Android TextView处理html的图片和<a>标签事件

    https://gitee.com/adminfun/HTMLTextView https://blog.csdn.net/songmingzhan/arti...

    包子388321
  • 简单的通过demo了解C++的基础语法笔记

    许久未碰C++的相关内容,已经有些被大脑的程序执行Lru算法了,导致近期在做NDK开发的时候,各种操作卡顿,决心还是回忆整理一下相关的基础知识。

    包子388321
  • Java Web Response对象的27个方法及状态码

    response表示HttpServletResponse对象,主要将JSP容器处理后的结果传回到客户端。 ? 网络配图 1、void addCookie(...

    用户1289394
  • python小整数对象池

    整数在程序中使用非常广泛,python为了优化速度,使用了小整数对象池,避免为了整数频繁申请和销毁内存空间. python对小整数的定义时[-5,257]这些书...

    py3study
  • 如果你买了新电脑,一定要做到这5个关键设置,别怪我没提醒你

    电脑已经成为我们生活中必备的工具,我们经常用来办公、游戏、看视频,可是如果你买了新电脑,一定要做到下面这5个关键设置,可以更好的帮助到你。

    办公小达人
  • 肺癌:流行病学和生信病理

    根据国家癌症中心最新发布的《2015年中国恶性肿瘤流行情况分析》,肺癌高居恶性肿瘤发病率榜首,在392.9万例新发恶性肿瘤病例中,约有1/5都是肺癌患者(全球发...

    芒果先生聊生信
  • ios设备突破微信小视频6S限制的方法

      刷微信朋友圈只发文字和图片怎能意犹未竟,微信小视频是一个很好的补充,音视频到位,流行流行最流行。但小视频时长不能超过6S,没有滤镜等是很大的遗憾。but有人...

    ytkah
  • 肺癌:早期治疗快过感冒?是真的!

    根据国家癌症中心最新发布的《2015年中国恶性肿瘤流行情况分析》,肺癌高居恶性肿瘤发病率榜首,在392.9万例新发恶性肿瘤病例中,约有1/5都是肺癌患者(全球发...

    芒果先生聊生信
  • SpringCloud Zuul2.X网关实现服务熔断降级(复制即用)

    Arebirth

扫码关注云+社区

领取腾讯云代金券