
前言
在Android OS上开发应用程序,有提供了两种开发包:SDK和NDK。
我们知道Android的SDK主要是基于Java的,所以在用Android SDK进行开发必须使用Java语言。不过,Android 平台一开始就支持C/C++了,Google说明Android也支持JNI编程,使用第三方应用(比如:NDK)通过JNI调用自己的C动态库。
一、什么是NDK,什么是JNI
1、什么是NDK
NDK全名:Native Develop Kit。
Android NDK 是一个工具集,可让您使用 C 和 C++ 等语言以原生代码实现应用的各个部分。对于特定类型的应用,这可以帮助您重复使用以这些语言编写的代码库。
简单来说,Android NDK 是一个工具集,可以让你通过 C 和 C++ 来实现Android中部分应用程序的功能,不用java开发也可以。
Android 开发语言是Java,Android是基于Linux的,其核心库很多都是C/C++,NDK的作用,就是一个在Java中调用C/C++的方式。NDK本身其实就是一个交叉工作链,包含了Android上的一些库文件,然后,NDK为了方便使用,提供了一些脚本,使得更容易的编译C/C++代码。一般情况,是用NDK工具把C/C++编译为.so文件,然后在Java中调用。
官网
https://developer.android.google.cn/ndk/index.html
2、什么是JNI
JNI全称为Java Native Interface。
JNI是Java的本地接口。Java本支持调用C/C++,即JNI。JNI就是Java调用C++的规范。通过JNI可以使得Java与C/C++进行交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。
二、环境安装
在使用ndk之前需要先安装好环境
1、jdk下载地址(安装过程略过)
此处建议安装jdk1.8
https://www.androiddevtools.cn/
2、NDK安装
下载地址,并配置好JAVA_HOME,PATH的环境变量
https://www.androiddevtools.cn/
我们这里是安装的r10 STL debug info版本,下载后解压到C盘
配置环境变量,将ndk的绝对目录配置的path中
cmd窗口,输入
ndk-build
注意:这里有个报错,是因为少配置文件、所在的目录等问题
3、adb安装
下载
https://www.androiddevtools.cn/
解压,配置环境变量
验证adb安装,cmd直接执行adb
三、JNI接口及用法详解
从上图中可知,JNI接口是处在java层和C/C++层之间,承担桥梁作用
1、JNI详解
前面我们也说了JNI的全称是Java Native Interface: Java本地开发接口,它提供了若干的API实现了Java和其他语言的通信(主要是C和C++),目的就是Java可以调用C或C++开发的函数,C或C++也能调用Java的方法。
特点:
其一就是效率,C/C++是本地语言,比java更高效;
其二就是可以复用已经存在的C/C++代码;
其三是Java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译。
如图JNI接口源码:
JNI接口就是一大堆函数的API,纽带、桥梁的作用
2、JNI接口文件分析
刚开始,#include <**>,典型的C语言中代码
之后我们看到很对typedef,typedef是C语言代码,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char的缩写,例如:
BYTE b1, b2;
详细请参考
https://www.runoob.com/cprogramming/c-typedef.html
typedef uint8_t jboolean;
就是java的boolean数据类型进行重命名为uint8_t。(前面价格j代表为java语言)
3、NI接口讲解
接口中的类型
1.调用java层普通方法-Call0bjectMethod
2.获取Java层实例字段的值-GetMethodID
3.获取java层实列字段的值-GetObjectField
4.设置java层实例字段的值-SetObjectField
......
1、调用java层普通方法-CallObjectMethod
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
**jobject:**返回值是object,若是void是表示返回值为空。
**CallObjectMethod:**方法名,并且后面跟着三个参数,要想使用方法需要构建这三个参数。
**JNIEnv:**动态调试经常用到,本地调用的一个接口,提供了大量的jni接口函数去调用,就理解JNI默认传入的即可!默认参数型。
**jobject:**也是默认参数,那么CallObjectMethod最少有两个默认参数。
**jmethodID:**CallObjectMethod运行操作,还需要一个方法ID,这里是java层方法的ID,这个如何获取呢? 可以使用GetMethodID方法。
想要使用jmethodID参数,先要使用GetMethodID方法,然后把这个方法ID作为一个返回值给jmethodID参数。
...:调用方法参数列表细信息。
2、获取Java层实列字段的值-GetMethodID
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
**jmethodID:**方法的返回值
GetMethodID:方法名称,后面有四个参数
*JNIEnv:** 默认参数
jclass:jclass参数需要调用findclass()方法,获得findclass()方法的返回值,将返回值传入jclass参数使用。
*const char:**java层方法的名称
*const char:** java层方法的一个签名,就是返回值+参数!
3、获取java层实列字段的值-GetObjectField
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
**jobject:**返回值是object。
**GetObjectField:****方法名,后面跟着三个参数。
**JNIEnv:**动态调试经常用到,本地调用的一个接口,提供了大量的jni接口函数去调用,就理解JNI默认传入的即可!默认参数型。
**jobject:**也是默认参数,那么CallObjectMethod最少有两个默认参数。
**jfieldID:**需要GetFieldID()方法返回值获取jfieldID参数
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
调用GetFieldID方法需要四个参数
*JNIEnv:**动态调试经常用到,本地调用的一个接口,提供了大量的jni接口函数去调用,就理解JNI默认传入的即可!默认参数型。
**jclass:**jclass参数需要调用findclass()方法,获得findclass()方法的返回值,将返回值传入jclass参数使用
*const char:**是实列字段的名称
*const char:**是实例字段的签名信息
4、设置java层实例字段的值-SetObjectField
void (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
**void:**返回值为void,代表设置好参数就可以,不需要返回
**JNIEnv:**动态调试经常用到,本地调用的一个接口,提供了大量的jni接口函数去调用,就理解JNI默认传入的即可!默认参数型。
**jobject:**也是默认参数,那么CallObjectMethod最少有两个默认参数。
**jfieldID:**都是GetFieldID的返回值
**jobject:**是一个java层实列字段设置的值
根据上面对接口方法的分析,方法大概分为三类
get:获取
set:配置
call:调用
四、通过编译so文件
1、编写C代码
使用记事本编写C语言实现输出Hello word
#include<stdio.h>
int main(){
printf("Hello word!")
return 0;
}
将文件后缀名修改为.c
2、编写Android.mk文件
1)什么是Android.mk文件
Android.mk 文件位于项目 jni/ 目录的子目录中,用于向构建系统描述源文件和共享库。 它实际上是构建系统解析一次或多次的微小 GNU makefile 片段。 Android.mk 文件用于定义 Application.mk、构建系统和环境变量所未定义的项目范围设置。 它还可替换特定模块的项目范围设置。
Android.mk 的语法用于将源文件分组为模块。 模块是静态库、共享库或独立可执行文件。 可在每个 Android.mk 文件中定义一个或多个模块,也可在多个模块中使用同一个源文件。 构建系统只会将共享库放入应用软件包。 此外,静态库可生成共享库。
除了封装库之外,构建系统还可为您处理各种其他详细信息。例如,您无需在 Android.mk 文件中列出标头文件或生成的文件之间的显式依赖关系。 NDK 构建系统会自动为您计算这些关系。
2)Android.mk内容详述
LOCAL_PATH := $(call my-dir) #获取jni文件路径
include $(CLEAR_VARS)
LOCAL_MODULE := test #模块名称
LOCAL_SRC_FILES := test.c #源文件 .c或者.cpp
LOCAL_ARM_MODE := arm #编译后的指令集ARM指令
LOCAL_LDLIBS += -llog #依赖库
LOCAL_CFLAGS += -pie -fPIE #引入PIE
LOCAL_LDFLAGS += -pie -fPIE #引入PIE
include $(BUILD_SHARED_LIBRARY) #指定编译文件的类型.so
1)LOCAL_PATH :=(call my-dir):获取jni文件路径,每个mk必须以local开始,my-dir是由Build System提供的,他会返回一个包含mk文件的路径,就是要获取相应的文件目录路径去调用jni属性
2)include $(CLEAR_VARS):CLEAR_VARS变量也是由Build System提供,他会指定清理LOCAL_开头的文件,但不会清理LOCAL_PATH开头的!
3) LOCAL_ARM_MODE :=arm:编译后的指令集,arm每个指令由有四个字节。
4)LOCAL_MODULE :=test : 定义的模块名称,如果这里编译出so文件前面+lib后面+.so
5)LOCAL_SRC_FILES :=test.c : 表示同文件目录下.c文件
6) LOCAL_CFLAGS += -pie -fPIE;LOCAL_LDFLAGS += -pie -fPIE :PIE这个安全机制从4.1引入的,但是Android L之前的系统版本并不会去检验可执行文件是否基于PIE编译出的。因此不会报错,但是android L开始开启验证机制,如果调用可执行文件不是基于PIE方式编译的,则无法运行
6)include $(BUILD_SHARED_LIBRARY):把文件构造建成可执行程序,如果是动态链接库就用:include (shared library),如果是静态链接库:include $(static library)
3、编写Application.mk文件代码
1)什么是Application.mk文件
Application.mk文件,这是android NDK构建系统使用的一个可选构建文件。它的目的是描述应用程序需要哪些模块,也定义了所有模块的一些通用变量。
2)Application.mk内容详述
APP_ABI :=X86 armeabi-v7a
这里只有一行代码,这里是关于程序是否可以在不同框架的CPU上运行,目前主流的Android设备是armeabi-v7a架构的,然后就是x86和armeabi了。如果同时包含了 armeabi,armeabi-v7a和x86,armeabi-v7a是可以兼容armeabi的,所有设备都可以运行。
4、ndk编译c文件为so文件
1)编译方式
**第一种方式:**将三个文件放在$PROJECT/jni/目录下, 其中$PROJECT表示你的工程目录,这样就可以被ndk-build脚本文件找到.(注:在这种方式下,进入jni目录,即$PROJECT/jni/,然后执行ndk-build,就可以直接编译jni生成.so文件了)。
第二种方式:另将三个文件放在$NDK/app//目录,其中$NDK为NDK的安装目录,为你的应用程序名.在这种方式下,进入$NDK安装目录,然后输入make APP=,即可编译你的JNI代码.此种方法是ndk-r4之前的方法,虽然出于兼容的原因目录还支持,但是不建议使用此种方法,因为第一种方法简单,且方便。
2)编译测试
首先创建一个名字为jni的文件夹,分别编辑test.c、Application.mk、Android.mk
在jni执行ndk-build,直接编译jni生成.so文件了
ndk-build
生成两个文件夹:libs、obj
libs文件加里面都是ELF文件,是可以直接在安卓执行的
3)libs库中的armeabi-v7a,armeabi和x86
armeabi-v7a,armeabi和x86都表示的是CPU类型,早期的Android系统几乎只支持ARMv5的CPU架构,但是现在已经很多种了。ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起)等,每一种都关联着一个相应的ABI(应用程序二进制接口(ApplicationBinary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库)。Android现在的主流CPU是armeabi-v7a。armeabi-v7a是针对有浮点运算或高级扩展功能的ARMv7CPU。
5、运行so文件
使用adb将armeabi-v7a目录下面的test文件上传到安卓模拟器或者真机(root过后的)
adb devices #检查设备列表
adb push C:\test\libs\armeabi-v7a\test /data/local/tmp/ #将test文件上传安卓的tmp目录下
adb shell #获得安卓的shell
su #切换为root权限
cd /data/local/tmp/ #进入/data/local/tmp/ 目录
ls -al #查看目录下的详细
赋777权限,并执行,测试是否输出hello word
chmod 777 test
./test
五、总结
此章节使用的工具如下:
JDK:需使用jDK1.8B版本的,JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。
adb:全称为Android Debug Bridge,就是起到调试桥的作用。通过adb我们可以在Eclipse中方便通过DDMS来调试Android程序,说白了就是debug工具。
ndk:是Android的一种开发工具包,快速开发C、 C++的动态库,并自动将so和应用一起打包成 APK即可通过 NDK在 Android中 使用 JNI与本地代码(如C、C++)交互。
我们通过此章节熟悉了,ndk工具功能及使用方法,了解了jni的概念,通过实践将jni通过ndk编译为so文件,详细讲解了编译的方法及编译过程中使用的到配置文件详述,为日后进一步分析和调试安卓apk建立概念。如有不足之处请多见谅,谢谢。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
