【Android】ndk-build各个文件的路径

先上结论

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文件再生成一次。

目录

一、普通流程

1.1、准备工作

1.2、java文件

1.3、h文件

1.4、c文件

1.5、mk文件

1.5、so文件(ndk-build命令)

二、作死流程

2.1、准备工作

2.2、java文件放默认目录

2.3、生成h头文件的命令解析

2.4、c文件不放jni目录

2.5、mk文件不放jni目录

2.6、ndk-build的参数

2.7、无法import A

2.8、Application.mk中的平台指定问题

2.9、不使用jniLibs,修改build.gradle(app)

2.10、关于sourceSets的其他写法


一、普通流程

在理解之前,先按照普通流程走一遍,确保可以运行,后续再一点点修改,比如把c文件或者so文件放到其他目录下,不使用jni或者jniLibs文件夹名,看看会发生什么,用来加深理解。

1.1准备工作

1、创建empty工程

 

2、进入Project Structure

 

3、选择NDK所在路径

 

4、如果这一步没有NDK,就去设置里面下载

 

勾选后点击OK即可自动下载。

说明:这里没有LLDB,但是目前不影响,可以继续执行,后续使用CMake会用到。

1.2java文件

5、创建一个java文件,在里面写一个公共静态本地字符串方法

 

文件名和方法名写那么短是为了后面生成h文件时方法名看起来不会太长。

6、底部点击Terminal,并输入cd空格

 

7、右键Java文件所在的包名,选择Copy Path

 

8、选择绝对路径

 

9、然后回到之前的Terminal,粘贴路径,回车,进入java文件所在路径

 

1.3h文件

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.4c文件

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.5mk文件

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.5so文件(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.2java文件放默认目录

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.4c文件不放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.5mk文件不放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.6ndk-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.8Application.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文件再生成一次。

 

 

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值