Android JNI - 从C++调用Android UI线程上的函数

14

我们的游戏引擎 Cocos2d-x 在 Android 上本地运行在自己的 非 Java UI 线程 上。我们需要通过 JNIAndroid UI 线程 上调用某些 Java 函数。

为了调用 JNI 函数,我们使用了这里的 JNIHelper.h/cpp(GitHub): JniHelper.hJniHelper.cpp

例如,这段 C++ 代码:

auto retVal = JniHelper::callStaticStringMethod("org/utils/Facebook",
                         "getFacebookTokenString");
理想情况下,我们希望所有这些调用都在Android UI线程上发生,并传递一个std::function作为参数,在函数调用完成后再次在Cocos2d-x线程上调用该返回值。
调用此函数的理想方式:
auto retVal = JniHelper::callStaticStringMethod("org/utils/Facebook",
  "getFacebookTokenString", [=](std::string retVal) {
 printf("This is the retval on the C++ caller thread again: %s", retVal.c_str());
});

但是也有很多调用没有任何返回值,因此对于这些情况,在Java线程上调用它们应该更容易。


据我所见,本地到Java的调用应该在某个非主线程上发出?然后您想将调用发布到主线程,并等待结果准备就绪? - Sergio
1
谢谢你的问题,我想了一下并编辑了问题,说明了我的理想工作方式。这样说清楚了吗? - keyboard
4个回答

16

正如 @Elviss 提到的,要将您的代码发布到主线程,您应该使用 Looper。实际上,这可以在不额外处理 JNI 和创建自定义 java.lang.Runnable 以及通过复杂的JNI机制发布它的情况下完成。

Android NDK提供了一种非常轻量级和高效的方法来将本地代码发布到任意的looper中。关键点是您应该为looper提供任意文件描述符,并指定您感兴趣的文件事件(输入、输出等)。底层looper将轮询该文件描述符,并一旦事件可用,就在适当的线程上运行您的回调。

这里是一个最简单的示例(没有错误检查和拆卸):

#include <android/looper.h>
#include <unistd.h>

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "sergik", __VA_ARGS__)

static ALooper* mainThreadLooper;
static int messagePipe[2];

static int looperCallback(int fd, int events, void* data);

void someJniFuncThatYouShouldCallOnceOnMainThread() {
    mainThreadLooper = ALooper_forThread(); // get looper for this thread
    ALooper_acquire(mainThreadLooper); // add reference to keep object alive
    pipe(messagePipe); //create send-receive pipe
    // listen for pipe read end, if there is something to read
    // - notify via provided callback on main thread
    ALooper_addFd(mainThreadLooper, messagePipe[0],
                  0, ALOOPER_EVENT_INPUT, looperCallback, nullptr);
    LOGI("fd is registered");    

    // send few messages from arbitrary thread
    std::thread worker([]() {
        for(char msg = 100; msg < 110; msg++) {
            LOGI("send message #%d", msg);
            write(messagePipe[1], &msg, 1);
            sleep(1);
        }
    });
    worker.detach();
}

// this will be called on main thread
static int looperCallback(int fd, int events, void* data) {
    char msg;
    read(fd, &msg, 1); // read message from pipe
    LOGI("got message #%d", msg);
    return 1; // continue listening for events
}

这段代码会产生以下输出:

06-28 23:28:27.076 30930-30930/? I/sergik: fd is registered
06-28 23:28:27.076 30930-30945/? I/sergik: send message #100
06-28 23:28:27.089 30930-30930/? I/sergik: got message #100
06-28 23:28:28.077 30930-30945/? I/sergik: send message #101
06-28 23:28:28.077 30930-30930/? I/sergik: got message #101
06-28 23:28:29.077 30930-30945/? I/sergik: send message #102
06-28 23:28:29.078 30930-30930/? I/sergik: got message #102
06-28 23:28:30.078 30930-30945/? I/sergik: send message #103
06-28 23:28:30.078 30930-30930/? I/sergik: got message #103
06-28 23:28:31.079 30930-30945/? I/sergik: send message #104
06-28 23:28:31.079 30930-30930/? I/sergik: got message #104
06-28 23:28:32.079 30930-30945/? I/sergik: send message #105
06-28 23:28:32.080 30930-30930/? I/sergik: got message #105
06-28 23:28:33.080 30930-30945/? I/sergik: send message #106
06-28 23:28:33.080 30930-30930/? I/sergik: got message #106
06-28 23:28:34.081 30930-30945/? I/sergik: send message #107
06-28 23:28:34.081 30930-30930/? I/sergik: got message #107
06-28 23:28:35.081 30930-30945/? I/sergik: send message #108
06-28 23:28:35.082 30930-30930/? I/sergik: got message #108
06-28 23:28:36.082 30930-30945/? I/sergik: send message #109
06-28 23:28:36.083 30930-30930/? I/sergik: got message #109

