Qt for Android(九) ——APP 崩溃卡死拉起保活实战

1,096 阅读9分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

这篇文章要基于前面的基础,我们才能继续下面的内容,建议阅读。

Qt for Android(一) —— QT 中如何调用android方法 Qt for Android(二) —— QT 中调用自定义Android方法详细教程(获取Android设备的SN号)

背景

首先,本文的案例环境基于一些特殊的 android 设备,比如瑞星微的RK系列,在该设备上不会熄屏,没有锁屏键,运行的应用也仅限于几个 APP,大部分不会存在应用被系统杀死的可能。

应用拉起说白了就是进程保活,关于Android 的进程保活文章有很多,但是本文是基于 QT for Android 的开发,因此过程可能有些许不同,同时针对的场景也不同,因此在操作上可能更有针对性。

由于我们的应用属于广告播放类 APP, 需要长时间的稳定运行,但不可避免的由于某种原因 APP 发生崩溃或者界面卡死,为了尽可能的减小损失,因此我们需要在发生上述情况时重新启动我们的APP。

分析

假设我们的主应用称为A,而为了做到进程保活,我们需要另一个进程B,称之为Monitor,即监视进程,也可以称为守护进程(“守护”,这个词在2020年显得很特别),这决定了我们的方案需要安装两个应用。

方法和思路:

  1. A启动后向B发送登录请求,建立通信,B开启定时器,开始监测A的数据,通信的实现方式不限,可以是socket,或者广播
  2. 通信建立后A立即开始向B发送心跳,每1s一个心跳包。
  3. 假如发生崩溃,B没有收到A的心跳包,则重新拉起A。
  4. 假如发生卡死,B没有收到A的心跳包,则重新拉起A。
  5. 正常退出A的时候向B发送登出请求,停止心跳,防止B误以为A死亡而被拉起。

其实思路很简单,但是其实在开发的时候碰到一个问题,QT的事件循环和Android的事件循环互不干扰,即QT的卡死不会影响到Android层的事件。为了解决这个问题,就往下看具体的代码。

代码详述

应用B之MonitorServices:

package com.qht.b;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Calendar;
import java.util.Timer;
import java.util.TimerTask;

