JNI—第一个程序HelloWorld
NDK开发流程
- 在Java里面写native代码
- 在main目录下创建jni目录,写C代码—生成头文件
- 配置动态链接库的名称
- 加载动态链接库
//把.so加载进来
System.loadLibrary("hello");
- 使用
交叉编译
-
在一个平台上编译出在另外一个平台上可以运行的本地代码
-
平台CPU平台:x86、arm、mips
-
操作系统平台:Windows、Linux、Mac os、Unix
-
NDK:模拟另外一个平台的特性进行编译
NDK压缩包下的文件
-
sources:NDK相关的源代码
-
platforms:根据不同的api-level 有多个文件夹 每个文件夹中都有不同CPU架构的文件夹
-
include:跟Android下做jni开发相关的头文件
-
lib:Google编译好的jni开发可能用到的函数库
-
toolchains:交叉编译工具链
-
build/tools .sh linux的批处理命令 通过这些批处理命令调用交叉编译工具链中的编译工具
-
ndk-build.cmd:通过这个命令可以开启交叉编译的过程 如果想通过命令行来编译可以在Android上运行的本地代码 要吧ndk-build放到环境变量中
看到这个错说明配置成功了
JNI的理解
现在是Java是程序入口,所以C代码不用写main方法,只用写函数对native方法的具体实现
cd /d:直接进入到后面的目录下
cd /d E:\AndroidDemo\JNIDemo
JNI_Helloworld的步骤
-
首先新建一个NDKDemo的项目,我们在包名目录下建立一个调用C方法的类JNI
-
给AS配置关联NDK
1). local.properties中添加配置
ndk.dir=G\:\\android-ndk-r10(=号后面为ndk的解压路径)
2). gradle.properties中添加配置
兼容老的ndk(老的版本):android.useDeprecatedNdk=true
android.useDeprecatedNdk=true
- 在 JNI 中声明一个native方法,native方法不用实现
//通过native关键字 声明了一个本地方法 本地方法不用实现,需要用jni调用C的代码来实现
public native String helloInc();
native关键字表明该Java方法由非Java语言实现
最后类的样式如下:
我们为准备生成的so文件起名为 Hello ,JNI 会预加载该库,平时使用so库的小伙伴一定不会陌生了,这是一个标准的使用方法,说明我们的native方法来自于Hello.so库,其实JNI也是最后生成so库供以使用。
-
生成提供该方法的C类(可以先看这个:https://blog.csdn.net/weixin_42814000/article/details/105279704)
那么既然这样,我们就要用c语言实现一个同名方法用以调用,首先我们根据这个类生成一个c的头文件(这是c语言的内容如果不了解的话先照着做),在android studio下面terminal窗口执行如下命令:
这里我遇到了第一个坑,先把正确实现的步骤写出来
生成头文件的方式
点击 Terminal 输入命令,便可以生成一个JNI的 C 头文件.
输入第二个命令行提示错误的原因是注释为中文。所以在输入命令行时不能出现注释或者中文
javac -h ./ JNI.java
上面的命令的作用:根据Java中的 native 方法生成对应在 C 中的方法该怎么写(自动生成)
这样为正确的,输入命令行没问题
这样会提示错误
下图为生成的头文件
JNI的头文件的代码如下
坑1(绕路):有些资料给出的步骤是去/build/intermediates/classes目录下对.class文件执行javah,其实需要在生成头文件的文件夹下(cd app/src/main/java/com/lwm/ndkdemo)执行就可以。
坑2(找不到类文件):有些资料给出的命令是 javah -d jni 包名.类型,在不同的本地环境下可能出现找不到类文件的提示,网上有人给出的解决方法是在classpath中做配置,其实直接使用javah -d jni -classpath 命令就可以了。
坑3(JNI目录问题):注意JNI目录的位置是在/src/main之下,很多同学的JNI目录生成不对(比如使用了坑1方法生成再挪过来),位置错误导致最后运行时一直崩溃。
- 生成完头文件之后我们需要在写一个.c 文件并引用该头文件(还是c的内容,不了解的同学直接copy),在项目根目录下创建 jni 文件夹 在 jni 目录下创建.c 的代码(New–>c/c++SourceFile---->后缀选择.c),取名 Hello.c:
C函数命名规则: Java_包名_native方法所在类名_native方法名(JNIEnv* env,jobject jobj)
//
// Created by 林伟茂 on 2020/4/2.
//
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
// 因为 sayHello() 方法返回的为String类型所以对应JNI中的jstring类型
// 然后把.改为_,然后前面加一个 Java_
/**
jstring:返回值
本地函数命名规则:Java_包名—_native函数所在类的类名_native方法名
JNIEnv* env:相当于环境变量,里面有很多方法
jobject jobj:谁调用了这个方法就是谁的实例
当前就是 JNI.this
*/
// 第一个参数 JNIEnv* JNIEnv是结构体 JNINativeInterface 这个结构体的一级指针
// env又是JNIEnv的一级指针 那么 env就是 JNINativeInterface 的二级指针
// 结构体 JNINativeInterface 定义了大量的函数指针,这些函数指针在 JNI 开发中十分常用
// 第二个参数 jobject 就是调用当前 native 方法的 java 对象
jstring Java_com_lwm_ndkdemo_JNI_sayHello(JNIEnv* env,jobject jobj){
// jstring (*NewStringUTF)(JNIEnv*, const char*);
char* text = "I am from c";
//通过 NewStringUTF方法把C的字符串转换成java的jstring类型
return (*env)->NewStringUTF(env,text);
}
- 在jni目录下创建一个Android.mk、Application.mk
Android.mk文件
#makefile:作用就是向编译系统描述 我要编译的文件在什么位置 要生成的文件叫什么名字,是什么类型
#call my-dir:获取当前的目录,因为放在jni这个目录,所以就找当前的目录
LOCAL_PATH := $(call my-dir)
#把上次编译的时候的信息清空,但是call my-dir里的内容不会被清除掉的
include $(CLEAR_VARS)
#在这里指定最后生成的文件叫什么名字
LOCAL_MODULE := hello
#要编译的C的代码的文件名
LOCAL_SRC_FILES := hello.c
#要生成的是一个动态链接库( 通过BUILD_SHARED_LIBRARY指定存储的扩展名为 .so)
include $(BUILD_SHARED_LIBRARY)
Application.mk 文件
APP_ABI := all
APP_STL := stlport_static
在工程上app目录上右击选择link c++ project with Gradle,这个选项的意思是导入外部的c++工程,也就是把我们写好的c当做外部工程导入:
在下拉框中选择ndk-build,并选择对应的Android.mk文件的位置:
这里说明一下mk文件的作用,mk文件其实里面也就是一些 ndk 的文件路径配置、生成 so库名字、生成平台等的配置。
我们需要在 jni 目录里加入如上两个 mk 文件 Android.mk 和 Application.mk,如图是最基本的配置,解释下每个配置的注意的地方:
Android.mk:LOCAL_PATH 和两个 include 照写就行了,这三个配置 google 出来的意思比较生涩,解释也比较难懂,我们要关注的是 LOCAL_MUDLE 和 LOCAL_SRC_FILES,LOCAL_MUDLE 其实就是我们之前配置的 moduleName,指定了生成 so 库的名字,LOCAL_SRC_FILES则是我们引用的c文件位置。
Application.mk:APP_ABI 就是 abiFilters 了,所以之前我们做的配置都可以在这里写,赋值为 all 表明全平台生成,如果有多个用空格分开,APP_ABI:= armeabi-v7a armeabi。APP_STL指运行库类型,通常都是stlport_static,表示以静态链接方式使用的sttport版本的STL(写出这个配置翻译,是挺生涩的吧)。
项目build完之后,回想一下,我们选择的mk文件是 Android.mk,Applicaiton.mk 还没配置进去,打开app/build.gradle,你会发现多了一个配置:
所以Android.mk最后还是在gradle里面引入,其实我们就可以直接在build.gradle里加入这句引入代码,刚才用的方式是可视化界面的操作方式。接着我们在里面配置Application.mk:
externalNativeBuild {
ndkBuild{
// 指定 Application.mk 的路径
arguments "NDK_APPLICATION_MK=src/main/jni/Application.mk"
//cFlags 和 cppFlags 是用来设置环境变量的,一般不需要动
cFlags "-DTEST_C_FLAG1","-DTEST_C_FLAG2"
cppFlags "-DTEST_CPP_FLAG2","-DTEST_CPP_FLAG2"
}
}
最后再build一下,终于大功告成了!
7. 通过ndk-build在项目根目录下编译.c文件 生成.so文件
如果生成了如上两个文件,并且ndkBuild文件夹下有对应so文件则说明成功了。
但是其实还有最后一个坑
我发现Application.mk中APP_ABI的配置并不起作用,于是在build.gradle中做了最后一步修改:
ndkBuild下生成的so文件也变了,终于ok了!
JNI 的 sayHello 方法已经可以调用了,我们在c里面让它返回一个 I am from c 的字符串,赶快试试吧!
- 调用.so之前需要使用System.loadLibrary来加载.so文件
//把.so加载进来
System.loadLibrary("hello");
- 调用 C 的函数
public class MainActivity extends AppCompatActivity {
private Button cid;
private String result;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
result = new JNI().sayHello();
System.out.println("result" + result);
initView();
}
private void initView() {
cid = (Button) findViewById(R.id.cid);
cid.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "result=" + result, Toast.LENGTH_SHORT).show();
}
});
}
}
部分转载:https://www.jianshu.com/p/eae320ee9b2d