在Android JNI中从任何线程中查找类(FindClass)

51
安卓JNI技巧页面提到了这个FAQ:为什么FindClass找不到我的类?,其中列举了多种解决方案,最后一个方案是:

在方便的地方缓存ClassLoader对象的引用,并直接发出loadClass调用。这需要一些努力。

所以,我尝试让它工作,但似乎无论如何,这种方法对我都根本不起作用。最终,我想出了如何使用ClassLoader,但如果从本地线程中尝试加载尚未被触摸/加载的类,则它将无法工作。实质上,在从本地线程调用时,它与env->FindClass的行为相同,唯一的例外是对于已在应用程序中使用的类,它不会返回0。任何想法,如果我理解得不对或者从本地线程中访问尚未使用/加载的类是不可能的。
编辑:我将提供更多信息来解释我的意思。有常规的JNI env->FindClass(className),还有另一个我编写的myFindClass(env, className),它使用缓存的ClassLoader->loadClass。
我正在尝试从本地C/C++访问的类是“com/noname/TestClient”。在myFindClass中,我还使用env->FindClass并记录它返回的值:
jclass myFindClass(JNIEnv * env, const char* name)
{
    ...
    jclass c0 = env->FindClass(name);
    jclass c1 = (jclass)env->CallObjectMethod(ClassLoader,
        MID_loadClass, envNewStringUTF(name));
    dlog("myFindClass(\"%s\") => c0:%p, c1:%p, c0 and c1 are same: %d",
        name, c0, c1, env->IsSameObject(c0, c1));
    ...
}

然后,我有以下3种组合来解释这个问题。 1)
//inside JNI_OnLoad thread
myFindClass(env, "com/noname/TestClient");
...

//inside native thread created by pthread_create
myFindClass(env, "com/noname/TestClient");

我得到了这个logcat:

myFindClass("com/noname/TestClent") => c0:0x41b64558, c1:0x41b64558, c0和c1相同: 1
...
myFindClass("com/noname/TestClent") => c0:0, c1:0x41b64558, c0和c1不同: 0

2)
//inside JNI_OnLoad thread
env->FindClass("com/noname/TestClient");
...

//inside native thread created by pthread_create
myFindClass("com/noname/TestClient");

我得到了这个logcat:
我的findClass("com/noname/TestClent") => c0:0,c1:0x41b64558,c0和c1是相同的:0
3)
//inside JNI_OnLoad thread
//"com/noname/TestClient" isn't touched from JNI_OnLoad.
...

//inside native thread created by pthread_create
myFindClass(env, "com/noname/TestClient");

我得到了以下的logcat:
我的问题在于ClassLoader在第三种情况下无法找到我的类。这是一个错误吗?有什么方法可以解决这个问题吗?
此外,看起来ClassLoader::loadClass是有缺陷的。如果我要求myFindClass("noname/TestClent"),它会返回一些垃圾,并且当我以任何方式使用该返回的jclass时,应用程序会崩溃。

是的,这很正常,Android应用程序默认不使用系统类加载器。只需在JNI_OnLoad()中缓存所需的所有内容,问题就会得到解决。 - Samuel Audet
1
回复:编辑2:听起来该方法抛出了异常,此时返回值是未定义的。 loadClass()永远不会返回null;它要么返回类引用,要么抛出异常。 - fadden
@fadden 可能是这样。我不记得具体问题是什么,但最终我解决了它。 - Pavel P
2个回答

81

经过多次尝试和我的应用程序崩溃,我和一位同事成功地将类加载器缓存并在另一个本地线程中成功使用。我们使用的代码如下(C++11,但很容易转换为C++2003),在此发布,因为我们找不到任何上述“在方便的地方缓存对ClassLoader对象的引用,并直接发出loadClass调用。这需要一些努力。”的示例。当从与JNI_OnLoad不同的线程调用findClass时,它可以完美地工作。希望这可以帮助您。

JavaVM* gJvm = nullptr;
static jobject gClassLoader;
static jmethodID gFindClassMethod;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
    gJvm = pjvm;  // cache the JavaVM pointer
    auto env = getEnv();
    //replace with one of your classes in the line below
    auto randomClass = env->FindClass("com/example/RandomClass");
    jclass classClass = env->GetObjectClass(randomClass);
    auto classLoaderClass = env->FindClass("java/lang/ClassLoader");
    auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
                                             "()Ljava/lang/ClassLoader;");
    gClassLoader = env->CallObjectMethod(randomClass, getClassLoaderMethod);
    gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
                                    "(Ljava/lang/String;)Ljava/lang/Class;");

    return JNI_VERSION_1_6;
}

jclass findClass(const char* name) {
    return static_cast<jclass>(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStringUTF(name)));
}

JNIEnv* getEnv() {
    JNIEnv *env;
    int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6);
    if(status < 0) {    
        status = gJvm->AttachCurrentThread(&env, NULL);
        if(status < 0) {        
            return nullptr;
        }
    }
    return env;
}

7
当使用ClassLoader.loadClass()加载类时,如果找不到该类,它不会返回null,而是会抛出异常。因此返回值是未定义的。在调用CallObjectMethod后必须检查异常,并且如果引发了异常,则不能使用返回值。这通常适用于任何Call*Method调用,都应该跟随ExceptionCheck或ExceptionOccurred,并且可以使用ExceptionDescribe在日志中查看异常信息。 - fadden
6
我明白,我只是想确保任何复制它的人都在JNI_OnLoadfindClass中添加了必要的检查(特别是后者--您可以将异常转换为NULL,并清除异常,以获得所需的语义)。请注意,您无需调用FindClass来查找java.lang.Class;您可以在已经拥有的jclass上使用GetObjectClass(它更快且永远不会在有效对象上失败)。您是否尝试在Android >= 4.0上运行此代码?看起来您需要在gFindClassMethod上使用NewGlobalRef - fadden
1
很好。顺便说一下,我指的是gClassLoader——gFindClassMethod是一个jmethodID。 - fadden
13
回答很好。只是想说,对于我来说这并没有起作用,因为出现了这个问题:https://dev59.com/2mUq5IYBdhLWcg3wHs6Y。在对gClassLoader对象调用NewGlobalRef()之后,问题得到了解决。谢谢! - LarryPel
7
谢谢给出的解决方案。注意在将其全局存储之前必须调用 NewGlobalRef()。从不同的线程中调用 NewGlobalRef() 是行不通的。请按照以下步骤进行操作: gClassLoader = env->NewGlobalRef(env->CallObjectMethod(myClass, getClassLoaderMethod));要了解更多详情,请访问以下链接: http://android-developers.blogspot.kr/2011/11/jni-local-reference-changes-in-ics.html - Andy Matteson
显示剩余10条评论

6

首先尝试将本地线程附加到JVM上。

jvm的指针可以在JNI_OnLoad函数中获取。

env->GetJavaVM(&jvm);

接下来是针对您的本地线程

JNIEnv *env;
jvm->AttachCurrentThread((void **)&env, NULL);

然后使用此env来进行FindClass操作。

9
Alex,当然我会这样做,否则1)和2)都不会起作用 :) - Pavel P

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