Android NDK双进程守护(Socket)

NDK双进程守护(单工机制)

最近在系统的学习Android NDK开发于是想着把刚学完的一个知识点总结写一篇博客,就是我今天要说的NDK进程守护。目前市面上的应用,貌似除了微信和手Q都会比较担心被用户或者系统(厂商)杀死的问题。而最近学的双进程守护就能很好解决这个问题,至少Service保活率在百分之七十以上,那么首先就得面临第一个问题,我们是保活整个App还是一个服务呢?

答案肯定是保活一个Service,因为当用户退出或者不使用的情况下还一直保活App?,这样很占资源不说,还会引起用户体验极差,就比如我们要玩个王者荣耀,而另外一个App一直在后台运行占用系统资源肯定会引起卡顿,所以保活一个重要Service就足够了,这里以最近学习的例子来说明。

如何保证Service不被Kill

首先查看AndroidAPI我们可以得知,当进程长期不活动,或系统需要资源时,会自动清理门户,杀死一些Service,和不可见的Activity等所在的进程。但是如果某个进程不想被杀死(如数据缓存进程,或状态监控进程,或远程服务进程)那该怎么办?可能第一想到的就是提升优先级吧,因为我之前也是。

  1. 提升service进程优先级
    通过AndroidAIP我们可以得知,Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收。Android将进程分为5个等级,它们按优先级顺序由高到低依次是:

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

当service运行在低内存的环境时,将会杀掉一些存在的进程。因此进程的优先级将会很重要,可以使用startForeground 将service放到前台状态。这样在低内存时被杀的几率会低一些。当然了网上重启方法很多,比如在onDestroy方法里重启service 利用service+broadcast等等的一些方式来重启,但是这样的保活率不是很高,下面我们来说重点。

双进程守护

如果从进程管理器观察会发现新浪微博、支付宝和QQ等都有两个以上相关进程,其中一个就是守护进程,由此可以猜到这些商业级的软件都采用了双进程守护的办法。 什么是双进程守护呢?顾名思义就是两个进程互相监视对方,发现对方挂掉就立刻重启。
这篇文章中实现双进程保护的方法基本上是纯的NDK开发,或者说全部是用C++来实现的,需要双进程保护的程序,只需要在程序的任何地方调用一下JAVA接口即可。下面几个知识点是需要了解的:

  1. linux中多进程;
  2. unix domain套接字实现跨进程通信;
  3. exec函数族的用法;
  4. 了解JNI/NDK
其实只要稍微了解一些Linux基本使用就可以了,本身并不复杂,目前主流的双进程方式有很多,可能我们最常见就是轮询,但是这种是非常消耗资源的,我之前也了解过是通过线程的方式,但是这种非常消化CPU资源,所以最近了解到一种非常有用的可以替代轮询的方式而且不占用和消耗系统资源的方式,就是利用socket方式
直接贴代码吧,首先看MainActivity.java
package com.example.panjianghao.ndk_shuangjincheng;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    /**
    *这里并没有太多操作,就是当App被打开的时候启用一个service服务
    */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this,PushService.class);
        startService(intent);
    }


}

接下来我们创建一个java类,定义所需要用到的方法,方便java调用,实际上就是搭建一个桥梁,通过这些方法可以实现java调C,C调java,全部通过JNI协议进行而NDK就是一个工具集方便我们进行NDK开发

package com.example.panjianghao.ndk_shuangjincheng;

public class Natives {
    static {
        System.loadLibrary("native-lib");
    }
    //创建服务端
    public native void Socket_Server(String id);
    //创建客户端
    public native void Socket_Client();
}

这里主要定义两个方法,一个是服务端跟客户端,是不是跟写java,socket一模一样?没错就是利用这个特性。
接下来我们看看service类是怎么实现的:

package com.example.panjianghao.ndk_shuangjincheng;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
import android.support.annotation.Nullable;
import android.util.Log;

import java.util.Timer;
import java.util.TimerTask;

