在Android平台初学JNI踩过的几个小坑

学习Java语言的时候没有怎么重视JNI,后来在Android平台开发的时候发现JNI是很重要的技术,因此便尝试在Android平台学习JNI的使用。

下面记录了学习过程踩过的几个小坑:

关于如何在AndroidStudio建立包含JNI的工程这里不描述了,这里只讲一下我在C/C++代码里面调用Android API时不能成功的几点原因。

Activity代码如下:

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native_lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void clickShowToast(View view){
        showToast("太神奇了"); // 调用native方法弹出Toast
    }

    public static Toast getName(Context activity, String str, int time){
        return null;
    }

    public native void showToast(String str); // 需要在JNI层实现的方法
}
布局文件activity_main.xml如下:

xml version="1.0" encoding="utf-8"?>
xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.yolo.jnidemo.MainActivity">

    
很简单里面就一个Button,效果是这样的:
在Android平台初学JNI踩过的几个小坑_第1张图片

下来重点是如何在JNI层实现:

public native void showToast(String str);

在jni目录下建立native-lib.c文件,内容如下:

#include 
#include "MLog.h"

#define TAG "NativeActivity"


JNIEXPORT void JNICALL
Java_com_yolo_jnidemo_MainActivity_showToast(JNIEnv *env, jobject instance, jstring str) {

    jclass Toast = (*env)->FindClass(env,"android.widget.Toast"); // (1)

    jmethodID makeText = (*env)->GetStaticMethodID(env,Toast, "makeText", "(Lcom/yolo/jnidemo/MainActivity,Ljava/lang/CharSequence,I)Landroid/widget/Toast");//(2)

    jobject Toast2 = (*env)->CallStaticObjectMethod(env,Toast, makeText, instance, str);

    jmethodID show = (*env)->GetMethodID(env,Toast, "show", "()V");

    (*env)->CallVoidMethod(env,Toast2, show);

}


我先解释一下这个逻辑关系,

1. 获取android.widget.Toast类;2.获取Toast的static方法makeText;3.调用makeText方法实例化一个Toast对象;4.调用Toast对象的show方法将Toast显示出来。


注释(1) 处错误原因,FindClass函数传入类的全名时要用反斜杠隔开,正确的是

jclass Toast = (*env)->FindClass(env,"android/widget/Toast");

否则会爆这样的错误,对初学者来说是茫然的:

backtrace:
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #00 pc ffffe424  [vdso:ab698000] (__kernel_vsyscall+16)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #01 pc 0007a03c  /system/lib/libc.so (tgkill+28)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #02 pc 00075885  /system/lib/libc.so (pthread_kill+85)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #03 pc 0002785a  /system/lib/libc.so (raise+42)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #04 pc 0001ee36  /system/lib/libc.so (abort+86)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #05 pc 005183c5  /system/lib/libart.so (_ZN3art7Runtime5AbortEPKc+565)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #06 pc 0011a5b3  /system/lib/libart.so (_ZN3art10LogMessageD1Ev+1747)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #07 pc 00392b75  /system/lib/libart.so (_ZN3art9JavaVMExt8JniAbortEPKcS2_+3445)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #08 pc 00392d18  /system/lib/libart.so (_ZN3art9JavaVMExt9JniAbortVEPKcS2_Pc+120)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #09 pc 0013e152  /system/lib/libart.so (_ZN3art11ScopedCheck6AbortFEPKcz+82)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #10 pc 00128f91  /system/lib/libart.so (_ZN3art8CheckJNI9FindClassEP7_JNIEnvPKc+1105)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #11 pc 00000488  /data/app/com.yolo.jnidemo-2/lib/x86/libnative_lib.so (Java_com_yolo_jnidemo_MainActivity_showToast+88)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #12 pc 0010f2d7  /system/lib/libart.so (art_quick_generic_jni_trampoline+71)
07-29 01:06:43.440 19904-19904/? A/DEBUG:     #13 pc 00109262  /system/lib/libart.so (art_quick_invoke_stub+338)

注释(2)的地方主要是方法的签名,我在这里踩了几个坑,主要是因为我想手写一个方法的签名,并没有用javap去获取方法签名,如果你使用javap去获取那么应该不会有太大问题,我觉得这个东西还是需要真正熟练掌握,直接撸出来就可以了,因此就掉坑里了。

(Lcom/yolo/jnidemo/MainActivity,Ljava/lang/String,I)Landroid/widget/Toast
这个就是Toast.makeText的方法的签名,是我手写出来的

问题a: 参数类型错误,我单纯的以我传入的数据类型给这个方法写签名,因此就将本应是Landroid/content/Context类型写成Lcom/yolo/jnidemo/MainActivity,竟然毫无察觉,另一个就是第二个参数,本应是Ljava/lang/CharSequence写成Ljava/lang/String,这一点我在直观排查时就丝毫没有发觉,直到我去查看了Toast.makeText方法的原型。

问题b:参数之间的分割问题,通常都会想成是逗号(,),然而并不是这样,而是如果你传入了类类型的参数,那么要写成Ljava/lang/CharSequence;这个样子,末尾要加上分号(;),如果是基本类型就任何符号不用加,基本类型怎么表示这里就不讲了。

问题c:方法签名这样写还没完,同样要注意返回值类型也是遵循问题b描述的原则,即类类型返回值要在末尾加上分号(;),基本类型不用加符号。

这里讲上面的

jobject Toast2 = (*env)->CallStaticObjectMethod(env,Toast, makeText, instance, str);

改为如下:

jobject Toast2 = (*env)->CallStaticObjectMethod(env,Toast, makeText, instance, “Hello JNI”);

即将要Toast的文字直接以char*传入,而不是从java层传入,这样运行的话会报如下错误:

 JNI DETECTED ERROR IN APPLICATION: use of deleted weak global reference 0x7dd55cc6066b

A/zygote64: runtime.cc:500]     from void com.example.yongledu.myapplication.LoginActivity.showToast(java.lang.String)

由此可以看出这里如果传入数据的话,数据的类型一定要使用JNI里面定义的类型,而不是C或java里面的类型,否则可能会出现传入参数无效的问题。

关于签名踩得坑就是这些,下面还有一个疑惑还没有解开,也记录一下:

如果JNI方法是在.c文件里面实现的话直接这样实现该函数就可以了

JNIEXPORT void JNICALL
Java_com_yolo_jnidemo_MainActivity_showToast(JNIEnv *env, jobject instance, jstring str){}

但是调用JNIEnv的函数时需要(*env)获取到JNIEnv类型而非JNIEnv*类型。

而如果是在.cpp文件里面实现的话就需要写成这个样子:

extern "C"{
JNIEXPORT void JNICALL
Java_com_yolo_jnidemo_MainActivity_showToast(JNIEnv *env, jobject instance, jstring str){}
}
而在这个里面,我们调用JNIEnv的函数时使用的是JNIEnv*类型。本人C/C++基础不是很好,这一点原因还没高清楚,先记录一下格式。


最后我附上针对这个类的javap命令:首先编译工程,在out目录下找到需要javap的class文件的包的根目录,也就是com文件夹所在目录,然后执行javap -v com.yolo.jnidemo.MainActivity 就会得到该类方法的签名,找出你要的方法签名,拷贝一下就OK完事了。

那为什么我不用javap呢?我认为对于初学一个知识来讲,复制粘贴可不是好习惯,一字一字去码才是硬道理。






你可能感兴趣的:(移动开发)