先上结论
1、java文件、h头文件、c文件、mk文件、so文件放在什么目录并没有什么关系,不会影响最后的运行。
2、所有的文件链接都是通过相对路径的方式去寻找的,比如c对h文件的寻址是通#include相对路径,mk文件对c文件的寻址是通过LOCAL_SRC_FILES这一句,ndk-build对mk文件的寻址是通过命令的参数,gradel.build对jniLibs夹(so文件)的寻址是通过sourceSets这一句。
3、由指令生成文件的方式,比如先进入相关依赖文件的目录下。比如使用javac生成.h文件,必须进入java文件所在的目录;通过ndk-build生成so文件,必须进入mk文件所在文件夹。
4、找不到so文件,一方面可能是gradel.build中sourceSets格式写的不对,也有可能是路径指向有问题,要用Android视图检查。
5、程序没有报错,但是运行闪退,可能是java文件中loadLibrary加载so文件的名称写错,可以检查下,要跟Android.mk中的LOCAL_MODULE一致。如果一致还是不行,建议删掉so文件再生成一次。
目录
2.9、不使用jniLibs,修改build.gradle(app)
一、普通流程
在理解之前,先按照普通流程走一遍,确保可以运行,后续再一点点修改,比如把c文件或者so文件放到其他目录下,不使用jni或者jniLibs文件夹名,看看会发生什么,用来加深理解。
1.1、准备工作
1、创建empty工程
2、进入Project Structure
3、选择NDK所在路径
4、如果这一步没有NDK,就去设置里面下载
勾选后点击OK即可自动下载。
说明:这里没有LLDB,但是目前不影响,可以继续执行,后续使用CMake会用到。
1.2、java文件
5、创建一个java文件,在里面写一个公共静态本地字符串方法
文件名和方法名写那么短是为了后面生成h文件时方法名看起来不会太长。
6、底部点击Terminal,并输入cd空格
7、右键Java文件所在的包名,选择Copy Path
8、选择绝对路径
9、然后回到之前的Terminal,粘贴路径,回车,进入java文件所在路径
1.3、h文件
10、输入javac -h . J.java回车,生成h文件
该指令没有回显,但是你可以过几秒在java文件所在的目录看到新生成的class文件和h文件
补充:有的教程说编译工程生成class,然后javah相关的class文件生成h文件。但是可能会发现找不到classes目录,因为目前最新版本的AndroidStudio中classes文件的路径是在\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes。而原来的版本classes路径是在\app\build\intermediates\classes,而且即使找到class文件,javah在java10已经不起作用(但是可以使用javah -jni来生成,见后面的作死流程),需要使用java文件目录下用javac -h . xxx.java生成头文件.h。
11、生成了两个文件,class可以不管它,后面暂时用不到,观察h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class a_ndk_build_J */
#ifndef _Included_a_ndk_build_J
#define _Included_a_ndk_build_J
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: a_ndk_build_J
* Method: a
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_a_ndk_1build_J_a
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
12、对我们来说最有用的就是JNIEXPORT jstring JNICALL Java_a_ndk_1build_J_a
(JNIEnv *, jclass);这一行,因为后面可以直接复制到c文件去,稍做修改即可使用。
13、右键app或者src或者main任意一个,新建一个文件夹,名称为jni
这里有的教程说需要在main下创建,实际上我试了,在这几个目录下都可以,因为ndk-build命令会从Android.mk所在目录一直往上找以jni命名的目录,只是影响so文件生成的位置,而且反正最后so文件也要剪切到jniLibs文件夹。
一定要名称是jni吗?也可以不一定,虽然Android.mk文件会去找以jni命名的文件夹,但是只要配置一下就可以用其他文件名,具体见后面的作死过程。
14、剪切h文件到jni目录下,或者直接拖过去,点击OK确认移动即可
1.4、c文件
15、右键刚刚创建的jni文件夹,新建一个c文件或者c++文件
16、简单起见,就叫c好了
17、把刚刚h文件的方法声明JNIEXPORT jstring JNICALL Java_a_ndk_1build_J_a
(JNIEnv *, jclass);复制过来(PS:不要复制我这里的,去复制你h文件的方法声明),做一下修改,注意include也要跟你的h文件名一致
18、前面的步骤其实就是创建了一个java文件,然后javac生成h文件,又创建了c文件,可以说是做一些基础准备,接下来的操作才是ndk-build的关键步骤,也就是Android.mk和Application.mk的创建,这两个文件是生成so文件能否成功的关键。
1.5、mk文件
19、右键jni目录,新建Android.mk和Application.mk
20、在Android.mk中写入如下内容
# my-dir表示当前文件所在目录,
LOCAL_PATH:=$(call my-dir)
# 指向一个特殊的Makefile,负责清理 LOCAL_XXX 变量(LOCAL_PATH除外)
include $(CLEAR_VARS)
# 要生成的so库名称,随便写,比如写个aa,后面会被java文件调用
LOCAL_MODULE:=aa
# LOCAL_PATH这个变量被用来寻找C/C++源文件,
LOCAL_SRC_FILES:=c.c
# 负责将你在 LOCAL_XXX 等变量中定义信息收集起来,确定要编译的文件,如何编译
include $(BUILD_SHARED_LIBRARY)
具体内容请看注释。
21、在Application.mk写入如下内容
# 不写 APP_ABI或者写all 会生成全部支持的平台,也可以指定其中一两种,
# 比如APP_ABI := arm64-v8a armeabi-v7a x86 x86_64
APP_ABI := all
APP_PLATFORM := android-16
这里我之前复制其他的写android-15,后续发现报错信息为
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.
改为16以后就正常了。
1.5、so文件(ndk-build命令)
22、这个时候就可以执行ndk-build命令了,首先进入jni所在的文件夹
和刚刚一样,cd空格,复制jni的绝对目录,粘贴,进入即可
23、输入ndk-build回车,即可执行生成指令,回显信息如下
什么?你提示command not found(找不到该命令)?那么就需要设置环境变量了
24、还记得之前下载的ndk路径吗?将它设置为环境变量即可
win:右键‘计算机’-‘属性’-左侧‘高级系统设置’-标签‘高级’-底部‘环境变量-下侧’系统变量‘-选择’Path‘-选择’编辑‘-’新建‘,写入NDK路径即可(NDK文件夹下要有ndk-build)
Mac:在终端的home路径下输入vi .bash_profile,输入i编辑,在最后添加一句
export PATH=${PATH}:/Users/Su123/Library/Android/sdk/ndk/21.3.6528147(你的NDK路径)
,:wq保存退出,输入source .bash_profile生效。
25、再执行ndk-build,过个几秒钟,你会看到jni的同级目录下,生成了libaa.so文件,如果你还记得之前Android.mk中LOCAL_MODULE变量的赋值,就会明白随便命名带来的后果。
26、app/src/main目录下面创建一个jniLibs文件夹,将几个so文件夹剪切到这里
说明:一定要放在这个目录下吗?不一定,但是如果你放在其他目录下,待会运行的时候可能会提示couldn't find "xxx.so",说没有找到这个文件,因为这个文件AS默认在这个目录下,如果你想放其他地方,需要在build.gradle(app)进行声明,待会在作死部分的时候可以试试。
27、然后回到J.java文件,使用System.loadLibrary加载这个so库
package a.ndk_build;
public class J {
static public native String a();
static {
System.loadLibrary("aa");
}
}
28、到MainActivity.java去调用a()方法
package a.ndk_build;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((TextView)findViewById(R.id.id_tv)).setText(J.a());
}
}
记得去布局文件添加TextView的id
29、点击顶部绿色三角,运行
二、作死流程
2.1、准备工作
1、新建一个empty项目
2.2、java文件放默认目录
2、直接右键java目录(java以外的目录不让创建java文件),新建一个java文件,写入公共静态本地字符串方法
PS:把java文件放在src/main/java这个默认目录下(没有包名)对生成so文件并没有什么影响,但是后续MainActivity.java想import该文件时会有一些问题,还有运行时会遇到一些问题,不过这也是一个关键的知识点,具体可以看后面的解释。
2.3、生成h头文件的命令解析
3、复制java文件夹所在绝对路径,进入以后执行javac命令
这里可以试试其他命令,把h文件和class删掉,然后再执行,比如
javac A.java
这个命令只生成了class文件,没有h文件
或者
javac -h A.java
去掉小数点以后,提示没有源文件
参考https://blog.csdn.net/hydra_jc/article/details/91402429
出现上述的原因是没有指定要生成的目录。
.表示当前目录,也可以使用../..,表示上级目录的上级目录,执行以后会生成到src目录下
参考https://www.jianshu.com/p/1ca555f05a53,还有一种用法
javah -jni A
直接生成h文件,没有class文件了
虽然这种方式看起来很好用,但是仅限于A.java在java目录下,如果在某个包内,就会提示错误: 找不到 'A' 的类文件。
4、双击打开A.h文件,内容如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class A */
#ifndef _Included_A
#define _Included_A
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: A
* Method: b
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_A_b
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
2.4、c文件不放jni目录
6、在a.a下创建一个c.cpp(之前是在jni下创建)
7、写入如下内容
#include "src/main/java/A.h"
JNIEXPORT jstring JNICALL Java_A_b
(JNIEnv *d, jclass){
return d->NewStringUTF("c.cpp文件传上来的");
}
PS:这么include是有问题的,要用相对路径(./../../../A.h),不过先这样写,等下ndk-build报错时说明
2.5、mk文件不放jni目录
8、mk文件我决定写在src目录下(之前是放在jni目录下,不过放哪都可以,改一下ndk-build命令的参数即可)
Android.mk
# my-dir表示当前文件所在目录,
LOCAL_PATH:=$(call my-dir/main/java/a/a/)
# 指向一个特殊的Makefile,负责清理 LOCAL_XXX 变量(LOCAL_PATH除外)
include $(CLEAR_VARS)
# 要生成的so库名称,随便写,后面会被java文件调用
LOCAL_MODULE:=ee
# LOCAL_PATH这个变量被用来寻找C/C++源文件,
LOCAL_SRC_FILES:=c.cpp
# 负责将你在 LOCAL_XXX 等变量中定义信息收集起来,确定要编译的文件,如何编译
include $(BUILD_SHARED_LIBRARY)
PS:这里的LOCAL_PATH用路径会有问题,先故意这么写,等下看报错信息。正确的写法是通过LOCAL_SRC_FILES去定位c文件。
Application.mk
APP_ABI := arm64-v8a armeabi-v7a
APP_PLATFORM := android-16
这里暂时选定生成arm64-v8a、armeabi-v7a两个平台的so文件,也可以选x86 x86_64,先暂时这么写。
9、复制src目录路径,cd空格进入src目录。
执行ndk-build试试。
报错信息如下
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/Users/Su123/Library/Android/sdk/ndk/21.3.6528147/build/core/build-local.mk:151: *** Android NDK: Aborting . Stop.
2.6、ndk-build的参数
10、参考https://www.cnblogs.com/luolizhi/p/5651558.html和https://www.jianshu.com/p/542f71e6e271
解释如下:
这是因为,当前Android.mk 未放置在jni目录内。所以ndk-build无法找到Android.mk. (ndk-build会从此目录向上一直找到jni目录,并从jni目录中找到Android.mk),如果不想创建jni文件,可以在ndk-build指令后手动指定文件所在位置,指定方式如下:
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=./Application.mk APP_BUILD_SCRIPT=./Android.mk
说明:NDK_PROJECT_PATH=.表示ndk工程在此目录,如果你想皮一点,可以跳到其他地方,然后用相对路径定位过来,然后NDK_APPLICATION_MK=./Application.mk表示告诉ndk-build命令,Application.mk文件在当前目录下,APP_BUILD_SCRIPT=./Android.mk也同理。
出现如下提示
说明mk已经被认可,但是c.cpp没有被认出来。
11、参考https://www.cnblogs.com/lance-ehf/p/4184596.html
试一下改动Android.mk如下两行
LOCAL_PATH:=$(call my-dir)
LOCAL_SRC_FILES:=main/java/a/a/c.cpp
这次结果如下
说明c文件也被找到了,但是A.h文件没找到。
之前是因为LOCAL_PATH通过调用my-dir函数来获取当前的路径,如果参数加了其他的内容很可能导致错误,但是在后面寻找cpp文件时可以按照路径的方式去找
12、上面的提示说明我的include指向有问题,说白了就是没找到h文件。其实一个最简单的方法就是直接把h文件复制到c文件同目录下,但是我会那么容易妥协吗?坚决不挪动文件,看一下怎么指向这个h文件的路径
改成#include "../../../../A.h"试试
说明一下,../表示上级目录,当前的cpp文件在/app/src/main/java/a/a/下,而h文件在src下,所以cpp文件要去找h文件,必须向上四层,到达src文件夹,才能找到h文件
再次执行:
生成so文件了!
位置:mk所在文件的同目录下的libs文件夹里
13、去java加载so库
public class A {
static public native String b();
static {
System.loadLibrary("ee");
}
}
然后我们来调用c中的方法,并放到activity_main.xml中的TextView中
2.7、无法import A
14、这里我遇到一个问题,就是之前把A.java写在了src/main/java目录下,现在直接import提示该类是在默认包下,不让导入。
其实只要把它移动到MainActivity.java同目录下就可以了,或者新建一个包把A.java文件放进去也行,添加一下包名即可。
但是我不是那么容易妥协的人,一番搜索
参考https://bbs.csdn.net/topics/330219738中最后的回答,想试着写下,但是发现snmpHandler找不到,然后参考
https://stackoverflow.com/questions/2193226/how-to-import-a-class-from-default-package中的某个回答,也是类似的写法,只不过snmpHandler换成了fooClass,结果还是找不到。
15、看来只能妥协,,况且把java文件放在默认包里本来也不是提倡的做法。
在java目录下新建一个包f
16、把A.java放进去,记得在底下选择确认
17、java文件的变化
但是这里一旦改动,c和h文件也要跟着改动,因为c文件的方法名是类和包的组合,也就是JNIEXPORT jstring JNICALL Java_A_b中的Java_A_b,这个文件名的组成结构是Java_包名_类名_方法名(比如普通流程中的Java_a_ndk_1build_J_a,其中a_ndk_1build就是包名)。
可以直接修改c文件和h文件的方法名,加入包名,如下:
JNIEXPORT jstring JNICALL Java_f_A_b
然后因为so文件是依托于c文件生成的,所以需要通过以下指令:
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=./Application.mk APP_BUILD_SCRIPT=./Android.mk
生成新的so文件。
18、MainActivity.java文件修改如下
本来我想把A文件的内容直接放到MainActivity下的(即class MainActivity{class A{...}}),因为反正A也是一个类,而本地方法也已经被so文件定义好了,但是因为A中有静态方法,放过去会报错,所以还是放弃了。
接下来准备运行,但是为了防止之前的运行文件对后面的编译产生影响,建议都点一次清理工程,再运行。
2.8、Application.mk中的平台指定问题
19、点击顶部绿色三角,直接运行。
运行果然报错,报错结果如下
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/a.a-IeARgzaVo7jPZCdUXuBFmA==/base.apk"],nativeLibraryDirectories=[/data/app/a.a-IeARgzaVo7jPZCdUXuBFmA==/lib/x86, /system/lib, /system/product/lib]]] couldn't find "libee.so"
提示没有so文件,说明之前生成的so文件编译器并不知道在哪,我们之前是生成在src/libs下,先剪切到src/main/jniLibs目录下运行试试,确保前面的步骤都没有错,后面再移动到其他目录。
20、运行,依然提示couldn't find "libee.so"
再仔细观察一下完整的日志
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/a.a-nsNst_ulKhOU5urnxHZJlA==/base.apk"],nativeLibraryDirectories=[/data/app/a.a-nsNst_ulKhOU5urnxHZJlA==/lib/x86, /system/lib, /system/product/lib]]] couldn't find "libee.so"
其实很明显了,说白了就是生成so文件的时候没有x86平台,但是模拟器可能是x86架构的,所以找不到x86的so文件,就报错了,发现这一点,我们修改Application.mk文件
APP_ABI := arm64-v8a armeabi-v7a x86 x86_64
APP_PLATFORM := android-16
然后把之前的so文件所在libs文件夹直接删掉,然后进入src目录(Android.mk所在目录)执行以下指令
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=./Application.mk APP_BUILD_SCRIPT=./Android.mk
重新生成so文件,并放入jniLibs
21、清理工程,运行
PS:遇到过一个现象,就是运行成功,也没有任何错误提示,但是程序一闪而过就关闭,这种情况删掉so文件重新生成剪切运行即可。
2.9、不使用jniLibs,修改build.gradle(app)
22、把jniLibs目录下几个文件夹剪切回src/libs下,参考https://blog.csdn.net/lbcab/article/details/72771729/的介绍,打开build.gradle(app),写入如下内容
android {
...
sourceSets{
main{
jniLibs{
srcDirs "src/libs"
}
}
}
...
}
右上角刷新
提示BUILD SUCCESSFUL
清理工程,运行
提示找不到so文件,
说明我们的sourceSets写的有问题?
23、先判断路径是否有错,参考https://blog.csdn.net/smallwei2014/article/details/74475105的写法,我们可以通过Android视图来确认路径指向问题
当我们未修改build.gradle(app)文件时,该目录指向src/main/jniLibs目录,当修改后,指向新的目录.
我们可以这么玩,先在project视图下创建这几个文件
然后切换到Android视图,先把gradle的sourceSets部分注释,右上角刷新
可以看到,指向了默认的路径,显示了我们创建的文件
然后取消注释,把路径改为libs
显示了app下的libs里的文件。
为什么是libs?如果你还记得之前c文件找h文件时使用的定位方式,那么你就可以理解,build.gradle(app)和libs文件夹,是在同一个目录src下的,build.gradle(app)中的代码想去找libs,自然不需要加src前缀
还有这里为什么会有两个txt文件呢?我觉得可能是默认路径一定会被读取,是为了防止路径设置错误之类的问题,而且标准也倡导使用默认路径。
我们再将路径改为src/libs
可以看到src/libs下的所有文件都显示出来了。
此时我们再运行一次看看
可以自己再验证验证,比如放到src/main/java/libs目录下试试。
2.10、关于sourceSets的其他写法
还有其他的写法,比如把jniLibs和srcDirs合在一起
android {
...
sourceSets{
main{
jniLibs.srcDirs "src/libs"
}
}
...
}
刷新,BUILD SUCCESSFUL
干脆所有合在一起
sourceSets.main.jniLibs.srcDirs "src/libs"
刷新,BUILD SUCCESSFUL
上面这种写法的优点就是简洁,一行搞定,缺点是不能使用main()代替main,因为main()后面必须接{},否则会提示Gradle DSL method not found: 'main()'
也可以换一种赋值方式,使用等号加中括号的方式,和空格加字符串没有区别
sourceSets.main.jniLibs.srcDirs=["src/libs"]
ps:不可以等号直接加字符串(srcDirs="src/libs"),会提示
Cannot cast object 'src/libs' with class 'java.lang.String' to class 'java.lang.Iterable',意思是无法将字符串对象转换成迭代器对象。
运行:
三、总结
重复一下开始时的结论:
1、java文件、h头文件、c文件、mk文件、so文件放在什么目录并没有什么关系,不会影响最后的运行。
2、所有的文件链接都是通过相对路径的方式去寻找的,比如c对h文件的寻址是通#include相对路径,mk文件对c文件的寻址是通过LOCAL_SRC_FILES这一句,ndk-build对mk文件的寻址是通过参数,gradel.build对jniLibs夹(so文件)的寻址是通过sourceSets这一句。
3、由指令生成文件的方式,比如先进入相关依赖文件的目录下。比如使用javac生成.h文件,必须进入java文件所在的目录,通过ndk-build生成so文件,必须进入mk文件所在文件夹。
4、找不到so文件,一方面可能是gradel.build中sourceSets格式写的不对,也有可能是路径指向有问题,要用Android视图检查。
5、程序没有报错,但是运行闪退,可能是java文件中loadLibrary加载so文件的名称写错,可以检查下,要跟Android.mk中的LOCAL_MODULE一致。如果一致还是不行,建议删掉so文件再生成一次。