public class PushService extends Service {
    public static final String TAG = "TAG";
    public int i = 0;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Timer timer = new Timer();
        Natives natives = new Natives();
        //Process.myUid()返回此进程的uid的标识符。
        natives.Socket_Server(String.valueOf(Process.myUid()));
        natives.Socket_Client();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                Log.e(TAG, "服务端开启中"+ i);
                i++;
            }
        },0,1000*3);
    }
}

可以看到这里没什么复杂操作,调用C层的方法其中Socket_Server()方法传一个ID是当服务被杀死服务端创建的时候使用相同的pid,唯一就是每当service在onCreate()方法被创建的时候循环打印,每个三秒打印一次,一值到service被杀死,第二次重启重新开始,这里只是方便我们观察service启动和被杀死之后重启之间的区分。
接下来开始就是NDK层的了
首先创建的是c++所需要用到的头文件:

//
// Created by PanJiangHao on 2018/9/8.
//

#ifndef NDK_SHUANGJINCHENG_NATIVE_LIB_H
#define NDK_SHUANGJINCHENG_NATIVE_LIB_H

#endif //NDK_SHUANGJINCHENG_NATIVE_LIB_H
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define LOG_TAG "socket"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
void work();

int create_channel();

void listen_message();

这里只是把所需要用到的头文件库文件全写在一起,方便应用。
好了下面就是最重要的:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_panjianghao_ndk_1shuangjincheng_Natives_Socket_1Server(JNIEnv *env,
                                                                        jobject instance,
                                                                        jstring id_) {
    id = env->GetStringUTFChars(id_, 0);

    /**
     * 开双进程 并且有两个返回值0和1,0代表子进程,1代表父进程
     * 这里我们只用到子进程,父进程不会用到
     */
    pid_t pid = fork();
    if (pid<0){
      //这里如果pid小于0则代表开双进程失败
    } else if (pid ==0){
        //子进程
        work();
    } else if (pid>0){
        //父进程
    }

    env->ReleaseStringUTFChars(id_, id);
}

这里就是服务端方法因为我们刚刚在Natives定义了,用了一个Linux提供的双进程方法fork();并且自定义了work();用于在C层创建服务端:

//用于开启socket
void work() {
    //把服务端分成两部分,create_channel用来连接,listen_message用来读取数据
    if (create_channel()){
        listen_message();
    }
}

注释已经写的很清楚了,主要是服务端的创建分成了两部分
第一部分:

/**
 * 创建服务端sockte
 * @return 1;
 */
int create_channel() {
    //socket可以跨进程,文件端口读写  linux文件系统  ip+端口 实际上指明文件
    int listenfd = socket(AF_LOCAL,SOCK_STREAM,0);
    unlink(PATH);
    struct sockaddr_un addr;
    //清空刚刚建立的结构体,全部赋值为零
    memset(&addr,0, sizeof(sockaddr_un));
    addr.sun_family = AF_LOCAL;
    //    addr.sun_data = PATH; 不能够直接赋值,所以使用内存拷贝的方式赋值
    strcpy(addr.sun_path,PATH);
    //相当于绑定端口号
    if (bind(listenfd,(const sockaddr*)&addr, sizeof(sockaddr_un))<0){
            LOGE("绑定错误");
    }
    int connfd = 0;
    //能够同时连接5个客户端,最大为10
    listen(listenfd,5);
    //用死循环保证连接能成功
    while(1){
        //返回客户端地址 accept是阻塞式函数,返回有两种情况,一种成功,一种失败
        if ((connfd = accept(listenfd,NULL,NULL))<0){
            //有两种情况
                if (errno == EINTR){
                    //成功的情况下continue继续往后的步骤
                    continue;
                } else{
                    LOGE("读取错误");
                }
        }
        m_child = connfd;
        LOGE("APK 父进程连接上了 %d", m_child);
        break;
    }
    return 1;

}

这里主要类似于java Socket的方式,来创建端口跟IP,过程是一样的,只是方式不一样,这里创建服务端完全按照Linux的方式来的,有兴趣可以去看看java socket的底层实现也是基于这样的方式,而且同样是利用循环使用accept() 不断监听来自服务端的链接

/**
 * 服务端读取信息
 * 客户端
 */
