一、概述
搞安卓的工作中难免需要使用native的方法,高效,安全。。。优点就不说了。以前使用到native方法的时候,都是临时抓起来一种方式就用了,也没详细整理两种方式的差别和详细的使用方式,虽然不复杂,但是中间还是有很多小细节需要注意的。虽然ndk-build的方式谷歌官方已经不支持使用了,新版的studio和ndk中工具中已经将其移除了,但是还是有必要了解怎么使用的,以备不时之需,cmake是官方推荐的方式,使用起来也很方便,基本studio把需要的步骤都给你创建好了,当离开studio,手写的时候你还能知道怎么写吗?第一步写什么,第二部写什么来着?今天就主要总结了下两种编译方式的详细步骤和方法,以免日后忘记了。
二、ndk-build方式
1.新建Java类,声明native方法和参数
eg:
public class NdkJniUtils {
public native String getCLanguageString();
}
2.使用的地方引用加入的native工具类
eg:
NdkTest ndkTest = new NdkTest();
TextView tv2 = (TextView) findViewById(R.id.tv_2);
tv2.setText(ndkTest.getStringFromC());
3.编译一下工程,将native工具类编译为class文件
找到指定目录:
projectname\app\build\intermediates\classes\debug
输入命令行:
javac HelloJNI.java
或者makeproject进行编译
注:命令行编译后的class文件会生成在当前路径下
4.找到对应的class文件
利用Android Studio的Terminal,进入你自己的Android工程文件的对应的class目录,
在Terminal中输入命令
cd \app\build\intermediates\classes\debug
或者在进入指定目录后再打开控制台即可找到编译成功的class文件
5.利用javah生成对应的 .h头文件
androidstudio的Terminal中cd到~/workspace/projectname/app/src/main/java目录
在Terminal中输入命令
输入
javah -classpath . -jni com.demo.testc.MyTestC (-jni为默认值可省略)
注意:classpath后面有个 "." 前后都有空格
com.demo.testc.MyTestC 是自己要转换.h文件的类的全路径名;
可以加入-d 参数来指定生成的.h头文件位置,如 -d ../jni
mac系统下一定要加入-classpath 参数,否则编译时会报找不到com.xxx类的错误
javah -classpath ~\workspace\TestC\app\src\main\java com.demo.testc.MyTestC
~\workspace\TestC\app\src\main\java 要生成.h文件的类的全路径 com.demo.testc.MyTestC 就是包名+类名 ,注意指令之间的空格
指令用法说明:
javah -d jni -jni -classpath ..\..\build\intermediates\classes\debug com.demo.JniTest
使用以上命令需要先对native类进行编译为class文件
Javah命令的参数说明如下:
-d
-jni 生成Jni样式的标头文件
-classpath
..\..\..\build\intermediates\classes\debug(这个是类所在的路径)
com.demo.JniTest(类的包名)
用法:
javah [options]
-o
-d
-v -verbose 启用详细输出
-h --help -? 输出此消息
-version 输出版本信息
-jni 生成 JNI 样式的标头文件 (默认值)
-force 始终写入输出文件
-classpath
-bootclasspath
在我们正常使用的时候只需要简单的几个参数即可,我们以Hello这个类来举例说明:
javah -d ~\workspace\TestC com.demo.testc.NdkTest
6.编写C/C++代码
建立jni目录,编写对应的.c或者.cpp文件,引入生成的.h头文件,以及从.h中拷贝方法体,实现方法,实现函数时注意参数和头文件中定义的有所不同,定义时只定义参数类型,没有定义参数名
//
// Created by wangjp on 2018/10/18.
//
#include
#include
#include
using namespace std;
/*
* Class: com_demo_testc_NdkTest
* Method: getStringFromC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringFromC
(JNIEnv *env, jobject instance) {
return env->NewStringUTF("自己编写的c文件");
}
/*
* Class: com_demo_testc_NdkTest
* Method: getStringWithParams
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_demo_testc_NdkTest_getStringWithParams
(JNIEnv *env, jobject instance, jstring paras_) {
const char *paras = env->GetStringUTFChars(paras_, 0);
string s = string(paras);
string s1 = "我的c文件";
s = s + s1;
env->ReleaseStringUTFChars(paras_, paras);
return env->NewStringUTF(s.c_str());
}
7.编写编译的配置文件
#每个Android.mk文件必须以LOCAL_PATH开头,在整个开发中,它通常别用做定位资源文件,例如,功能宏my-dir提供给编译系统当前的路径。
LOCAL_PATH := $(call my-dir)
#CLEAR_VARS指编译系统提供一个特殊的GUN MakeFile来为你清除所有的LOCAL_XXX变量,LOCAL_PATH不会被清除。使用这个变量是因为在编译系统时,所有的控制文件都会在一个GUN Make上下文进行执行,而在此上下文中所有的LOCAL_XXX都是全局的。
include $(CLEAR_VARS)
#LOCAL_MODULE变量是为了确定模块名,并且必须要定义。这个名字必须是唯一的同时不能含有空格。会自动的为文件添加适当的前缀或后缀,模块名为“foo”它将会生成一个名为“libfoo.so”文件。
LOCAL_MODULE := myjni
#包含一系列被编译进模块的C 或C++资源文件
LOCAL_SRC_FILES := JNI_C++.cpp
#指明一个GUN Makefile脚本,并且收集从最近“include$(CLEAR_VARS)”下的所有LOCALL_XXX变量的信息,最后告诉编译系统如何正确的进行编译。将会生成一个静态库hello-jni.a文件或者动态库libhello-jni.so。
include $(BUILD_SHARED_LIBRARY)
总结:
LOCAL_PATH即为调用命令的所在目录,你在哪个目录下使用cmd命令,这里就会返回它的路径地址
LOCAL_MODULE你生成的文件名称是什么,输出之后会自动在名称的前后加上lib和.so
LOCAL_SRC_FILES要对哪个文件进行编译
可没有,主要指定so调用库名以及编译的so对应CPU平台
若没有,则可在build.gradle中设置,此时使用系统进行编译,而非使用ndk-build
APP_MODULES := MyJni
APP_ABI := all all代表全平台
8.编译方式选择
在app module目录下的build.gradle中设置库文件名(生成的so文件名)。找到gradle文件的defaultConfig这项,在里面添加如下内容:
defaultConfig {
......
ndk{
moduleName "JniLibName" //生成的so名字
ldLibs "log", "z", "m" //添加依赖库文件,如果有log打印等
abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定cpu体系结构下的so库。
}
}
externalNativeBuild {
ndkBuild { path file("src/main/java/jni/Android.mk")
}
}
sourceSets {
main {
jni.srcDirs('src/main/java/jni')
}
}
此种生成的so文件在app/build/intermediates/ndkBuild/debug下
defaultConfig {
......
sourceSets.main{
jni.srcDirs = []
jniLibs.srcDirs "src/main/java/libs"
}
}
9.在native方法的申明中引用so库
static {
System.loadLibrary("myjni"); //defaultConfig.ndk.moduleName
}
10.可能遇到的问题
> Error: Your project contains C++ files but it is not using a supported native build system.
解决:
意思是项目中没有使用NDK的配置,解决方法是在gradle.properties文件中添加如下配置:
android.useDeprecatedNdk=true
需要注意的是你的jni所在module 的gradle需要如下配置,一般来说在创建jni folder时就已经自动创建了:
sourceSets {
main {
jni.srcDirs = ['src/main/java/jni', 'src/main/java/cpp']
}
}
ndk-build后报错
fatal error: 'string' file not found
#include
^~~~~~~~
1 error generated.
解决:
在网上搜索了一大圈, 原来是需要让Android NDK支持STL(Standard Template Library)
Import STL libraries to the Android NDK code
将Application.mk放在jni目录下,添加以下内容:
APP_STL := stlport_static
三:CMake方式
1.创建项目时勾选包含c/c++文件,或者手动创建cpp目录并创建.c/.cpp文件,创建CMakeLists.txt文件
2.在对应的类中定义对应的native方法,并实现.c/.cpp中对应的native方法
3.配置CMakeLists.txt文件,主要关注以下几个方法
- 设置构建本地库所需要的CMake的最低版本
cmake_minimum_required(VERSION 3.4.1)
- 设置本地库的名称,类型,路径
创建并命名一个库,将其设置为静态或共享动态(安卓只支持加载动态so库,但是动态库依赖库可以是静态库.a),并提供其源代码的相对路径。您可以定义多个库,并为您构建CMake。Gradle会自动将共享库与APK打包。
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
- 找到需要添加的其他本地依赖库,配置其名称,路径
搜索指定的预构建库并将路径存储为变量。因为CMake在默认情况下在搜索路径中包含系统库,所以您只需要指定要添加的公共NDK库的名称
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
- 将需要依赖的三方库引入本地库
指定CMake应该链接到目标库的库。您可以链接多个库,例如在这个构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
4.配置build.gradle文件
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.demo.testc"
minSdkVersion 15
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
// 配置需要的cpu平台
ndk {
abiFilters "armeabi", "armeabi-v7a","arm64-v8a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
// cmake方式编译时再打开,不能和ndk-build方式共存
// cmake {
// path "CMakeLists.txt"
// }
// 使用gradle编译ndk时打开,指定编译方式
ndkBuild {
path file("src/main/java/jni/Android.mk")
}
}
//此种为使用ndk-build命令行编译成so后指定资源
// sourceSets {
// main {
// jniLibs.srcDirs('src/main/java/libs')
// }
// }
// 此种为使用gradle编译ndk,指定编译的c源文件
sourceSets {
main {
jni.srcDirs('src/main/java/jni')
}
}
}
5.在对应的类中调用native方法
首先使用静态代码块加载动态库
static {
System.loadLibrary("myjni"); //defaultConfig.ndk.moduleName
}
然后即可调用动态库中的函数
6.可能遇到的问题
解决:由于当前使用的ndk的版本高于r16,目前最新的是18
16.1.4479499 |
Update Available: 18.1.5063045 |
,新版本的ndk已经移除了对这几个平台的支持,如果还需要使用,只能降低ndk的版本(<=16),具体原因可以看官方说明https://developer.android.com/studio/build/configure-apk-splits
总结:
好了能力有限,就先是这么多,详细的可以看谷歌官方文档,https://developer.android.com/ndk/guides/大部分还是中文的,真是大大的方便了我等英语渣渣啊,最后附上demo的下载地址https://download.csdn.net/download/you__are_my_sunshine/10730266