public class MonitorService extends Service {
    public static final String CLASS_NAME = "MonitorService";
    private Thread thread;
    private DatagramSocket socket = null;
    private Context m_context;
    private long lastTimeMillis = 0; //代表了最后一次收到A应用心跳包的时间戳
    Timer timer = null;
    TimerTask task;
    public MonitorService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(CLASS_NAME, "onBind !!");
        return null;
    }
 @Override
    public void onCreate() {
        super.onCreate();
        m_context = this;
        Log.d(CLASS_NAME, "onCreate !!");
        lastTimeMillis = 0;
        thread=new Thread(new Runnable()
        {
            @Override

           public void run()
            {
                try {
                    System.out.println("监听端口16667");
                    socket = new DatagramSocket(16667);
                    socket.setSoTimeout(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                while (true) {
                  byte data[] = new byte[1024];
                    DatagramPacket packet = new DatagramPacket(data, data.length);
                    try {
                        socket.receive(packet);
                    } catch (SocketTimeoutException e) {
                        System.out.println("socket 10s 超时:" + e.getMessage());
                    } catch (SocketException e) {
                        System.out.println("socket SocketException:" + e.getMessage());
                       e.printStackTrace();
                    } catch (IOException e) {
                        System.out.println("socket IOException:" + e.getMessage());
                        e.printStackTrace();
                    }
                    String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
                    //校验包
                    if (result.equals("hertbeat"))
                   {
                        lastTimeMillis =  System.currentTimeMillis();
                        System.out.println("rec : hertbeat");
                    }else if (result.equals("login"))
                    {
                      //login
                        lastTimeMillis =  System.currentTimeMillis();
                        startTimer();
                        System.out.println("rec : login");
                    } else if (result.equals("logout"))
                    {
                                           //退出取消,等待login再开启
                        System.out.println("rec : logout timer.cancel()");
                        stopTimer();
                    } else if (result.equals("anr"))
                    {
                        //退出取消,等待login再开启
                        System.out.println("rec : anr restartApp");
                        restartApp();
                    }
                }
            }
        });
        thread.start();
    }
  @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        Log.d(CLASS_NAME, "onStart !!");
    }
   private void startTimer(){
        if (timer == null) {
            timer = new Timer();
        }

        if (task == null) {
            task = new TimerTask() {
                @Override
                public void run() {
                    System.out.println("run TimerTask");
                    if (lastTimeMillis != 0 &&  System.currentTimeMillis()- lastTimeMillis > 2000)
                    {
                        System.out.println("失去心跳,拉起APPlastTimeMillis :" + lastTimeMillis + ":" + (Calendar.getInstance().getTimeInMillis() - lastTimeMillis) );
                        //  心跳超时,杀死并拉起
                        restartApp();
                    }
                                    }
            };
        }
        if(timer != null && task != null )
            timer.schedule(task,0,1000);
    }
   private void restartApp() {
        killProcess(ConstantUtil.PACKAGE_NAME);
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        PackageManager localObject = m_context.getPackageManager();
        if (PackageUtil.checkPackInfo(m_context, ConstantUtil.PACKAGE_NAME)) {
            Log.i(CLASS_NAME, "find package, ready to lanunch! "+ConstantUtil.PACKAGE_NAME);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
                Log.i(CLASS_NAME, "Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE "+ (int)Build.VERSION.SDK_INT);
                m_context.startActivity((localObject).getLaunchIntentForPackage(ConstantUtil.PACKAGE_NAME));
          }
        } else {
            Log.i(CLASS_NAME, "not find package!" + ConstantUtil.PACKAGE_NAME);
        }
        lastTimeMillis = 0;
        stopTimer();
    }
   private void stopTimer(){
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        if (task != null) {
            task.cancel();
            task = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(CLASS_NAME, "onDestroy !!");
        stopTimer();
    }
        /**
     * 结束进程
     */
    private void killProcess(String packageName) {
        Process process = Runtime.getRuntime().exec("su");
        OutputStream out = process.getOutputStream();
        String cmd = "am force-stop " + packageName + " \n";
        try {
            out.write(cmd.getBytes());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在 Monitor内部,我们维护了一个定时器Timer,需要不停的检测A应用的心跳数据。44行我们首先开启一个工作线程去监听一个udp端口,我这边采用的是udp通信,因为我只需要收到A应用的心跳即可。由于socket的receive函数是阻塞式的,因此我们在线程内部开启while循环接受数据,收到的数据类型分为4种:

login: 代表A应用上线,这个时候我们开启定时器即可。 logout: 代表A应用下线,这个时候我们关闭定时器即可。 hertbeat: 代表A应用发送的心跳数据,这个时候我们主需要不停的更新 lastTimeMillis (代表了最后一次收到A应用心跳包的时间戳)这个值即可。 anr: 代表A应用发生卡死,这个时候我们需要调用restartApp方法强制杀死A应用并重启它。

当然,services不能自己启动,需要一个activity去启动它,同时也要注册到manifest文件中。

        //启动
        Intent intent = new Intent(MainActivity.this, MonitorService.class);
        startService(intent);
        <service android:name="com.qht.b.MonitorService" >
        </service>

上面就是我们MonitorServices的全部内容,再来梳理下它的工作:

  1. 监听login请求,并开启心跳检测。
  2. 随时注意心跳是否断开,断开则拉起A应用。
  3. 监听logout请求,避免定时器空跑,保证A的正常退出,而不是当做崩溃处理。
  4. 监听anr消息,收到anr消息则重启A应用。

应用A之TestApp:

最开始我是将A应用通信的代码放到Android的Service中的,但是经过测试,在频繁的崩溃拉起后,有时候会出现拉起失败的情况,具体原因和A应用包含的服务有关。而通过之前的文章我们已经知道了我们的QT程序都有一个入口Activity,因此我将通信的代码放到了这个入口Activity中。

package com.qht.a;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.WindowManager;
import android.view.KeyEvent;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;


public class MainActivity extends org.qtproject.qt5.android.bindings.QtActivity {

    DatagramSocket socket= null;
    InetAddress serverAddress = null;
    private boolean isStop = false;//logout,停止心跳
    private int lasttick, mTick;//两次计数器的值
    private Handler mHandler = new Handler();
    private boolean isNotAnr = true;//是否anr标识
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        anrDetection();
        loginAndHert();
    }
private  void loginAndHert() {
  System.out.println("开始 loginAndHert");
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(300);
                        if (socket == null)
                        {
                            System.out.println("绑定 16666 端口");
                            socket = new DatagramSocket(16666);
                        }
  						System.out.println("udp 使用回环地址 : 127.0.0.1");
                        serverAddress = InetAddress.getByName("127.0.0.1");
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    } catch (SocketException e) {
                        e.printStackTrace();
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
 					String sendData = "login";
					 byte data[] = sendData.getBytes();
                    DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, 16667);
                    System.out.println("发送给 16667 端口,被monitor服务监听");
                    try {
                        socket.send(packet);
                        System.out.println("socket.send:" + sendData + ",登录后300ms,每隔1s发送一次心跳包");
                        Thread.sleep(300);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
 					//上线后发送心跳
                    while (!isStop) {
                        try {
                            String sendData2 = "hertbeat";
                            byte data2[] = sendData2.getBytes();
                            DatagramPacket packet2 = new DatagramPacket(data2, data2.length, serverAddress, 16667);
                            socket.send(packet2);
                            System.out.println("socket.send:" + sendData2);
                            Thread.sleep(1000);
							 } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
            System.out.println("按下了back键   onKeyDown() send logout,500ms after System.exit(0)");
            logout();
            return false;
        }else {
            return super.onKeyDown(keyCode, event);
        }
    }
  private  void logout(){
        System.out.println("退出 MainActivity");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
 					isStop = true;
                    String sendData = "logout";
                    byte data[] = sendData.getBytes();
                    DatagramPacket packet = new DatagramPacket(data, data.length, serverAddress, 16667);
                    socket.send(packet);
                    System.out.println("socket.send:" + sendData);
  					Thread.sleep(500);
                    System.exit(0);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

 /*
    * 卡死监测原理描述:利用service中的线程向主线程发送mTick+1,然后线程睡眠5s后,再去检测这个值是否被改变,没改变的话说明主线程卡死了,主线程卡死后直接退出进程,等待最多2s后monitor拉起
    * */
    private void anrDetection() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isNotAnr) {
                    lasttick = mTick;
                    mHandler.post(tickerRunnable);//向主线程发送消息 计数器值+1
 				try {
                        Thread.sleep(8000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(" mTick :" + mTick + "lasttick:" + lasttick);
                    if (mTick == lasttick) {
 						isNotAnr = false;
                        Log.e("QHT", "anr happned in here");
                        try {
                            handleAnrError();
                        } catch (SocketException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }
 //发生anr的时候,在此处写逻辑
    private void handleAnrError() throws SocketException {
        System.out.println("ANR exit ,wait monitor 拉起");
        System.exit(0);
    }

    private final Runnable tickerRunnable= new Runnable() {
        @Override
        public void run() {

            mTick = (mTick + 1) % 10;
        }
    };
}

在上面30行的时候我们从Activity的onCreate方法开始,也就是从应用A启动那一刻开始,就调用loginAndHert方法向应用B发送login请求,因为应用A不需要接受数据,因此无法确认login是否发送成功,但是使用回环地址不会存在失败的情况,因此我们延迟300ms后再去每一秒发送一次心跳。

在MainActivity中我们也监听了返回键,当收到返回键时我们认为应用被正常退出,因此我们调用了logout方法,告诉MonitorServices程序是正常退出的。

到这儿,其实就已经完成了应用A和MonitorServices的基础通信了,假如此时应用A突发崩溃,则自然而然的没有心跳包了,MonitorServices就会拉起应用A。

没错,关于崩溃拉起的工作算是完了,但是Android Activity ANR呢? QT程序block呢?

重点

oncreate() 方法中,我们还调用了一个anrDetection() 方法,这便是我们Android层的ANR检测方法。它的原理是这样的:

在应用一开始UI线程中初始化两个变量tick1和tick2为同一个值,然后开启一个工作线程,并向UI线程post一个tick1的+1请求,tick2不变。然后工作线程sleep几秒钟,模拟anr的发生,sleep结束后,再去判断这两个值是否相等,如果相等,则说明tick1没有被+1,也就是说主线程没有处理这个+1请求,那必然是主线程卡住了,则我们认为此时应用发生了ANR;若这两个值不相等,或者说tick1=tick2+1,则说明主线程处理了这个+1请求,主线程工作正常,,程序继续运行。

在上面的代码中我偷懒了,当发生anr时我强制通过system.exit函数退出进程,然后MonitorServices检测不到心跳了就会拉起应用A,其实在这儿也可以向MonitorServices发送一个"anr"消息,让MonitorServices主动去处理。

上面的代码解决了我们QT程序 Android 层的卡死问题,但往往这是不多见的,因为这个Activity没有什么高负荷的工作,一般是不会卡死的。出问题总是会出在我们的QT程序内部。碰巧的是,QT程序内部卡死,MainActivity却不会卡死,即呼应了我上面提到的两者的事件循环是独立的。

但我认为,这个检测卡死的思想是想通的。因此我尝试将anrDetection()方法移植到QT程序中,发现完全可行。

void AndroidDaemonMonitor::start()
{
     qDebug() << "QHT udp client thread start";
    m_thread = std::thread([this]()
    {
        while (isNotAnr) {
            qDebug() << "QHT isNotAnr threadID:" << QThread::currentThreadId();
            lasttick = mTick;
            emit signalTickChange();
            std::this_thread::sleep_for(std::chrono::milliseconds(8000));
            qDebug() << "QHT  mTick :" <<  mTick << ",lasttick:" << lasttick;
            if (mTick == lasttick) {
                isNotAnr = false;
                qDebug() << "QHT anr happned in here";
                std::string str = "anr";
                int sendres = m_udpClient->send(str.data(), str.length(), "127.0.0.1", 16667);
                std::this_thread::sleep_for(std::chrono::milliseconds(300));
            }
        }
    });
    m_thread.detach();
}

void AndroidDaemonMonitor::slotTickChange()
{
    qDebug() << "QHT slotTickChange threadID:" << QThread::currentThreadId();
   //向主线程发送消息 计数器值+1
   mTick = (mTick + 1) % 10;
}

看见没,两者的代码几乎是一样的,不同的是,使用QT中的信号槽取代了Android中的handler.post方法,但都是在主线程中去执行+1操作。

在 QT 代码中,我没有像android那样直接退出程序,比如:qApp.exit() 等,因为你会发现根本退不了,因此我只能向MonitorServices发送"anr"消息,等待MonitorServices杀死并重启应用A。

下面我附上本次测试的两个APP源码,希望对你有所帮助,如有问题,添加我的微信,q2nAmor,欢迎交流。

Demo 下载地址:download.csdn.net/download/u0…