void listen_message() {
    fd_set fdSet;
    struct timeval timeval1{3,0};
    while(1){
        //清空内容
        FD_ZERO(&fdSet);
        FD_SET(m_child,&fdSet);
        //如果是两个客户端就在原来的基础上+1以此类推,最后一个参数是找到他的时间超过3秒就是超时
        //select会先执行,会找到m_child对应的文件如果找到就返回大于0的值,进程就会阻塞没找到就不会
        int r = select(m_child+1,&fdSet,NULL,NULL,&timeval1);
        if(r>0){
            //缓冲区
            char byte[256] = {0};
            //阻塞式函数
            LOGE("读取消息后 %d", r);
            read(m_child,byte, sizeof(byte));
            LOGE("在这里===%s", id);
            //不在阻塞,开启服务
            //新进程与原进程有相同的PID。
            execlp("am","am","startservice", "--user", id,
                   "com.example.panjianghao.ndk_shuangjincheng/com.example.panjianghao.ndk_shuangjincheng.PushService",
                   (char *)NULL);
            break;
        }
    }

}

这里就是服务端建立之后读取消息的操作,因为我们只是建立连接并不需要读取消息什么的,只是想知道有没有阻塞,如果没用就证明服务被杀死进程不会阻塞,然后就执行execlp重新调启service服务,当然如果阻塞证明service还活着就不需要重新调启service
接下来就是客户端:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_panjianghao_ndk_1shuangjincheng_Natives_Socket_1Client(JNIEnv *env,
                                                                        jobject instance) {
    //客户端进程调用
    int socked;
    struct sockaddr_un addr;
    while(1){
        LOGE("客户端父进程开始连接");
        socked = socket(AF_LOCAL, SOCK_STREAM, 0);
        if (socked < 0) {
            LOGE("连接失败");
            return;
        }
        memset(&addr, 0, sizeof(sockaddr_un));
        addr.sun_family = AF_LOCAL;
//    addr.sun_data = PATH; 不能够直接赋值
        strcpy(addr.sun_path, PATH);
        if(connect(socked, (const sockaddr *)(&addr), sizeof(sockaddr_un)) < 0){
            LOGE("连接失败");
            //如果连接失败了就关闭当前socked,休眠一秒重新开始连接
            close(socked);
            sleep(1);
            continue;
        }
        LOGE("连接成功");
        break;
    }

}

这里跟创建服务端方法是一样的同样使用connect()根据相同的协议IP去寻找服务端并且建立连接。这样就成功了
当然还要最重要的:

//通过文件读写,进行socket通信
char const *PATH = "/data/data/com.example.panjianghao.ndk_shuangjincheng/and.sock";

这是什么意思呢,Linux完全是基于文件的,所以Linux层的socket通信是基于文件的也就是说服务端创建and.sock,客户端跟服务端通信完全是基于这个文件,java通过流的方式,而Linux不同是通过相同的文件通信的,也就是端口的意思
好了下面贴出完整代码:

#include 
#include 
#include 
#include "native_lib.h"

/**
 * 2018-9-8
 */

//通过文件读写,进行socket通信
char const *PATH = "/data/data/com.example.panjianghao.ndk_shuangjincheng/my.sock";
int m_child;
const char *id;

extern "C" JNIEXPORT jstring
JNICALL
Java_com_example_panjianghao_ndk_1shuangjincheng_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_panjianghao_ndk_1shuangjincheng_Natives_Socket_1Server(JNIEnv *env,
                                                                        jobject instance,
                                                                        jstring id_) {
    id = env->GetStringUTFChars(id_, 0);

    /**
     * 开双进程 并且有两个返回值0和1,0代表子进程,1代表父进程
     * 这里我们只用到子进程,父进程不会用到
     */
    pid_t pid = fork();
    if (pid<0){
      //这里如果pid小于0则代表开双进程失败
    } else if (pid ==0){
        //子进程
        work();
    } else if (pid>0){
        //父进程
    }

    env->ReleaseStringUTFChars(id_, id);
}
//用于开启socket
void work() {
    if (create_channel()){
        listen_message();
    }
}
/**
 * 创建服务端sockte
 * @return 1;
 */
