最近在系统的学习Android NDK开发于是想着把刚学完的一个知识点总结写一篇博客,就是我今天要说的NDK进程守护。目前市面上的应用,貌似除了微信和手Q都会比较担心被用户或者系统(厂商)杀死的问题。而最近学的双进程守护就能很好解决这个问题,至少Service保活率在百分之七十以上,那么首先就得面临第一个问题,我们是保活整个App还是一个服务呢?
答案肯定是保活一个Service,因为当用户退出或者不使用的情况下还一直保活App?,这样很占资源不说,还会引起用户体验极差,就比如我们要玩个王者荣耀,而另外一个App一直在后台运行占用系统资源肯定会引起卡顿,所以保活一个重要Service就足够了,这里以最近学习的例子来说明。
首先查看AndroidAPI我们可以得知,当进程长期不活动,或系统需要资源时,会自动清理门户,杀死一些Service,和不可见的Activity等所在的进程。但是如果某个进程不想被杀死(如数据缓存进程,或状态监控进程,或远程服务进程)那该怎么办?可能第一想到的就是提升优先级吧,因为我之前也是。
提升service进程优先级
通过AndroidAIP我们可以得知,Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收。Android将进程分为5个等级,它们按优先级顺序由高到低依次是:
当service运行在低内存的环境时,将会杀掉一些存在的进程。因此进程的优先级将会很重要,可以使用startForeground 将service放到前台状态。这样在低内存时被杀的几率会低一些。当然了网上重启方法很多,比如在onDestroy方法里重启service 利用service+broadcast等等的一些方式来重启,但是这样的保活率不是很高,下面我们来说重点。
如果从进程管理器观察会发现新浪微博、支付宝和QQ等都有两个以上相关进程,其中一个就是守护进程,由此可以猜到这些商业级的软件都采用了双进程守护的办法。 什么是双进程守护呢?顾名思义就是两个进程互相监视对方,发现对方挂掉就立刻重启。
这篇文章中实现双进程保护的方法基本上是纯的NDK开发,或者说全部是用C++来实现的,需要双进程保护的程序,只需要在程序的任何地方调用一下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系统服务都是基于这个,两条不同的线路保证系统服务的安全,我今天讲的只是单工机制,好了就这么多。