1. 程式人生 > >Android NDK雙程序守護(Socket)

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 <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <android/log.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
#include <linux/signal.h>
#include <android/log.h>
#include <stdio.h>
#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 <jni.h>
#include <string>
#include <unistd.h>
#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系統服務都是基於這個,兩條不同的線路保證系統服務的安全,我今天講的只是單工機制,好了就這麼多。