从pid-tid对中可以看出,消息是在主线程上接收的。当然,您可以发送比一个字节更复杂的内容。


看起来非常酷,谢谢!你说的主线程是指Java-Main-Thread,对吗?现在你只是交换字符串吗?我有什么遗漏吗?在文本中,你说可以执行任意代码。 - keyboard
@keyboard 是的,我指的是Java主线程(实际上,Java和本地代码的主线程是相同的)。您可以从Activity.onCreate()中调用注册函数。例如,此示例不会交换任何内容:-)。它只是从工作线程发送数字100...109到主(UI)线程。它们可以被视为消息ID。如果需要,您可以发送一些序列化对象。唯一需要做的就是设计序列化-写入-读取-反序列化流程。 - Sergio
如果我理解正确的话,在android_main()中,我首先调用someJniFuncThatYouShouldCallOnceOnMainThread()。然后,每当我通过write发送消息时,looperCallback就会在与Java UI线程相同的线程上执行。换句话说,JNI代码应该在Java UI线程上执行,可以在looperCallback中执行吗? - Viktor Sehr
@Viktor Sehr,是的,这段代码片段基本上允许您这样做。严格来说,对于本机代码和Java而言,UI或主线程是相同的事情。在某些时候,它执行本机机器代码,在其他一些时候执行Java字节码。但无论如何,从内核的角度来看,它都是相同的线程。 - Sergio
1
@Sergio 嗯,如果我理解正确的话,我可以在位于android_main()内的while(true){...}循环中执行与主线程相关的操作?同时,android_app->onAppCmd等回调函数是在其他线程上执行的吗?(我有一些jni异常似乎与不在java主线程上运行jni命令有关) - Viktor Sehr
显示剩余2条评论

3
要在Android UI(主)线程上运行C++代码,您需要使用Android looper(activity.getMainLooper()或Java中的Looper.getMainLooper()):
jmethodID getMainLooperMethod = jniEnv->GetMethodID(mainActivityClass, "getMainLooper", "()Landroid/os/Looper;");
jobject mainLooper = jniEnv->CallObjectMethod(mainActivity, getMainLooperMethod);

"

