這篇文章要基于前面的基礎,我們才能繼續下面的內容,建議閱讀,
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年顯得很特別),這決定了我們的方案需要安裝兩個應用,
方法和思路:
- A啟動后向B發送登錄請求,建立通信,B開啟定時器,開始監測A的資料,通信的實作方式不限,可以是socket,或者廣播
- 通信建立后A立即開始向B發送心跳,每1s一個心跳包,
- 假如發生崩潰,B沒有收到A的心跳包,則重新拉起A,
- 假如發生卡死,B沒有收到A的心跳包,則重新拉起A,
- 正常退出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的全部內容,再來梳理下它的作業:
- 監聽login請求,并開啟心跳檢測,
- 隨時注意心跳是否斷開,斷開則拉起A應用,
- 監聽logout請求,避免定時器空跑,保證A的正常退出,而不是當做崩潰處理,
- 監聽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,歡迎交流,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/243614.html
標籤:其他
上一篇:干貨
下一篇:干貨-ubuntu 16.04