int create_channel() {
    //socket可以跨进程,文件端口读写  linux文件系统  ip+端口 实际上指明文件
    int listenfd = socket(AF_LOCAL,SOCK_STREAM,0);
    unlink(PATH);
    struct sockaddr_un addr;
    //清空刚刚建立的结构体,全部赋值为零
    memset(&addr,0, sizeof(sockaddr_un));
    addr.sun_family = AF_LOCAL;
    //    addr.sun_data = PATH; 不能够直接赋值,所以使用内存拷贝的方式赋值
    strcpy(addr.sun_path,PATH);
    //相当于绑定端口号
    if (bind(listenfd,(const sockaddr*)&addr, sizeof(sockaddr_un))<0){
            LOGE("绑定错误");
    }
    int connfd = 0;
    //能够同时连接5个客户端,最大为10
    listen(listenfd,5);
    //用死循环保证连接能成功
    while(1){
        //返回客户端地址 accept是阻塞式函数,返回有两种情况,一种成功,一种失败
        if ((connfd = accept(listenfd,NULL,NULL))<0){
            //有两种情况
                if (errno == EINTR){
                    //成功的情况下continue继续往后的步骤
                    continue;
                } else{
                    LOGE("读取错误");
                }
        }
        m_child = connfd;
        LOGE("APK 父进程连接上了 %d", m_child);
        break;
    }
    return 1;

}
/**
 * 服务端读取信息
 * 客户端
 */
void listen_message() {
    fd_set fdSet;
    struct timeval timeval1{3,0};
    while(1){
        //清空内容
        FD_ZERO(&fdSet);
        FD_SET(m_child,&fdSet);
        //如果是两个客户端就在原来的基础上+1以此类推,最后一个参数是找到他的时间超过3秒就是超时
        //select会先执行,会找到m_child对应的文件如果找到就返回大于0的值,进程就会阻塞没找到就不会
        int r = select(m_child+1,&fdSet,NULL,NULL,&timeval1);
        if(r>0){
            //缓冲区
            char byte[256] = {0};
            //阻塞式函数
            LOGE("读取消息后 %d", r);
            read(m_child,byte, sizeof(byte));
            LOGE("在这里===%s", id);
            //不在阻塞,开启服务
            //新进程与原进程有相同的PID。
            execlp("am","am","startservice", "--user", id,
                   "com.example.panjianghao.ndk_shuangjincheng/com.example.panjianghao.ndk_shuangjincheng.PushService",
                   (char *)NULL);
            break;
        }
    }

}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_panjianghao_ndk_1shuangjincheng_Natives_Socket_1Client(JNIEnv *env,
                                                                        jobject instance) {
    //客户端进程调用
    int socked;
    struct sockaddr_un addr;
    while(1){
        LOGE("客户端父进程开始连接");
        socked = socket(AF_LOCAL, SOCK_STREAM, 0);
        if (socked < 0) {
            LOGE("连接失败");
            return;
        }
        memset(&addr, 0, sizeof(sockaddr_un));
        addr.sun_family = AF_LOCAL;
//    addr.sun_data = PATH; 不能够直接赋值
        strcpy(addr.sun_path, PATH);
        if(connect(socked, (const sockaddr *)(&addr), sizeof(sockaddr_un)) < 0){
            LOGE("连接失败");
            //如果连接失败了就关闭当前socked,休眠一秒重新开始连接
            close(socked);
            sleep(1);
            continue;
        }
        LOGE("连接成功");
        break;
    }

}

利用socket的这种方式和思路能很好解决,使用轮询所带来的资源消耗问题,大概就是service先调用Socket_Server(String id)先建立服务端,然后Socket_Client()跟服务端建立连接,一旦service被杀死Socket_Client()得不到执行就会跟服务端失去连接,这个时候listen_message()里的select就不会阻塞,进而就会执行execlp重新启用service大概就是这样,其他的代码里注释已经写的很清楚了,当然还有双工机制,所有Android系统服务都是基于这个,两条不同的线路保证系统服务的安全,我今天讲的只是单工机制,好了就这么多。

你可能感兴趣的:(Android NDK双进程守护(Socket))