“mainActivity”是android.app.Activity的一个实例,从Java传递给JNI,但您也可以简单地使用Looper类的静态getMainLooper方法。接下来,您需要创建Handler类的一个实例(在Java中为new Handler(mainLooper):

"
jclass handlerClass = jniEnv->FindClass("android/os/Handler");
jmethodID handlerConstructor = jniEnv->GetMethodID(handlerClass, "<init>", "(Landroid/os/Looper;)V");
postMethod = jniEnv->GetMethodID(handlerClass, "post", "(Ljava/lang/Runnable;)Z");
handler = jniEnv->NewObject(handlerClass, handlerConstructor, mainLooper);
handler = jniEnv->NewGlobalRef(handler);

请注意,您必须存储处理程序(jobject)以便以后使用。 您将不得不编写一些Java代码来实现Runnable接口,因此此代码将放在Java中:

package my.package;

import java.lang.Runnable;

public class Runner implements Runnable
{
    native public void run();
}

如您所见,run()方法是本地方法,因此我们可以按照以下方式在C++中实现它:

extern "C" JNIEXPORT void JNICALL 
Java_my_package_Runner_run(JNIEnv*, jclass)
{
    // here goes your native code
}

现在您需要获取C ++中的Runner类及其构造函数:
runnerClass = jniEnv->FindClass("org/ouzelengine/Runner");
runnerClass = static_cast<jclass>(jniEnv->NewGlobalRef(runnerClass));
runnerConstructor = jniEnv->GetMethodID(runnerClass, "<init>", "()V");

将runnerClass(jclass)和runnerConstructor(jmethodID)存储在某个地方以供以后使用。最后要做的事情是实际创建Runner类的实例并将其发布到处理程序:

jobject runner = jniEnv->NewObject(runnerClass, runnerConstructor);

if (!jniEnv->CallBooleanMethod(handler, postMethod, runner))
{
    // something wrong happened
}

Ouzel engines代码中,我所做的是创建一个std :: function队列,并用互斥锁进行保护。每当我需要在Android UI线程上执行std :: function时,我将std :: function实例添加到队列中,然后从队列中弹出并在本地方法(Java_my_package_Runner_run)中执行它。
这是你可以接近不编写Java代码的最佳方式(您将不得不编写6行代码来实现Runnable接口)。

为什么要让生活变得更难呢?只需从C++后台线程调用Java回调方法(将任何数据作为参数传递),然后从您的Java回调中通过调用Handler#post / Handler#sendMessage / 等将其发布到UI线程。 - pskink
我的引擎是C++,因此我希望尽可能少地使用Java,因为Java驻留在应用程序的“用户部分”,而不是我的引擎。 - Elviss Strazdins
2
与主循环器交互绝对不需要使用JNI。有NDK头文件<android/looper.h>,它公开了本地循环器的接口。请参见我的答案。 - Sergio
谢谢!我认为你的方法更好。我已经将它实现到了Ouzel引擎中。谢谢! - Elviss Strazdins
我曾经使用JNI来完成这项工作,但很快发现在C++线程中清理本地JNI引用是一场噩梦。不使用JNI的答案是一个更好的想法。 - Chris Watts

3
基于@Sergio的答案,这里提供一个简单的包装器NativeHandler,它可以接受函数、函数对象和lambda作为参数,并尝试模拟android.os.Handler的行为。
class NativeHandler {
public:
    static constexpr auto TAG = "NativeHandler";
    static NativeHandler* forCurrentThread() {
        return new NativeHandler;
    }

    template<typename FUNC, typename... ARGS>
    bool post(FUNC&& func, ARGS&&... args) {
        auto callable = new Callable(func, std::forward<ARGS>(args)...);
        write(_pipeFDS[1], &callable, sizeof(decltype(callable)));
        return true;
    }

    NativeHandler(const NativeHandler&) = delete;
    NativeHandler(NativeHandler&&) = delete;
    NativeHandler& operator=(const NativeHandler&) = delete;
    NativeHandler& operator=(NativeHandler&&) = delete;
    virtual ~NativeHandler() {
        ALooper_removeFd(_looper, _pipeFDS[0]);
        ALooper_release(_looper);
        close(_pipeFDS[0]);
        close(_pipeFDS[1]);
    }

private:
    class Callable {
    public:
        void call() {
            if (_function) _function();
        }

        template<typename FUNC, typename... ARGS>
        Callable(FUNC func, ARGS... args) : _function(std::bind(func, args...)) {}

        Callable() = delete;
        Callable(const Callable&) = delete;
        Callable(Callable&&) = delete;
        Callable operator=(const Callable&) = delete;
        Callable operator=(Callable&&) = delete;
        virtual ~Callable() {}
    private:
        std::function<void()> _function;
    };

    NativeHandler() {
        if (pipe(_pipeFDS) != 0) {
            throw std::bad_alloc();
        }
        _looper = ALooper_forThread();
        ALooper_acquire(_looper);
        if (ALooper_addFd(_looper, _pipeFDS[0], ALOOPER_POLL_CALLBACK,
                          ALOOPER_EVENT_INPUT, _looperCallback, nullptr) == -1) {
            throw std::bad_alloc();
        }
    };

    ALooper* _looper;
    int _pipeFDS[2];
    static int _looperCallback(int fd, int events, void* data) {
        void* buf = new char[sizeof(Callable*)];
        ssize_t nr = read(fd, buf, sizeof(Callable*));
        Callable* callable = *((Callable**)buf);
        __android_log_print(ANDROID_LOG_INFO, "Callable", "read size is %d %p", nr, callable);
        callable->call();
        delete[] buf;
        return 1;
    }
};

以下是使用示例,希望对任何想要在JNI中实现类似于Android Java API Handler的行为的人有所帮助。

void f(char c, short s) {
    __android_log_print(ANDROID_LOG_DEBUG, NativeHandler::TAG, "%s c = %c, s = %d", __FUNCTION__, c, s);
}

struct Task {
    void operator()(int i, double d) {
        __android_log_print(ANDROID_LOG_DEBUG, NativeHandler::TAG, "Task i = %d, d = %f", i, d);
    }
};

// ...
auto handler = NativeHandler::forCurrentThread();
std::thread worker([handler]() {
    handler->post([](int i, double d, void* p) {
        __android_log_print(ANDROID_LOG_DEBUG, "NativeHandler", "i = %d, d = %f, p = %p", i, d, p);
    }, 100, -123.4, nullptr);

    handler->post(f, 'c', 128);
    handler->post(Task(), 123, 3.1415926);
});
worker.detach();

非常好,感谢您发布这段代码!我在 _looperCallback 中进行了一些调整。首先,我只确定缓冲区大小一次:std::size_t size = sizeof(Callable*);。我不再在堆上分配缓冲区,而是使用栈内存:char buffer[size]; read(fd, static_cast<void*>(buffer), size);。因为只有一个地址被交换,所以使用栈内存应该没问题。然后我替换了 C 风格的转换:Callable* callable = *(reinterpret_cast<Callable**>(buffer)); - ackh
还要注意内存泄漏问题:每次调用 post 都会在堆上分配内存,但是这些内存从未被释放。为了解决这个问题,在 _looperCallback 的结尾处添加 delete callable; - ackh
如果你能放弃古老的管道,改用event_fd,这段代码会更好。它是为此而设计的,并且自Android 4起得到支持。 - undefined

1
另一个选择是使用 Arcana.cpp C++ 库,其中包括基于 Android Looper 的“调度程序”。在其最简单的形式中,您可以像这样使用它:
#include <arcana/threading/task_schedulers.h>

void SomeFunctionCalledFromUIThread()
{
  // Note: The '64' below is the max size of the callables passed to the scheduler.
  // This is done to reduce allocations and make schedulers more efficient.
  auto looper_scheduler = arcana::looper_scheduler<64>::get_for_current_thread();

  // Get on a background thread to test getting back on the UI thread.
  std::thread worker([looper_scheduler = std::move(looper_scheduler)]() {
    looper_scheduler([]() {
      // Do something on the UI (looper) thread
    });
  });
}

调度程序是Arcana.cpp中的一般构造,并且也用于低开销的跨平台异步任务系统,因此如果您选择使用该系统,则可以使用此调度程序进行典型的异步任务编程:

#include <arcana/threading/task_schedulers.h>
#include <arcana/threading/task.h>
#include <arcana/threading/dispatcher.h>

// Schedulers need to outlive task chains, so imagine m_looper_scheduler and m_background_dispatcher are created and stored from some constructor.
// "Dispatchers" in Arcana.cpp are a class of schedulers that own their own work queue.

arcana::task<void, std::exception_ptr> SomeFunctionCalledFromUIThread() {
  return arcana::make_task(m_background_dispatcher, arcana::cancellation::none(), []() {
    // Do something on a background thread (via background_dispatcher).
  }).then(m_looper_scheduler, arcana::cancellation::none(), []() {
    // Do something on the UI thread (via looper_scheduler).
  });
}

如果你想更加冒险,C++ 协程可以与任务或调度程序直接配合使用:
#include <arcana/threading/task_schedulers.h>
#include <arcana/threading/task.h>
#include <arcana/threading/dispatcher.h>
#include <arcana/threading/coroutine.h>

arcana::task<void, std::exception_ptr> SomeFunctionCalledFromUIThread() { 
  auto looper_scheduler = arcana::looper_scheduler<64>::get_for_current_thread();
  arcana::background_dispatcher<64> background_dispatcher;

  // Code executing here is on the UI thread (since the function is called from the UI thread).

  co_await arcana::switch_to(background_dispatcher);

  // Code executing here is on a background thread.

  co_await arcana::switch_to(looper_scheduler);

  // Code executing here is back on the UI thread.
}

您可以在此处阅读有关Arcana.cpp调度程序、任务和协程的更多信息:https://github.com/microsoft/arcana.cpp/blob/master/Source/Arcana.Tasks.md


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接