学习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"?>很简单里面就一个Button,效果是这样的: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">
下来重点是如何在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呢?我认为对于初学一个知识来讲,复制粘贴可不是好习惯,一字一字去码才是硬道理。