技术标签: NDK开发 JNI开发 Android NDK开发 Android引入第三方so库
注意:本文操作环境为mac,Android Studio版本3.5
NDK全称Native Development Kit,是Android的一个工具开发包,能够快速开发C,C++的动态库,并自动将so和应用打包成APK。而NDK的使用场景就是通过NDK在Android中使用JNI,那么JNI又是啥呢?JNI全称是Java Native Interface,即Java的本地接口,JNI可以使得Java与C,C++语言进行交互。这么一来,通过NDK和JNI,就可以很方便的在Android的开发环境中使用c,c++的开源库。
可通过Android Studio下载和官网下载,下面为Android Studio下载
启动终端,进入当前用户的home目录
cd ~(注意中间的空格)
创建.bash_profile(假如之前已经创建好了,执行这个命令行不会对原本的文件内容造成影响)
终端输入:touch .bash_profile
查看、编辑.bash_profile
如果忘记了NDK的目录,可以通过Android Studio的File->Project Structure中的NDK location查看
export NDK_ROOT=/Users/{
你的用户名}/Library/Android/sdk/ndk-bundle
export PATH=$PATH:$NDK_ROOT
保存然后关闭.bash_profile文件
更新刚配置的环境变量
终端输入: source .bash_profile
重新打开终端,检查是否配置成功(如果不成功,记得先关闭当前终端然后打开)
终端输入: ndk-build
如果出现下列结果的即为成功
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
在Android Studio新建一个Native C++的工程,然后填写项目名,并选择Toolchain Default使用默认的C++标准。
当点击Finish后,Android Studio会自动添加NDK开发相关的文件。cpp是AS帮我们自动生成的,里面有两个文件:
另外还对MainActivity,build.gradle进行了一些改动。下面将分析这四个重要的文件
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 这里会把 native-lib.cpp转换成共享库,并命名为 native-lib
add_library( # 库的名字
native-lib
# 设置成共享库
SHARED
# 库的源文件(由于native-lib.app和CMakeLists.txt同处于一个包,因此可以直接写文件名
# 不然的话需要写成src/main/cpp/native-lib.cpp)
native-lib.cpp)
# 如果需要使用第三方库,可以使用 find-library来找到
find_library( # so库的变量路径名字,在关联的时候使用
log-lib
# 你需要关联的so名字
log)
# 通过link将源文件的库和第三方库添加进来
target_link_libraries(
# 源文件库的名字
native-lib
# 添加第三方库的变量名
${
log-lib})
需要注意的是第三库是路径变量名,因此需要用${}方式引用
Native-lib.cpp
一看就是C++的代码,这个方法的作用其实就是返回一个字符串“Hello from C++”。需要重点注意的是这个方法的命名格式,包名,类名,方法名其实就是在Java代码中定义这个native方法stringFromJNI所在的包名和类名。
MainActivity
public class MainActivity extends AppCompatActivity {
//加载so库
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
//直接调用native方法
tv.setText(stringFromJNI());
}
//native方法
public native String stringFromJNI();
}
在这里我们验证了native-lib.cpp里面方法的命名格式,在MainActivity确实有一个stringFromJNI的方法。在这里我们首先需要加载so库,so库的名称就是我们在CmakeList.txt定义的库的名字。然后通过定义的native方法就可以调用C++层的Java_com_example_ndkdemo_MainActivity_stringFromJNI方法。
build.gradle
看到这,你可以先运行下,看看AS自动生成的JNI例子是否运行成功了。想必当你看到成功运行后是不是已经热血沸腾,迫不及待的想自己尝试下。并且一个so库中不可能只有一个方法,因此接下来就让我们照猫画虎的添加自己编写的c++文件。
创建Java对应的加载类。在这里我们不准备在MainActivity中加载.so库,而是新建了一个JNI工具类来完成加载.so库和声明native方法的任务。然后将MainActivity中的native方法复制过来,并且新建了一个helloFromJNI的方法。另外为了在新项目中使用该so库,我们将so库的名字更改为hello,下面也会在CMakeList.txt中更改so库的名称。(可以发现下面的native方法是红色的,这是因为我们还没有在C++层中实现这两个方法)
添加需要的C/C++文件。我们直接在cpp中新建一个就行,cpp->右键->new->c/c++ source File。然后就可以命名一个c/c++文件了,并且勾选create an associated header,表示在创建才C/C++文件的同时会创建对应的头文件。
(1) 编写头文件hello.h,你可以将头文件看成Java的接口,在这里我们需要声明方法。
#ifndef NDKDEMO_HELLO_H
#define NDKDEMO_HELLO_H
//声明接口
extern const char* helloWorld();
#endif //NDKDEMO_HELLO_H
(2) 然后在hello.cpp中实现这个头文件,可以发现在这里我们只是简单的返回了一个hello world的字符串
#include "hello.h"
extern const char* helloWorld(){
return "Hello World";
}
在native-lib中引入hello.h头文件。这个操作跟Java中的导包有点类似,并且我们新建了一个在前面JniUtil中声明的native方法,注意的是由于我们将加载so库和声明native方法都放到了JniUtil中,因此我们需要更改之前stringFromJNI的包名和方法名。
#include <jni.h>
#include <string>
#include "hello.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_util_JniUtil_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_util_JniUtil_helloFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string helloStr = helloWorld();
return env->NewStringUTF(helloStr.c_str());
}
CMakeList.txt中加入hello.cpp的路径添加
这里需要注意的是,如果是多次使用add_library,则会生成多个so库。在这里我们只是将多个本地文件编译到一个so库中,因此只需要在原本的add_library中添加hello的相对路径。并且为了方便在新项目中使用该so库,在这里我将之前native-lib的名字改成了hello。因此生成so库的时候也会生成libhello.so文件(生成so库的时候会自动加上lib的前缀)
在MainActivity中使用调用JniUtil中的native方法
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
//直接调用native方法
tv.setText(JniUtil.helloFromJNI());
}
}
运行项目
查看so库。在app->intermediates->cmake中就会生成对应类型的so库,因为生成so库的时候会自动加上lib的前缀。
通常情况下,引入第三方.so库会有两种场景:
在实际开发中,更常见的是第二种场景。两种场景的引入方法不同,第一种可以直接引入第三方so库,而第二种需要引入自己的so库,然后将自己的so库与第三方so库和头文件进行相关联。接下来我们就来分析这两种引入方式。
新建一个普通的Android项目。引入JNI规范的.so库并不需要Native C++类型的项目。
在main中新建一个jniLibs。我们在app->src->main中新建立一个jniLibs,然后将上面生成的libhello.so文件拷贝过来,这里我们直接将上面cmake->debug->obj中的四个文件夹都拷贝过来
新建一个JniUtil类。注意包名和类名都要跟引入so库中的暴露的JNI方法中的一致,接着就是加载hello这个so库,然后声明native方法,这个native方法就是hello.so库中暴露的JNI方法。其实你会发现这个JniUtil中的代码跟上述的NdkDemo中的是一样的。
在MainActivity中使用JniUtil中的native方法
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.tv);
//调用native方法
tv.setText(JniUtil.helloFromJNI() + " & "+JniUtil.stringFromJNI());
}
}
运行项目
可以发现引入JNI规范的.so库是很简单的,因为我们知道hello.so库中接口方法的命名方式,不用CMake,不用编写C++文件,直接在JniUtil中声明native方法,然后运行即可。
这里我们使用场景就是,在Java层中要调用hello.so库中hello.cpp中的helloWorld方法
新建一个Native C++工程。
因为引入这种类型的so库,我们需要创建自己的so文件,然后在自己的so文件里再调用第三方so,最后在Java层中调用自己的so,因此需要进行NDK开发。而上面我们已经分析了新建Native C++工程AS帮我们建立和修改的文件,因此如果你想在原有的项目中进行NDK开发的话,其实就是自己手动增加和修改这些文件即可。
新增文件夹,用来存放要导入的第三方so库以及头文件。主要是在cpp文件中新建include文件和在main中新建jniLibs,然后将第三方的头文件放在include中,第三方so库放入jniLibs中。
配置CMakeLists.txt。我们需要关联第三方头文件到native-lib,并配置好第三方so库以及头文件导入的路径。这里需要注意的是set_target_properties这里配置的so库目录,你可以利用message打印,so库的路径是否正确,CMAKE_SOURCE_DIR代表着CMakeLists.txt的路径,由于我的CMakeLists.txt在cpp中,因此需要加上/…进行回退到上一级的main目录,然后配置libhello.so的相对路径。
cmake_minimum_required(VERSION 3.4.1)
# 利用这个打印路径
message("******************************************************************")
message("CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}")
message("******************************************************************")
# 这里会把 native-lib.cpp转换成共享库,并命名为 native-lib
add_library( # 库的名字
native-lib
# 设置成共享库
SHARED
# 库的源文件(由于native-lib.app和CMakeLists.txt同处于一个包,因此可以直接写文件名
# 不然的话需要写成src/main/cpp/native-lib.cpp)
native-lib.cpp)
# 如果需要使用第三方库,可以使用 find-library来找到
find_library( # so库的变量路径名字,在关联的时候使用
log-lib
# 你需要关联的so名字
log)
#将native-lib关联到第三方库头文件
#由于我的inclue目录与CMakeList都在cpp目录,因此可以直接写include,否则需要写相对目录
include_directories(include)
#导入第三库,不同到第三方库需要分开导入,因为有4个so库需要导入,因此需要4次
add_library(hello SHARED IMPORTED)
#设置导入第三库名称,目标位置
set_target_properties(hello
PROPERTIES IMPORTED_LOCATION
${
CMAKE_SOURCE_DIR}/../jnilibs/${
ANDROID_ABI}/libhello.so)
# 通过link将源文件的库和第三方库添加进来
target_link_libraries(
# 源文件库的名字
native-lib
#第三方库的名称
hello
# 添加第三方库的变量名
${
log-lib})
新建JniUtil用于加载so库和声明native方法。在引入JNI规范的so库时,我们特别强调了该类要与hello.so库中的JniUtil包名,类名要一致。而在这里并不需要,因为在这里我们并不是引入hello.so库,而是引入自己的so库(native-lib),我们只是为了方便管理,然后取JniUtil。
在native-lib.cpp中引入第三方头文件(hello.h)。在这里我们引入了hello.h的头文件,然后实现了对外的JNI方法,在该方法中我们引用了第三方库中的hello.cpp中的helloWorld方法,而这也就是我们引入第三方so库和头文件的最终目的。
注意:JNI方法的包名,类名,方法名与上面的JniUtil一致
#include <jni.h>
#include <string>
#include "hello.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_util_JniUtil_helloFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string helloStr = helloWorld();
return env->NewStringUTF(helloStr.c_str());
}
在MainActivity中引用JniUtil中的native方法
运行项目。你就能发现神奇的HelloWorld
在引入JNI规范的.so库时一定要记得包名,类名要和引入的so库中的一致,不然运行时会报No implementation found for java.lang.String com.example…之类的错误,然后闪退
在引入第三so库的时候,如果你将so库放在src/main/jniLibs时,可以不在项目的build.gradle中配置so库路径,因为AS默认加载so库的路径就是src/main/jniLibs。但是如果放在其他地方的时候,或者不取名jniLibs时,比如我们放在了src/main/jniLib,这时候就得在build.gradle中配置,如下
引入第二种so库和头文件中配置CMakeList.txt的时候,我们会通过set_target_properties来设置目标so库的路径,网上大部分的教程配置的路径都是:${CMAKE_SOURCE_DIR}/jnilibs/${ANDROID_ABI}/so库完整名字.so
,但其实是要看具体情况的,如果你编译或运行的时候出现了类似下面这种的错误,那么大概率是由于so库的路径配置错误导致的。
'F:/Android/JNIDemo/app/src/main/cpp/jnilibs/armeabi-v7a/libhello.so', needed by 'F:/Android/JNIDemo/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so', missing and no known rule to make it
这时候我们可以利用message来打印${CMAKE_SOURCE_DIR}/jnilibs/${ANDROID_ABI}/so库完整名字.so
这个路径,然后对比一下你引入第三方so库的位置,就可以进行判断是否路径配置错误。
添加打印信息后我们进行编译,如果编译错误的话,应该能够在build中看到打印的message信息,如果看不到的话,可以查看app->.cxx->cmake->debug->随便一个机型->build_output.txt中的打印信息,然后对比你引入第三方库的位置。因为我的jnilibs是放在main层,所以这个路径明显是错误的,因此需要在${CMAKE_SOURCE_DIR}
加上/…进行回退一个目录,即最终的目录应该为${CMAKE_SOURCE_DIR}/../jnilibs/${ANDROID_ABI}/libhello.so
如果你确定你的路径配置没有错误,那么你也可以看看报错的so库位置,也有可能是因为你运行的环境缺少了相关的机型,比如在虚拟机中运行需要x86的环境,而你引入so库的时候没有将x86的so库导进来,或者是说你的手机运行需要arm64-v8a或armeabi-v7a的环境,但是你没有引入对应的环境,也有可能
missing and no known rule to make it
的错误,所以最好是将所有机型的so库文件拷贝过来。
自己是NDK开发和C++的小白,所以整个过程下来感觉收获很多。从安装到使用NDK开发,一路上下来也踩了不少的坑,所以想记录这整个过程,如果有错误的还请大家多多包涵,同时也欢迎大家指出错误。
参考博客:
文章浏览阅读1w次,点赞2次,收藏27次。来源:机器人小妹 很多时候企业拥有重复,乏味且困难的工作流程,这些流程往往会减慢生产速度并增加运营成本。为了降低生产成本,企业别无选择,只能自动化某些功能以降低生产成本。 通过数字化..._人工智能平台
文章浏览阅读2.2k次。热加载能够在每次保存修改的代码后自动刷新 electron 应用界面,而不必每次去手动操作重新运行,这极大的提升了开发效率。安装 electron 热加载插件热加载虽然很方便,但是不是每个 electron 项目必须的,所以想要舒服的开发 electron 就只能给 electron 项目单独的安装热加载插件[electron-reloader]:// 在项目的根目录下安装 electron-reloader,国内建议使用 cnpm 代替 npmnpm install electron-relo._electron-reloader
文章浏览阅读942次。在11.0 进行定制化开发,会根据需要去掉recovery模式的一些选项 就是在device.cpp去掉一些选项就可以了。_android recovery 删除 部分菜单
文章浏览阅读3.7k次。https://www.yuque.com/mnn/cn/cvrt_linux_mac基础依赖这些依赖是无关编译选项的基础编译依赖• cmake(3.10 以上)• protobuf (3.0 以上)• 指protobuf库以及protobuf编译器。版本号使用 protoc --version 打印出来。• 在某些Linux发行版上这两个包是分开发布的,需要手动安装• Ubuntu需要分别安装 libprotobuf-dev 以及 protobuf-compiler 两个包•..._mnn 编译linux
文章浏览阅读1.8k次。CSS3新增动画属性“@-webkit-keyframes”,从字面就可以看出其含义——关键帧,这与Flash中的含义一致。利用CSS3制作动画效果其原理与Flash一样,我们需要定义关键帧处的状态效果,由CSS3来驱动产生动画效果。下面讲解一下如何利用CSS3制作淡入淡出的动画效果。具体实例可参考刚进入本站时的淡入效果。1. 定义动画,名称为fadeIn@-webkit-keyf_css3入场效果淡入淡出
文章浏览阅读2.8k次。计算机系统应包括硬件和软件两个子系统,硬件和软件又必须依次分别包括中央处理器和系统软件。按人的要求接收和存储信息,自动进行数据处理和计算,并输出结果信息的机器系统。计算机是脑力的延伸和扩充,是近代科学的重大成就之一。计算机系统由硬件(子)系统和软件(子)系统组成。前者是借助电、磁、光、机械等原理构成的各种物理部件的有机组合,是系统赖以工作的实体。后者是各种程序和文件,用于指挥全系统按指定的要求进行..._计算机系统包括硬件系统和软件系统 软件又必须包括
文章浏览阅读7.9k次,点赞3次,收藏22次。一 定义这是最早出现的置换算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。该算法实现简单,只需把一个进程已调入内存的页面,按先后次序链接成一个队列,并设置一个指针,称为替换指针,使它总是指向最老的页面。但该算法与进程实际运行的规律不相适应,因为在进程中,有些页面经常被访问,比如,含有全局变量、常用函数、例程等的页面,FIFO 算法并不能保证这些页面不被淘汰。这里,我_进程调度fifo算法代码
文章浏览阅读133次。rownum是oracle才有的写法,rownum在oracle中可以用于取第一条数据,或者批量写数据时限定批量写的数量等mysql取第一条数据写法SELECT * FROM t order by id LIMIT 1;oracle取第一条数据写法SELECT * FROM t where rownum =1 order by id;ok,上面是mysql和oracle取第一条数据的写法对比,不过..._mysql 替换@rownum的写法
文章浏览阅读790次,点赞3次,收藏4次。官网下载下载链接:http://www.eclipse.org/downloads/点击Download下载完成后双击运行我选择第2个,看自己需要(我选择企业级应用,如果只是单纯学习java选第一个就行)进入下一步后选择jre和安装路径修改jvm/jre的时候也可以选择本地的(点后面的文件夹进去),但是我们没有11版本的,所以还是用他的吧选择接受安装中安装过程中如果有其他界面弹出就点accept就行..._ecjelm
文章浏览阅读245次。原文链接:https://linux.cn/article-7801-1.htmlifconfigping <IP地址>:发送ICMP echo消息到某个主机traceroute <IP地址>:用于跟踪IP包的路由路由:netstat -r: 打印路由表route add :添加静态路由路径routed:控制动态路由的BSD守护程序。运行RIP路由协议gat..._ifconfig 删除vlan
文章浏览阅读224次。reduxredux里要求把数据都放在公共的存储区域叫store里面,组件中尽量少放数据,假如绿色的组件要给很多灰色的组件传值,绿色的组件只需要改变store里面对应的数据就行了,接着灰色的组件会自动感知到store里的数据发生了改变,store只要有变化,灰色的组件就会自动从store里重新取数据,这样绿色组件的数据就很方便的传到其它灰色组件里了。redux就是把公用的数据放在公共的区域去存..._redux redis
文章浏览阅读2.2k次,点赞3次,收藏6次。unzip版本不支持4G以上的压缩包所以要使用p7zip:Linux一个高压缩率软件wget http://sourceforge.net/projects/p7zip/files/p7zip/9.20.1/p7zip_9.20.1_src_all.tar.bz2tar jxvf p7zip_9.20.1_src_all.tar.bz2cd p7zip_9.20.1make && make install 如果安装失败,看一下报错是不是因为没有下载gcc 和 gcc ++(p7_linux 7za解压中文乱码