Qt on Android Episode 7(翻译)

翻译 2015年06月02日 07:40:16

原文链接:http://www.kdab.com/qt-android-episode-7/,May 27,2015 by BogDan Vatra。

译者 foruok ,转载请注明出处。

在最近的两篇Qt on Android中学习了怎么使用基础的JNI以及如何使用外部IDE来管理Qt应用的Java部分。这章呢,我们继续前进,关注如何扩展我们的Qt on Android应用的Java部分以及怎样安全地使用JNI来交互。

这部分我们准备实现一个SD卡监听器。对于那些想使用SD卡来存储数据的应用来讲,这是很有用的一个例子,因为如果应用在收到通知后不立即关闭打开的文件,它就会被Android系统干掉。

正如我们在Episode 5中看到的那样,从C/C++代码里调用Java方法或者从Java代码里调用C/C++方法都是相当简单的,但不是所有情况都管用。为什么?

为了理解为什么不行,首先我们需要理解Qt on Android架构。

Qt on Android架构

qt on android architecture

关于架构图的几句话:

  • 左边的蓝色矩形代表Android UI线程
  • 右边的绿色矩形代表Qt的主线程(Qt主事件循环运行在其中)。如果你想了解Android UI和Qt线程的更多信息请阅读Episode 1
  • 顶部的黑色矩形是你应用中的Java部分。如你所见,它大部分运行在Android UI线程中。 Java部分运行在Qt线程中的唯一情况,是我们在Qt线程里从C/C++代码里调用它(这也是大部分JNI调用发生的地方)。
  • 底部的黑色矩形是你应用的C/C++(Qt)部分。如你所见,它大部分运行在Qt线程中。C/C++部分运行在Android UI线程的唯一情况,是我们在Android UI线程里的Java部分调用它(大部分的Java回调发生在这里)。

好啦……那么,问题是什么?嗯,问题是,有一部分Android API必须在Android UI线程中调用,而当我们在C/C++代码里调用Java方法时实际上是在Qt线程里做这件事。这就是说,我们需要有一种方法让这些代码运行在Android UI线程中而不是Qt线程中。为了从C/C++ Qt线程到Java Android UI线程实现这样的调用,我们需要三个步骤:

  1. 从C/C++ Qt线程里调用一个Java方法。这个方法会在Qt线程里执行,因此我们需要一种方法,能在Android UI线程里访问Android API。
  2. 我们的Java方法使用Activity.runOnUiThread来投递一个RunnableAndroid UI线程里。Android事件循环会在Android UI线程里执行这个Runnable。
  3. Runnable对象在Android UI线程里访问Android API。

当Java代码调用C/C++函数时也存在类似的问题,因为Java会在Android UI线程里调用我们的C/C++函数,因此我们需要一种方法在Qt线程里传递那些通知。也有三个步骤:

  1. 在 Android UI线程里调用一个C/C++函数.
  2. 使用QMetaObject::invokeMethodQt事件循环投递一个方法调用。
  3. Qt时间循环在Qt线程里执行那个函数。

扩展Java部分

在你开始之前,最好读一读Episode 6,因为你需要它来方便地管理Java文件。

第一步是创建一个定制的Activity,继承自QtActivity,并且定义一个用来投递我们的Runnable的方法。

// src/com/kdab/training/MyActivity.java
package com.kdab.training;

import org.qtproject.qt5.android.bindings.QtActivity;

public class MyActivity extends QtActivity
{
    // this method is called by C++ to register the BroadcastReceiver.
    public void registerBroadcastReceiver() {
        // Qt is running on a different thread than Android.
        // In order to register the receiver we need to execute it in the Android UI thread
        runOnUiThread(new RegisterReceiverRunnable(this));
    }
}

接下来要改变AndroidManifest.xml的默认Activity,从这样:

<activity ...
    android:name="org.qtproject.qt5.android.bindings.QtActivity"
    ... >

改成这样:

<activity ...
    android:name="com.kdab.training.MyActivity"
    ... >

我们这么做是为了确保应用启动时会实例化我们定制的Activity

第三步是定义我们的RegisterReceiverRunnable类:这个类的run方法会在Android UI线程里被调用。在run方法里我们注册我们的SDCardReceiver监听器。

// src/com/kdab/training/RegisterReceiverRunnable.java
package com.kdab.training;

import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;

