我们公司app要把核心功能进行封装,封装成SDK方便以后得开发和集成。在开发SDK时可能要用到JNI的开发。而且之前道长写的JNI还是用的eclipse。现在使用Android Studio要重新熟悉一下,发现有很多容易忽视的小细节。造成编译不成功等。现在和小伙伴们分享一下道长踩的坑(重点是记录一下,万一在掉坑里那不丢人了)。
一、概念介绍
在我们开始之前我们首先要了解一下几个概念:
1.JNI
- JNI的定义
JNI 是本地编程接口(Java Native Interface,简称JNI),允许Java代码和其他语言的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++设计的,但是它并不妨碍使用其他语言,只要调用约定受支持就可以了。也就是说JNI实际上就是一套协议。 - JNI的必要性
1)与底层交互。Java的虚拟机的“一处编译,到处运行”使得编译和跨平台运行大大简化,但是也隔绝了Java与底层C语言的交互。JNI使得在 Java 虚拟机内部运行的 Java 代码能够与用其它语言(如 C、C++)编写的代码进行交互。
2)代码的保护。由于apk的java代码很容易被反编译,而C/C++库反汇难度较大,所以一般app的核心代码要使用C/C++编写。
3)提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
4)特殊的业务场景,比如电视,车载系统,微波炉等与硬件直接相关的开发。
2.NDK
NDK定义
NDK是本地开发工具包(Native Development Kit,简称NDK),NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。NDK集成了交叉编译器(交叉编译器需要UNIX或LINUX系统环境),并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。.so文件
编译生成 .so文件是一个C/C++的函数库。在android的JNI中,要先将相应的C语言打包成so库,然后导入到lib文件夹中供java调用。每个不同架构的芯片都有不同的 .so文件。
二、NDK安装
这里首先说一下Android Studio版本,开始的时候道长使用的是3.0 Canary 5版本的。生成.so文件什么的没有问题,但是最后把项目编译到手机上是会报错。(截图丢了-_-!)。然后道长又使用Android Studio版本是2.2.1的,如果有需要其他版本的Studio点击传送门:android studio 下载(记得翻墙)
这里NDK的安装有两个方法:
1.方式一:通过Studio直接下载安装
点击打开,方法如下图所示
在弹窗中选择标红位置,如下图所示
点击上图Apply下载安装,重启Android Studio以完成安装
注意:使用这个方法只能安装最新版本的NDK,如果最新版本的NDK不支持当前版本的Android Studio,就用方式二下载合适的版本的NDK或者更换Android Studio版本
2.方式二:在网站上下载安装
下载NDK
点击传送门去官网下载NDK,官网上有各个版本的NDK。
下载完成后,点击Project Structure设置NDK
选择NDK解压后的根目录
注意这里的设置才是使用的NDK,假如已经通过方式一下载安装完成NDKa,然后又使用方式二下载安装NDKb,最终起作用的是NDKb
3.校验NDK是否安装成功
配置环境变量
因为生成.so文件时需要使用ndk-build,所以要配置环境变量,直接写根目录就可以了。
在Studio上的Terminal中输入ndk-build
基本上弹出上面内容就说明ndk安装成功。
三、JNI开发
1.配置项目参数
项目的结构
在项目gradle.properties写如下代码
android.useDeprecatedNdk=true
- 在项目local.properties中修改代码
一般NDK/SDK的路径可以自动生成,不需要修改,但是使用方式二安装NDK时有必要修改NDK的路径(下面的是道长自己的路径,只是个示例):
ndk.dir=D\:\\AS\\android-ndk-r12b
sdk.dir=D\:\\Eclipse\\sdk_r10_eclipse
- 在app文件夹下的build.gradle中的defaultConfig里加入如下代码
ndk{
moduleName "helloJNI" //生成的so文件名字,调用C程序的代码中会用到该名字
abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定三种平台下的so库
}
sourceSets.main {
jni.srcDirs = ['libs']
}
示例图如下:
- 创建Application.mk
在上面gradle文件中有指定输出不同平台的so库,也可以通过Application.mk中指定:
#指定CPU架构
APP_ABI := armeabi
#指定NDK的版本
APP_PLATFORM :=android-8
注意:如果没有配置sourceSets编译.so文件时就会报错:
Execution failed for task ':app:compileDebugNdk'
2.生成相应文件
创建一个jni文件夹
在MainActivity中输入如下代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.tv);
textView.setText(getStringFromNative());
}
//需要用native声明方法 使这个方法成为本地方法;
//本地方法它没有实现,它的实现在本地库(本地函数)里面
public native String getStringFromNative();
}
- 生成 .h文件
打开Android Studio底部的Terminal,默认命令行窗口路径已经在当前项目,输入以下命令:
cd app/src/main/java
javah -jni 包名+类名
注意:如果编码规则不是UTF-8的会报错:
错误: 编码GBK的不可映射字符
错误: 编码GBK的不可映射字符
如果编码规则不是UTF-8就输入以下指令:
cd app/src/main/java
javah -jni -encoding UTF-8 包名+类名
示例如下:
便会在java文件夹下生成一个com_yushan_jnidemo_MainActivity.h。内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yushan_jnidemo_MainActivity */
#ifndef _Included_com_yushan_jnidemo_MainActivity
#define _Included_com_yushan_jnidemo_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yushan_jnidemo_MainActivity
* Method: getStringFromNative
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yushan_jnidemo_MainActivity_getStringFromNative
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
然后把.h文件剪切到jni文件加下。
- 创建.c文件
输入内容如下:
本地函数命名规则 - 返回类型 Java_包名类名方法名(JNIEnv* env,jobject thiz)
本地函数必须俩个参数 - JNIEnv* env,jobject thiz
//JNIEnv 是结构体 struct JNINativeInterface*一级指针
//JNIenv* 是结构体 struct JNINativeInterface*二级指针
//env 是JNIEnv的一级指针
//env 是JNIEnv*的二级指针
//引入上面生成的头文件,并实现头文件中声明的方法
#include "com_yushan_jnidemo_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_yushan_jnidemo_MainActivity_getStringFromNative
(JNIEnv *env, jobject obj){
char *str="String from native C";
//jstring (*NewStringUTF)(JNIEnv*, const char*);
//jstring jstr=(**env).NewStringUTF(env,cstr);
//jstring jstr=(*env)->NewStringUTF(env,cstr);
//return jstr;
return (*env)->NewStringUTF(env, str);
}
注意:如果.c文件和.h文件不是在jni文件的根目录下生成.so文件时有可能会报错:
make: *** No rule to make target `D:/ASProjects/JNIDemo/app/src/main/jni/helloC.c', needed by `D:/ASProjects/JNIDemo/app/src/main/obj/local/arm64-v8a/objs/helloJNI/helloC.o'. Stop.
- 在jni目录下创建Android.mk文件
Android.mk文件内容如下:
#返回当前JNI目录
LOCAL_PATH := $(call my-dir)
#清除上一次的编译信息
include $(CLEAR_VARS)
#指定源文件生成的动态连接库的名称
LOCAL_MODULE := helloJNI
#指定源文件
LOCAL_SRC_FILES := helloC.c
#告诉编译系统生成动态连接库
include $(BUILD_SHARED_LIBRARY)
参数讲解:
LOCAL_MODULE:就是生成的.so文件的名字,要和在app文件夹下的build.gradle中的一致。
LOCAL_SRC_FILES:便是指定C语言的文件,关于.c文件的创建稍后再说。
注意:如果没有创建Android.mk生成.so文件时便会报错:
Android NDK: Your APP_BUILD_SCRIPT points to an unknown file: ./jni/Android.mk
所有生成文件后示例如下:
三、.so文件的生成与使用
1.so文件的生成
在Terminal下执行 ndk-build(注意:要在/app/src/main/java路径下使用,否则不起作用)
目前我们的.so库就全部生成了,生成的so文件都在src/main/libs目录下。
2.so文件的使用
在main文件中创建jniLibs 将我们的libs下的so文件拷贝到下面去
在MainActivity中添加代码块
//固定写法,表示我们要加载的资源文件为libhelloJNI.so
static {
//System.loadLibrary("LOCAL_MODULE指定的名称");
System.loadLibrary("helloJNI");
}
- 运行程序
结果如下:
好了。到这这里基本上已经完毕了,希望能给小伙伴们提供一些帮助。