对Android 开发艺术探索的阅读笔记... 待我 AS 里配置好 NDK 后再来完善,大流程没毛病....未完待续
一、作用
JNI:使得 Java 可以与本地其他类型语言(如C、C++,编译成的so库)交互 。即在 Java 代码中调用 C、C++ 等语言的代码,或 C、C++ 代码调用 Java 代码。
NDK:可以帮助我们在 Android Studio 中快速开发 C、C++ 的动态库(写好C、C++ 源文件,然后将他们编译成so库),并自动将 so 和应用一起打包成 apk
可通过 NDK 在 Android 中使用 JNI 与本地代码(如C、C++)交互
二、概念
1.JNI(Java Native Interface,Java 本地接口)
JNI 是属于 Java 的,与 Android 无直接关系
JNI 是为了方便 Java 调用 C、C++ 等本地代码所封装的一层接口。Java 的优点是跨平台,但作为优点的同时,其在和本地交互时就出现了短板。Java 的跨平台特性导致其本地交互的能力不够强大,一些和操作系统相关的特性 Java 无法完成,于是 Java 提供了 JNI 专门用于和本地代码交互,这样就增强了 Java 语言的本地交互能力。通过 JNI,用户可以调用C、C++所编写的本地代码。
2.NDK(Native Development Kit,是 Android 的一个开发工具包)
NDK 是属于 Android 的,和 Java 无直接关系
NDK 是 Android 提供的一个工具集合,通过 NDK 可以在 Android 中更加方便地通过 JNI 来访问本地代码,如C、C++。NDK 还提供了交叉编译器,开发人员只需要简单地修改 mk 文件就可以生成特定 CPU 平台的动态库。
使用 NDK 的好处:
提高代码安全性
so 库反编译比较困难,从而提高了 Android 程序的安全性
可方便地使用目前已有的 C、C++开源库
提高程序在某些特定情形下的执行效率,但并不能明显提升 Android 程序的性能
三、使用
1. JNI 开发流程
自己制作so库给自己用。
(1)在 Java 中声明 native 方法
package com.example.myapplication.jnitest;
import java.lang.System;
// Java 调用 JNI 方法
public class JniTest {
static{
System.loadLibrary("jni-test");
}
public static void main(String[] args) {
JniTest jniTest = new JniTest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}
// 这两个 native 方法就是需要在 JNI中实现的方法
public native String get();
public native void set(String str);
}
代码中声明了两个native方法,这两就是需要在 JNI 中实现的方法。
在JniTest的头部加载动态库,其中jni-test是so库的标志,完整的名称是libjni-test.so,这是加载so库的规范。
(2)编译 Java 源文件得到.class文件,然后通过javah命令导出 JNI 的头文件
.java文件的路径是MyApplication/app/src/main/java/com/example/myapplication/jnitest
进入上述路径运行
// 编译得到 class 文件
javac ./JniTest.java
在MyApplication/app/src/main/java/com/example/myapplication/jnitest下得到.class文件
然后,进入到MyApplication/app/src/main/java,运行下面指令,在该路径下生成头文件com_example_myapplication_jnitest_JniTest.h。用javah 导出类的头文件, 常见的错误及正确的使用方法
// 导出 JNI 的头文件
javah com.example.myapplication.jnitest.JniTest
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_example_myapplication_jnitest_JniTest */
#ifndef _Included_com_example_myapplication_jnitest_JniTest
#define _Included_com_example_myapplication_jnitest_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_myapplication_jnitest_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_myapplication_jnitest_JniTest_get
(JNIEnv *, jobject);
/*
* Class: com_example_myapplication_jnitest_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_myapplication_jnitest_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
函数名的命名规则:Java_包名_类名_方法名
Java 和 JNI 的数据类型之间的对应关系:ref 参考文献
(3)自己实现 JNI 方法
JNI 是指 Java 中声明 native 方法,可以选择 C++ 来实现。
在工程的主目录main下创建一个子目录,命名为jni,将之前通过javah生成的头文件拷贝到该目录下,接着创建test.cpp文件,实现如下。
我的存储路径
下面直接拷贝书中的实现....
#include "com_example_myapplication_jnitest_JniTest.h"
#include
JNIEXPORT jstring JNICALL Java_com_example_myapplication_jnitest_JniTest_get(JNIEnv *env, jobject thiz) {
printf("invoke get in c++\n");
return env->NewStringUTF("Hello from JNI !");
}
JNIEXPORT void JNICALL Java_com_example_myapplication_jnitest_JniTest_set(JNIEnv *env, jobject thiz, jstring string) {
printf("invoke set from C++\n");
char* str = (char*)env->GetStringUTFChars(string,NULL);
printf("%s\n", str);
env->ReleaseStringUTFChars(string, str);
}
(4)编译so库并在 Java 中调用
so库的编译这里采用gcc,切换到jni目录下MyProjects/AndroidStudioProjects/MyApplication/app/src/main/java/com/example/myapplication/jni,编译指令如下。
gcc -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/include -fPIC test.cpp -o libjni-test.so
有可能会提示找不到jni_md.h,去本机的 jdk 目录下找到jni_md.h把它拷贝到XXXX/include下。
生成so库
在 Java 中调用
main$ java -Djava.library.path=jni java.com.example.myapplication.jnitest.JniTest
输出日志
???????
2. NDK 开发流程
自己制作so库给自己用。
NDK 的开发是基于 JNI 的。简化了上述 JNI 开发流程。
从官网下载 NDK ,android-ndk-r21-darwin-x86_64,下载完成后将解压路径设置为:Android Studio 的sdk目录里,并命名为ndk-bundle。
这样做的好处:启动 Android Studio 时,AS 会自动检查它并直接添加到ndk.dir中,那么在使用时,就不用配置 AS 与 NDK 的关联工作
解压路径
AS 自动检测 NDK
下载完成后为 NDK 配置环境变量,修改.bash_profile
在终端运行命令ndk-build
说明配置成功
(2)创建一个 Android 项目,并声明所需的 native 方法
package com.example.myapplication.jnitest;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.R;
public class JniActivity extends AppCompatActivity {
static {
System.loadLibrary("jni-test");
}
private TextView mShowTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jni);
mShowTv = findViewById(R.id.activity_jni_show_tv);
mShowTv.setText(get());
}
public native String get();
public native void set(String string);
}
(3)实现 Android 项目中所需的 native 方法
在main下创建jni文件夹,jni下创建3个文件:test2.cpp、Android.mk、Application.mk。
// test.cpp
同上文,只是方法名称有变化
//
// Created by yanglijin on 2020-04-11.
//
#include "com_example_myapplication_jnitest_JniTest.h"
#include
JNIEXPORT jstring JNICALL Java_com_example_myapplication_jnitest_JniActivity_get(JNIEnv *env, jobject thiz) {
printf("invoke get in c++\n");
return env->NewStringUTF("Hello from JNI !");
}
JNIEXPORT void JNICALL Java_com_example_myapplication_jnitest_JniActivity_set(JNIEnv *env, jobject thiz, jstring string) {
printf("invoke set from C++\n");
char* str = (char*)env->GetStringUTFChars(string,NULL);
printf("%s\n", str);
env->ReleaseStringUTFChars(string, str);
}
// Android.mk
???
// Application.mk
???
APP_ABI:表示 CPU 的架构平台的类型,目前市面上常见的架构平台有 armeabi、x86、mips,其中在移动设备中占据主要地位的是 armeabi,这也是大部分 apk 中只包含 armeabi 类型的 so 库的原因。默认情况下,NDK 会编译产生各个 CPU 平台的 so 库,通过 APP_ABI 选项即可指定 so 库的 CPU 平台的类型,这样 NDK 就只会编译特定平台下的 so 库。
(4)通过 ndk-build 命令编译产生 so 库
切换到jni目录的父目录,然后通过ndk-build命令编译产生so库
这个时候 NDK 会创建一个和jni目录平级的目录libs,libs下面存放的就是so库。ndk-build 命令会默认置顶jni目录为本地源码的目录名,如果源码存放目录名不是jni,则 ndk-build 无法编译成功。
(5)在 AS 中使用 so 库
在app/src/main中创建目录jniLibs,将生成的so库复制到jniLibs中,然后 AS 即可编译运行成功。
3. JNI 调用 Java 方法的流程
(1)先通过类名找到类;(2)通过方法名找到方法的 id;(3)调用这个方法。
如果是调用 Java 中的非静态方法,需要构造出类的对象后再调用它。
(1)Java 中定义一个静态方法供 JNI 调用
/**
* JNI调用Java方法
*/
public class InvokeByJni {
private static final String TAG = "InvokeByJni";
public static void methodCalledByJni(String msgFromJni) {
Log.d(TAG, "methodCalledByJni()::msg=" + msgFromJni);
}
}
(2)JNI 中调用上面定义的静态方法
void callJavaMethod(JNIEnv * env, jobecjt thiz) {
jclass clazz = env - > FindClass("com/example/myapplication/jnitest/JniActivity");
if (clazz == null) {
printf("find class JniActivity error");
return;
}
jmethodId id = env - > GetStaticMethodID(clazz, "methodCalledByJni", "(Ljava/lang/String;)V");
if (id == NULL) {
printf("find method methodCalledByJni error");
return;
}
jstring msg = env - > NewStringUTF("msg send by callJavaMethod in testActivity.cpp");
env - > CallStaticVoidMethod(clazz, id, msg);
}
程序先根据类名找到类,然后再根据方法名找到方法,接着再通过JNIEnv对象的CallStaticVoidMethod方法来完成最终的调用过程。
4.作为伸手党 如何在 AS 中使用so库
四、参考文献