public class RegisterReceiverRunnable implements Runnable
{
    private Activity m_activity;
    public RegisterReceiverRunnable(Activity activity) {
        m_activity = activity;
    }
    // this method is called on Android Ui Thread
    @Override
    public void run() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
        filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
        filter.addDataScheme("file");

        // this method must be called on Android Ui Thread
        m_activity.registerReceiver(new SDCardReceiver(), filter);
    }
}

让我们看看类的样子:

// src/com/kdab/training/SDCardReceiver.java
package com.kdab.training;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class SDCardReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // call the native method when it receives a new notificatio**SDCardReceiver**n
        if (intent.getAction().equals(Intent.ACTION_MEDIA_MOUNTED))
            NativeFunctions.onReceiveNativeMounted();
        else if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED))
            NativeFunctions.onReceiveNativeUnmounted();
    }
}

SDCardReceiver重写了onReceive方法,然后它使用声明的原生方法向C/C++代码发送通知。

最后一步是声明在SDCardReceiver里用到的原生函数:

// src/com/kdab/training/NativeFunctions.java
package com.kdab.training;

public class NativeFunctions {
    // define the native function
    // these functions are called by the BroadcastReceiver object
    // when it receives a new notification
    public static native void onReceiveNativeMounted();
    public static native void onReceiveNativeUnmounted();
}

Java部分的架构

让我们结合结构图看看Java部分的调用总结:

扩展C/C++部分

现在我们来看看怎么扩展C/C++部分。为了演示怎么做,我使用一个简单的基于QWidget的应用。

首先我们需要做的是调用registerBroadcastReceiver方法。

// main.cpp
#include "mainwindow.h"
#include <QApplication>
#include <QtAndroid>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // call registerBroadcastReceiver to register the broadcast receiver
    QtAndroid::androidActivity().callMethod<void>("registerBroadcastReceiver", "()V");

    MainWindow::instance().show();
    return a.exec();
}

// native.cpp
#include <jni.h>

#include <QMetaObject>

#include "mainwindow.h"

// define our native static functions
// these are the functions that Java part will call directly from Android UI thread
static void onReceiveNativeMounted(JNIEnv * /*env*/, jobject /*obj*/)
{
    // call MainWindow::onReceiveMounted from Qt thread
    QMetaObject::invokeMethod(&MainWindow::instance(), "onReceiveMounted"
                              , Qt::QueuedConnection);
}

static void onReceiveNativeUnmounted(JNIEnv * /*env*/, jobject /*obj*/)
{
    // call MainWindow::onReceiveUnmounted from Qt thread, we wait until the called function finishes
    // in this function the application should close all its opened files, otherwise it will be killed
    QMetaObject::invokeMethod(&MainWindow::instance(), "onReceiveUnmounted"
                              , Qt::BlockingQueuedConnection);
}

//create a vector with all our JNINativeMethod(s)
static JNINativeMethod methods[] = {
    {"onReceiveNativeMounted", "()V", (void *)onReceiveNativeMounted},
    {"onReceiveNativeUnmounted", "()V", (void *)onReceiveNativeUnmounted},
};

