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

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,欢迎交流。

更多相关文章

  1. android异步图片加载三之handler+线程池+消息队列模式
  2. Android进程与线程基本知识一
  3. 有关Android线程的学习
  4. Android单线程模型相关概念详解
  5. Android 多线程-----AsyncTask详解
  6. 浅析Android线程模型
  7. android > 调用拨打电话 并子线程监控然后返回跳转
  8. Android基础系列-----------Android进程/线程管理应用示例(Androi

随机推荐

  1. 相对布局中的一些常用属性
  2. 2013.09.05——— android 蓝牙聊天室之
  3. android 收到SMS操作总结
  4. Android(安卓)布局----让一个控件居底部
  5. Android检测SD卡是否可用,Android获取磁盘
  6. android textview 跑马灯 滚动效果
  7. 自定义android RadioPreference组件
  8. Android(安卓)EditText 的 android:imeOp
  9. RelativeLayout用到的一些重要的属性
  10. Android布局的一些属性值