这篇文章要基于前面的基础,我们才能继续下面的内容,建议阅读。
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年显得很特别),这决定了我们的方案需要安装两个应用。
方法和思路:
其实思路很简单,但是其实在开发的时候碰到一个问题,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的全部内容,再来梳理下它的工作:
应用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,欢迎交流。