// this method is called automatically by Java after the .so file is loaded
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/)
{
    JNIEnv* env;
    // get the JNIEnv pointer.
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
      return JNI_ERR;

    // search for Java class which declares the native methods
    jclass javaClass = env->FindClass("com/kdab/training/NativeFunctions");
    if (!javaClass)
      return JNI_ERR;

    // register our native methods
    if (env->RegisterNatives(javaClass, methods,
                          sizeof(methods) / sizeof(methods[0])) < 0) {
      return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}

native.cpp中,我们注册了原生函数。在我们的静态原生函数里我们使用QMetaObject::invokeMethod来向Qt线程投递一个槽调用。

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    static MainWindow &instance(QWidget *parent = 0);

public slots:
    void onReceiveMounted();
    void onReceiveUnmounted();

private:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

MainWindow &MainWindow::instance(QWidget *parent)
{
    static MainWindow mainWindow(parent);
    return mainWindow;
}

// Step 6
// Callback in Qt thread
void MainWindow::onReceiveMounted()
{
    ui->plainTextEdit->appendPlainText(QLatin1String("MEDIA_MOUNTED"));
}

void MainWindow::onReceiveUnmounted()
{
    ui->plainTextEdit->appendPlainText(QLatin1String("MEDIA_UNMOUNTED"));
}

MainWindow类仅仅是在收到通知时给我们的plainText控件添加一些文字。在Android线程里调用这些函数可能大大损害我们应用的健壮性——它可能导致崩溃或不可预知的行为,因此它们必须在Qt线程里被调用。

C/C++部分的架构

在我们的架构图上,C/C++部分的调用概要如下:

Java & C/C++相互调用的结构

下面是我们已经完成的C/C++和Java之间的所有调用的架构图:

示例源码下载:Click Here

谢谢你肯花时间读这篇文章。

(译者注:BogDan Vatra真是超级nice,提供了这么多图,把Java <–> C++之间的相互调用解释得太清楚了。)


我翻译的Qt on Android Episode系列文章:

我开通了微信订阅号“程序视界”,关注即可第一时间看到我的原创文章以及我推荐的精彩文章:

程序视界

Qt的主窗口QMainWindow

Qt环境中QMainWindow表示主串口的意思。之前一直使用单独的QWidget。最近做一个基于Qt主窗口的项目。其实Qt的主串口框架有点类似于MFC中的单文档工程。个人觉得主窗口有以下几个好处。1...
  • xiaoye0028
  • xiaoye0028
  • 2016年08月04日 20:44
  • 419

QT android开发中QMainWindow的背景图片设置

这几天,需要将一个界面优化一下,需要将主界面加上一张背景图。 由于对QT本身的了解也不是很透彻,所以最初是使用的Baidu。在网上搜了一圈,无非就是使用 setStyleSheet("border-i...
  • wuqiuzhi
  • wuqiuzhi
  • 2017年11月24日 23:49
  • 75

Qt on Android Episode 4(翻译)

学习了如何搭建安卓开发环境和怎样使用 Qt on Android 之后,接下来在这篇文章里,我们将了解 Qt on Android 的几种部署系统,以及如何给 APK 签名以便能够在安卓市场上发布。...
  • foruok
  • foruok
  • 2014年04月10日 17:18
  • 9979

Qt on Android Episode 2(翻译)

这篇文章基于 Qt 5.2 !尽管你可以使用 Windows 和 Mac OSX 来开发 Android Qt apps,为了更好的使用体验我还是推荐 GNU/Linux 。...
  • foruok
  • foruok
  • 2014年01月24日 14:10
  • 10361

QT基类的概念

QT有三大窗口基类 1.QMainWindow: QMainWindow类提供一个带有菜单条,工具条和一个状态条的主应用程序窗口。主窗口通常提供一个大的中央窗口部件,以及周围菜单,工具条,...
  • hsjdw
  • hsjdw
  • 2017年03月29日 15:19
  • 222

Qt实现基本QMainWindow主窗口程序

这个实验用Qt实现基本QMainWindow主窗口 先上实验效果图      打开一个文件,读取文件类容 详细步骤: 1.打开Qt creat...
  • duxinfeng2010
  • duxinfeng2010
  • 2012年12月01日 11:17
  • 19721

Qt on Android Episode 6(翻译)

如何使用外部的IDE来调试Qt on Android应用?
  • foruok
  • foruok
  • 2015年05月28日 07:16
  • 4900

Qt on Android Episode 5(翻译)

我们已经知道了如何搭建 Qt on Android 开发环境,怎样使用 Qt on Android ,有哪些可用的部署策略以及如何为应用签名,是时候继续前进了。这篇文章,我们来讲 JNI 。...
  • foruok
  • foruok
  • 2014年12月10日 07:33
  • 5839

Qt入门-使用QT+VS2008开发windows应用程序

QT是跨平台的应用程序开发工具,闻名遐迩,下面使用VS2008结合QT开发一个应用程序。 (1)打开VS2008,新建QT工程   (2)点击下一步,这里是选择需要使用的QT库 (3)...
  • xgbing
  • xgbing
  • 2012年07月18日 14:36
  • 19778

Qt中在QMAinWindow内添加layout出现问题

在QDialog的派生类中,添加Layout,可在创建Layout对象的同时指定其父窗口,但这在QMainWindow中行不通,可能会出现" ..已经设置过布局.. "或者设置的Layout不能正常显...
  • zbw1185
  • zbw1185
  • 2015年09月29日 13:43
  • 1386
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Qt on Android Episode 7(翻译)
举报原因:
原因补充:

(最多只允许输入30个字)