Android 之 JNI 开发 详解

自古美人都是妖i 提交于 2019-12-02 18:50:16

NDK项目源码地址 : 

-- 第一个JNI示例程序下载 :  GitHub - https://github.com/han1202012/NDKHelloworld.git 

 

 

一. JNI介绍

 

1. JNI引入

 

JNI概念 : Java本地接口, Java Native Interface, 它是一个 协议, 该协议用来沟通Java代码和外部的本地C/C++代码, 通过该协议 Java代码可以调用外部的本地代码, 外部的C/C++ 代码可以调用Java代码;

 

C和Java的侧重 : 

-- C语言 : C语言中最重要的是 函数 function; 

-- Java语言 : Java中最重要的是 JVM, class类, 以及class中的方法;

 

C与Java如何交流 : 

-- JNI规范 : C语言与Java语言交流需要一个适配器, 中间件, 即 JNI, JNI提供了一种规范; 

-- C语言中调用Java方法 : 可以让我们在C代码中找到Java代码class中的方法, 并且调用该方法; 

-- Java语言中调用C语言方法 : 同时也可以在Java代码中, 将一个C语言的方法映射到Java的某个方法上; 

-- JNI桥梁作用 : JNI提供了一个桥梁, 打通了C语言和Java语言之间的障碍;

 

2. Android中的应用程序框架

 

正常情况下的Android框架 : 最 顶层是 Android的应用程序代码, 是纯Java代码, 中间有一层的 Framework框架层代码是 C/C++代码, 通过Framework进行系统调用, 调用底层的库 和 linux 内核;

 

 

使用JNI时的Android框架 : 绕过Framework提供的调用底层的代码, 直接调用自己写的C代码, 该代码最终会编译成为一个库, 这个库通过JNI提供的一个Stable的ABI 调用linux kernel;ABI是二进制程序接口 application binary interface.

 

 

3. JNI作用

 

JNI作用 : 

-- 扩展: JNI扩展了JVM能力, 驱动开发, 例如开发一个wifi驱动, 可以将手机设置为无限路由;

-- 高效 : 本地代码效率高, 游戏渲染, 音频视频处理等方面使用JNI调用本地代码, C语言可以灵活操作内存;

-- 复用 : 在文件压缩算法 7zip开源代码库, 机器视觉 openCV开放算法库 等方面可以复用C平台上的代码, 不必在开发一套完整的Java体系, 避免重复发明轮子;

-- 特殊 : 产品的核心技术一般也采用JNI开发, 不易破解;

 

Java语言执行流程 : 

-- 编译字节码 : Java编译器编译 .java源文件, 获得.class 字节码文件;

-- 装载类库 : 使用类装载器装载平台上的Java类库, 并进行字节码验证;

-- Java虚拟机 : 将字节码加入到JVM中, Java解释器 和 即时编译器 同时处理字节码文件, 将处理后的结果放入运行时系统;

-- 调用JVM所在平台类库 : JVM处理字节码后, 转换成相应平台的操作, 调用本平台底层类库进行相关处理;

 

 

Java一次编译到处执行 : JVM在不同的操作系统都有实现, Java可以一次编译到处运行, 字节码文件一旦编译好了, 可以放在任何平台的虚拟机上运行;

.

 

二. NDK详解

 

1. 交叉编译库文件

 

C代码执行 : C代码被编译成库文件之后, 才能执行, 库文件分为 动态库 和 静态库 两种;

-- 动态库 : unix环境下 .so 后缀的是动态库, windows环境下 .dll 后缀的是动态库; 动态库可以依赖静态库加载一些可执行的C代码;

-- 静态库 : .a 后缀是静态库的扩展名;

 

库文件来源 : C代码 进行 编译 链接操作之后, 才会生成库文件, 不同类型的CPU 操作系统 生成的库文件是不一样;

-- CPU分类 : arm结构, 嵌入式设备处理器; x86结构, pc 服务器处理器; 不同的CPU指令集不同;

-- 交叉编译 : windows x86编译出来的库文件可以在arm平台运行的代码;

-- 交叉编译工具链 : Google提供的 NDK 就是交叉编译工具链, 可以在linux环境下编译出在arn平台下执行的二进制库文件;

 

NDK作用 : 是Google提供了交叉编译工具链, 能够在 linux平台编译出在 arm平台下执行的二进制库文件;

 

NDK版本介绍 : android-ndk-windows 是在windows系统中的cygwin使用的, android-ndk-linux 是在linux下使用的;

 

2. 部署NDK开发环境

 

(1) 下载Cygwin安装器

 

下载地址 : http://cygwin.com/setup-x86.exe , 这是下载器, 可以使用该下载器在线安装, 也可以将cygwin下载到本地之后, 在进行安装;

 

安装器使用 : Cygwin的下载, 在线安装, 卸载 等操作都有由该安装器进行;

-- 本地文件安装 : 选择安装文件所在的目录, 然后选择所要安装的安装包;

-- 在线安装 : 选择在线安装即可, 然后选择需要的安装包;

-- 卸载 : windows上使用其它软件例如360, 控制面板中是无法卸载Cygwin的, 只能通过安装器来卸载;

 

(2) 安装Cygin

 

双击安装器 setup-x86.exe 下一步 : 

 

 

选择安装方式 : 

-- 在线安装 : 直接下载, 然后安装;

-- 下载安装文件 : 将安装文件下载下来, 可以随时安装, 注意安装文件也需要安装器来进行安装;

-- 从本地文件安装 : 即使用下载的安装文件进行安装;

 

 

选择Cygwin安装位置 : 

 

 

选择下载好安装文件位置 : 之前我下了一个完全版的Cygwin, 包括了所有的Cygwin组件, 全部加起来有5.23G, 下载速度很快, 使用网易的镜像, 基本可以全速下载;

 

 

选择需要安装Cygwin组件 : 这里我们只需要以下组件 : binutils , gcc , gcc-mingw , gdb , make , 不用下全部的组件;

 

 

之后点击下一步等待完成安装即可;

.

安装完之后, 打开bash命令窗口, 可以设置下显示的字体, 使用 make -version 查看是否安装成功 : 

 

(3) Cygwin目录介绍

 

以下是Cygwin安装目录的情况 : 该安装目录就是所模拟的 linux 的根目录;

 

对应的linux目录 : 这两个目录进行对比发现, 两个目录是一样的, Cygwin的安装目录就是 linux根目录;

 

cygdrive目录 : 该目录是Cygwin模拟出来的windows目录结构, 进入该目录后, 会发现windows的盘符目录, 通过该目录可以 访问windows中的文件;

 

 

 

(4) 下载NDK工具 

 

从Google的Android开发者官网上下载该工具, 注意NDK工具分类 : 下载地址 - http://developer.android.com/tools/sdk/ndk/index.html -;

-- windows版本NDKandroid-ndk-r9c-windows-x86.zip (32位), android-ndk-r9c-windows-x86_64.zip (64位) 该版本是用在windows上的Cygwin下, 不能直接在windows上直接运行;

-- linux版本NDK : android-ndk-r9c-linux-x86.tar.bz2(32位) , android-ndk-r9c-linux-x86_64.tar.bz2 (64位) , 该版本直接在linux下执行即可;

 

在这里下载windows版本的NDK, 运行在Cygwin上;

 

 

(4) NDK环境介绍

 

NDK工具的文件结构 : 

 

 

ndk-build脚本 : NDK build 脚本是 gun-make 的简单封装, gun-make 是编译C语言代码的工具, 该脚本执行的前提是linux环境下必须安装 make 程序;

 

 

NDK安装在Cygwin中 : 将NDK压缩文件拷贝到Cygwin的根目录中, 解压 : android-ndk-r9c 目录就是NDK目录;

执行以下NDK目录下的 ndk-build 命令 : ./ndk-build ;

执行结果 :

 

Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/android-ndk-r9c/build/core/build-local.mk:148: *** Android NDK: Aborting    。 停止。

 

 

 

 

三. 开发第一个NDK程序

 

1. 开发NDK程序流程

 

a. 创建Android工程

首选创建一个Android工程, 在这个工程中进行JNI开发;

 

b. 声明native方法 : 

注意方法名使用 native 修饰, 没有方法体 和 参数, eg : public native String helloFromJNI();

 

c. 创建C文件 : 

在工程根目录下创建 jni 目录, 然后创建一个c语言源文件, 在文件中引入 include <jni.h> , C语言方法声明格式 jstring Java_shuliang.han.ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env) , jstring 是 Java语言中的String类型, 方法名格式为 : Java_完整包名类名_方法名();

-- JNIEnv参数 : 代表的是Java环境, 通过这个环境可以调用Java里面的方法;

-- jobject参数 : 调用C语言方法的对象, thiz对象表示当前的对象, 即调用JNI方法所在的类;

 

d. 编写Android.mk文件 : 

如何写 查看文档, NDK根目录下有一个 documentation.html 文档, 点击该html文件就可以查看文档, 查看 Android.mk File 文档, 下面是该文档给出的 Android.mk示例 : 

 

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)

 

 

--  LOCAL_PATH : 代表mk文件所在的目录;

--  include $(CLEAR_VARS) : 编译工具函数, 通过该函数可以进行一些初始化操作;

--  LOCAL_MODULE : 编译后的 .so 后缀文件叫什么名字;

--  LOCAL_SRC_FILES: 指定编译的源文件名称;

--  include $(BUILD_SHARED_LIBRARY) : 告诉编译器需要生成动态库;

 

e. NDK编译生成动态库 : 

进入 cygdrive 找到windows目录下对应的文件, 编译完成之后, 会 自动生成so文件并放在libs目录下, 之后就可以在Java中调用C语言方法了;

 

f. Java中加载动态库 : 

在Java类中的静态代码块中使用System.LoadLibrary()方法加载编译好的 .so 动态库;

 

 

NDK平台版本 : NDK脚本随着 android-sdk 版本不同, 执行的脚本也是不同的, 不同平台会引用不同的头文件, 编译的时候一定注意 sdk 与 ndk 版本要一致;

so文件在内存中位置 : apk文件安装到手机上之后, .so动态库文件存在在 data/安装目录/libs 目录下;

 

2. 开发实例

 

 

按照上面的步骤进行开发

 

 

(1) 创建Android工程

 

Android工程版本 : 创建一个Android工程, minSdk 为 7 即 android-2.1, 编译使用的 sdk为 10 即 android-2.3.3 ;

<uses-sdk
        android:minSdkVersion="7"
        android:targetSdkVersion="10" />


 

NDK编译原则 : 编译NDK动态库是 按照最小版本进行编译, 选择编译的平台的时候, 会选择 NDK 7 平台进行编译;

 

        

 

(2) 声明native方法

 

声明native方法, 注意该方法没有方法体 和 参数, 如下 :

 

/*
	 * 声明一个native方法
	 * 这个方法在Java中是没有实现的, 没有方法体
	 * 该方法需要使用C语言编写
	 */
	public native String helloFromJNI();


 

(3) 创建C文件

 

引入头文件: 首先要包含头文件 jni.h, 该 头文件位置定义在 android-ndk-r9c\platforms\android-5\arch-arm\usr\include目录下的 jni.h, 下面是该头文件中定义的一些方法, 包括本项目中使用的 NewString 方法;

jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
jsize       (*GetStringLength)(JNIEnv*, jstring);
const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
jstring     (*NewStringUTF)(JNIEnv*, const char*);
jsize       (*GetStringUTFLength)(JNIEnv*, jstring);


 

 

调用Java类型 : C中调用Java中的String类型为 jstring;

 

C语言方法名规则 : Java_完整包名类名_方法名(JNIEnv *env, jobject thiz), 注意完整的类名包名中包名的点要用 _ 代替;

 

参数介绍 : C语言方法中有两个重要的参数, JNIEnv *env, jobject thiz ;

--  JNIEnv参数 : 该参数代表Java环境, 通过这个环境可以调用Java中的方法;

--  jobject参数 : 该参数代表调用jni方法的类, 在这里就是MainActivity;

 

调用jni.h中的NewStringUTF方法 : 该方法的作用是在C语言中创建一个Java语言中的String类型对象, jni.h中是这样定义的 jstring (*NewStringUTF)(JNIEnv*, const char*), JNIEnv 结构体中包含了 NewStringUTF 函数指针, 通过 JNIEnv 就可以调用这个方法;

 

C语言文件源码 : 

#include <jni.h>

/*
 * 方法名称规定 : Java_完整包名类名_方法名()
 * JNIEnv 指针
 *
 * 参数介绍 :
 * env : 代表Java环境, 通过这个环境可以调用Java中的方法
 * thiz : 代表调用JNI方法的对象, 即MainActivity对象
 */
jstring Java_shuliang_han_ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env, jobject thiz)
{
	/*
	 * 调用 android-ndk-r9c\platforms\android-8\arch-arm\usr\include 中jni.h中的方法
	 * jni.h 中定义的方法  jstring (*NewStringUTF)(JNIEnv*, const char*); 
	 */
	return (*env)->NewStringUTF(env, "hello world jni");
}

 

 

(4) 编写Android.mk文件

 

 

查询NDK文档 : NDK的文档在NDK工具根目录下, 点击 documentation.html 文件, 就可以在浏览器中打开NDK文档;

 

上面的开发流程中详细的介绍了Android.mk 五个参数的详细用处, 这里直接给出源码 : 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)



 

(5) 编译NDK动态库

 

 

进入Cygwin相应目录 : 从Cygwin中的cygdrive 中进入windows的工程jni目录 ;


 

编译hello.c文件 : 注意Android.mk文件 与 hello.c 文件在同一目录中;

 

编译完成后的情况 : 编译完之后 会成成一个obj文件, 在obj文件中会生成 libhello.so, 系统会自动将该 so后缀文件放在libs目录下;

 

 

 

(6) Java中加载动态库

 

静态代码块中加载 : Java中在静态代码块中加载库文件, 调用 System.loadLibrary("hello") 方法, 注意 libs中的库文件名称为 libhello.so我们加载的时候 将 lib 去掉, 只取hello 作为动态库名称, 这是规定的;

//静态代码块加载C语言库文件
	static{
		System.loadLibrary("hello");
	}

 

 

(7) 其它源码

 

MainActivity源码 : 

package shuliang.han.ndkhelloworld;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity {

	//静态代码块加载C语言库文件
	static{
		System.loadLibrary("hello");
	}
	
	/*
	 * 声明一个native方法
	 * 这个方法在Java中是没有实现的, 没有方法体
	 * 该方法需要使用C语言编写
	 */
	public native String helloFromJNI();
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        System.out.println(helloFromJNI());
    }

    public void onClick(View view) {
    	//点击按钮显示从jni调用得到的字符串信息
    	Toast.makeText(getApplicationContext(), helloFromJNI(), 1).show();
	}
    
}


XML布局文件 : 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="显示JNI返回的字符串" />

</RelativeLayout>


 

(8) 将源码上传到GitHub中

 

 

在上一篇博客  http://blog.csdn.net/shulianghan/article/details/18812279 中对GitHub用法进行了详解;

 

在GitHub上创建工程 : 

 

项目地址 

-- HTTP: https://github.com/han1202012/NDKHelloworld.git 

-- SSH : git@github.com:han1202012/NDKHelloworld.git

 

生成的命令 :  

touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:han1202012/NDKHelloworld.git
git push -u origin master


打开 Git Bash 命令行窗口 : 

-- 从GitHub上克隆项目到本地 : git clone git@github.com:han1202012/NDKHelloworld.git , 注意克隆的时候直接在仓库根目录即可, 不用再创建项目根目录 ;

 


-- 添加文件 : git add ./* , 将目录中所有文件添加;

 
-- 查看状态 : git status ;

-- 提交缓存 : git commit -m '提交';

 
-- 提交到远程GitHub仓库 : git push -u origin master ;

 

 

GitHub项目 : 

 

 

3. 项目讲解 

 

(1) Android.mk文件讲解

 

Android.mk文件内容 : 

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)

 

获取当前文件内容 : $(call my-dir) 是编译器中的宏方法, 调用该宏方法, 就会 返回前的目录路径

赋值符号 : " := " 是 赋值符号, 第一句话 是 返回当前文件所在的当前目录, 并将这个目录路径赋值给 LOCAL_PATH;

初始化编译模块参数 : $(CLEAR_VARS) 作用是将编译模块的参数初始化, LOCAL_MODULE LOCAL_SRC_FILES 也是这样的参数;

指定编译模块 : LOCAL_MODULE    := hello , 指定编译后的 so 文件名称, 编译好之后系统会在该名称前面加上 "lib", 后缀加上 ".so";

指定编译源文件 : LOCAL_SRC_FILES := hello.c 告诉编译系统源文件, 如果有多个文件那么就依次写在后面即可; 

编译成静态库 : include $(BUILD_SHARED_LIBRARY), 作用是高速系统, 编译的结果编译成 .so 后缀的静态库;

 

静态库引入 : NDK的platform中有很多 ".a" 结尾的动态库, 我们编译动态库的时候, 可以将一些静态库引入进来;

 

 

(2) 自动生成方法签名

 

 

使用javah工具 : 在C中实现Java调用的jni方法, 方法的签名很复杂, 需要将完整的包名类名方法名都要使用 "_" 连接起来, 很麻烦, jdk提供的生成签名方法的工具;

 

遗留问题 : 目前查到的方法是 在bin目录下 执行 javah -jni 包名类名 命令, 但是执行不成功, 暂时没找到解决方案;

-- Android中会自动生成 .class文件吗, 没发现啊, PY人!

 

 

 

问题解决 : 在bin目录下有一个classes目录, 该目录在eclipse中看不到, 但是实际没目录中是存在的; 

 

 

 

(3) NDK开发中乱码问题

 

解决乱码思路 : C语言编译的时候用的是 ISO-8859-1 码表进行编码, 如果我们使用C语言jni开发, 需要进行转码操作;

-- 将ISO-8859-1转为UTF-8字符: String string = new String(str.getBytes("iso8859-1"), "UTF-8");

 

 

示例 : 

 

添加中文jni调用 : 将jni中的hello.c 中返回的字符串修改为中文, 重新编译 .so 静态库文件;

-- 修改后的hello.c文件如下 : 只改变了返回的字符串, 添加了中文;

#include <jni.h>

/*
 * 方法名称规定 : Java_完整包名类名_方法名()
 * JNIEnv 指针
 *
 * 参数介绍 :
 * env : 代表Java环境, 通过这个环境可以调用Java中的方法
 * thiz : 代表调用JNI方法的对象, 即MainActivity对象
 */
jstring Java_shuliang_han_ndkhelloworld_MainActivity_helloFromJNI(JNIEnv *env, jobject thiz)
{
	/*
	 * 调用 android-ndk-r9c\platforms\android-8\arch-arm\usr\include 中jni.h中的方法
	 * jni.h 中定义的方法  jstring (*NewStringUTF)(JNIEnv*, const char*);
	 */
	return (*env)->NewStringUTF(env, "hello world jni 中文");
}


使用NDK重新编译hello.c文件 : 修改了C源码之后, 重新将该c文件编译成so文件;

-- 编译过程: 打开cygwin, 进入cygdrive/ 下对应windows中源码项目中的jni目录, 执行 /android-ndk-r9c/ndk-build 命令;

 

 

运行Android代码报错 : 因为jni中c文件有中文, 中文不能被识别;

01-31 14:36:04.803: W/dalvikvm(389): JNI WARNING: illegal continuation byte 0xd0
01-31 14:36:04.803: W/dalvikvm(389):              string: 'hello world jni ����'
01-31 14:36:04.803: W/dalvikvm(389):              in Lshuliang/han/ndkhelloworld/MainActivity;.helloFromJNI ()Ljava/lang/String; (NewStringUTF)
01-31 14:36:04.834: I/dalvikvm(389): "main" prio=5 tid=1 NATIVE
01-31 14:36:04.834: I/dalvikvm(389):   | group="main" sCount=0 dsCount=0 obj=0x4001f1a8 self=0xce48
01-31 14:36:04.834: I/dalvikvm(389):   | sysTid=389 nice=0 sched=0/0 cgrp=default handle=-1345006528
01-31 14:36:04.844: I/dalvikvm(389):   | schedstat=( 257006717 305462830 51 )
01-31 14:36:04.844: I/dalvikvm(389):   at shuliang.han.ndkhelloworld.MainActivity.helloFromJNI(Native Method)
01-31 14:36:04.844: I/dalvikvm(389):   at shuliang.han.ndkhelloworld.MainActivity.onCreate(MainActivity.java:26)
01-31 14:36:04.844: I/dalvikvm(389):   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1611)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1663)
01-31 14:36:04.853: I/dalvikvm(389):   at android.app.ActivityThread.access$1500(ActivityThread.java:117)
01-31 14:36:04.864: I/dalvikvm(389):   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:931)
01-31 14:36:04.864: I/dalvikvm(389):   at android.os.Handler.dispatchMessage(Handler.java:99)
01-31 14:36:04.864: I/dalvikvm(389):   at android.os.Looper.loop(Looper.java:123)
01-31 14:36:04.864: I/dalvikvm(389):   at android.app.ActivityThread.main(ActivityThread.java:3683)
01-31 14:36:04.864: I/dalvikvm(389):   at java.lang.reflect.Method.invokeNative(Native Method)
01-31 14:36:04.874: I/dalvikvm(389):   at java.lang.reflect.Method.invoke(Method.java:507)
01-31 14:36:04.874: I/dalvikvm(389):   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
01-31 14:36:04.874: I/dalvikvm(389):   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
01-31 14:36:04.874: I/dalvikvm(389):   at dalvik.system.NativeStart.main(Native Method)
01-31 14:36:04.884: E/dalvikvm(389): VM aborting

 

 

 

四. Java传递数据给C语言与日志打印

 

 

1. JNI数据类型

 

 

Java数据类型 C数据类型 JNI数据类型对比 : 32位 与 64位机器可能会有出入;

 

Java数据类型 C本地类型 JNI定义别名
int long jint/jsize
long __int64 jlong
byte signed char jbyte
boolean unsigned char jboolean
char unsigned short jchar
short short jshort
float float jfloat
double doyble jdouble
object'  _jobject jobject

 

数据类型表示方法 : int数组类型 jintArray , boolean数组 jbooleanArray ...

 

头文件定义类型 : 这些基本的数据类型在 jni.h 中都有相应的定义 : 

jobject     (*CallObjectMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
    jboolean    (*CallBooleanMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jboolean    (*CallBooleanMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
    jbyte       (*CallByteMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jbyte       (*CallByteMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
    jchar       (*CallCharMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jchar       (*CallCharMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
    jshort      (*CallShortMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jshort      (*CallShortMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    jint        (*CallIntMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jint        (*CallIntMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
    jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
    jlong       (*CallLongMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    jlongArray    (*NewLongArray)(JNIEnv*, jsize);
    jfloatArray   (*NewFloatArray)(JNIEnv*, jsize);
    jdoubleArray  (*NewDoubleArray)(JNIEnv*, jsize);


 

2. JNI在Java和C语言之间传递int类型

 

 

Java中定义的方法 : 

//将Java中的两个int值 传给C语言, 进行相加后, 返回java语言 shuliang.han.ndkparameterpassing.DataProvider
	public native int add(int x, int y);


C语言中定义的方法 : 

#include <jni.h>

//方法签名, Java环境 和 调用native方法的类 必不可少, 后面的参数就是native方法的参数
jint Java_shuliang_han_ndkparameterpassing_DataProvider_add(JNIEnv * env, jobject obj, jint x, jint y)
{
	return x + y;
}


使用NDK工具变异该c类库 : 

在cygwin中进入cygdrive, 然后 进入windows中相应的目录, 执行  /android-ndk-r9c/ndk-build 命令, 即可完成编译;

 

 

3. NDK中C代码使用LogCat

 

 

(1) 引入头文件

 

NDK中断点调试 : 断点调试在NDK中实现极其困难, 因此在这里我们一般都是打印日志;

 

引入头文件 : 在C代码中引入下面的头文件;

#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


头文件介绍 : log.h 是关于调用 LogCat日志文件;

-- log.h头文件路径 : android-ndk-r9c\platforms\android-9\arch-arm\usr\include\android\log.h;

-- 主要方法 :  __android_log_write, 下面有该方法的解析, 传入参数 日志等级 日志标签 日志内容;

-- 宏定义 : __android_log_write 方法太麻烦, 这里做出一个映射, LOGD(...) 输出debug级别的日志, LOGI(...) 输出Info级别的日志;

-- LogCat日志级别 : verbose < debug < info < warn < error < assert;

 

使用到的log.h文件内容解析 : __android_log_write 方法中的日志等级参数就使用 枚举中的内容 

/*
 * Android log priority values, in ascending priority order. 日志等级
 */
typedef enum android_LogPriority {
    ANDROID_LOG_UNKNOWN = 0,
    ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */
    ANDROID_LOG_VERBOSE,
    ANDROID_LOG_DEBUG,
    ANDROID_LOG_INFO,
    ANDROID_LOG_WARN,
    ANDROID_LOG_ERROR,
    ANDROID_LOG_FATAL,
    ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */
} android_LogPriority;

/*
 * Send a simple string to the log. 向LogCat中输出日志 
	参数介绍: 日志优先级 , 日志标签 , 日志内容
 */
int __android_log_write(int prio, const char *tag, const char *text);


C语言中输入输出函数占位符介绍 : 

占位符 数据类型
%d int
%ld long int
%c char
%f float
&lf double
%x 十六进制
%O 八进制
%s 字符串

 

.

.

 

(2) Android.mk增加liblog.so动态库

 

在该make配置文件中, 增加一行 : LOCAL_LDLIBS += -llog , 该语句添加在 LOCAL_SRC_FILES 语句下面一行;

 

完整的Android.mk文件 : 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := DataProvider
LOCAL_SRC_FILES := DataProvider.c
#增加log函数对应的函数库 liblog.so  libthread_db.a
LOCAL_LDLIBS += -llog -lthread_db

include $(BUILD_SHARED_LIBRARY)


函数库位置 : android-ndk-r9c\platforms\android-9\arch-arm\usr\lib;

函数库截图 : 从该目录下的 liglog.so可以看出, 存在该库;

 

引入函数库方法 : 使用 LOCAL_LDLIBS += -l函数库名, 注意 函数库名不带 lib前缀 和 .so 后缀, 同时可以添加多个库, 使用 -l库1 -l库2 -库3 ;

 

 

(3) 编译执行

 

根据(1) 中的占位符, 编写打印日志代码

//Java中的int对应的是C语言中的long类型, 对应JNI中的jint类型, C语言中
	LOGI("JNI_日志 : x = %ld , y = %ld" , x , y);


最终的包含打印日志的完整代码 : 注意, 这里有一处可能错误, 如果是32位机器, int类型占位符使用 %d 即可;

#include <jni.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)


//方法签名, Java环境 和 调用native方法的类 必不可少, 后面的参数就是native方法的参数
jint Java_shuliang_han_ndkparameterpassing_DataProvider_add(JNIEnv * env, jobject obj, jint x, jint y)
{
	//Java中的int对应的是C语言中的long类型, 对应JNI中的jint类型, C语言中
	LOGI("JNI_日志 : x = %ld , y = %ld" , x , y);
	return x + y;
}


重新编译C文件 : 执行  /android-ndk-r9c/ndk-build命令;

-- 第一次编译 : 出现警告, long int占位符行不通, 注意区分机器位长, 64位 与 32位不同, 这样编译出现的结果就不会打印日志;

-- 第二次编译 : 将占位符改为 %d ;

 

执行按钮之后打印的日志 : 虽然有乱码, 不过显示出来了;

 

 

 

4. 字符串处理

 

Java中的String转为C语言中的char字符串 : 下面的工具方法可以在C程序中解决这个问题;

// java中的jstring, 转化为c的一个字符数组
char*   Jstring2CStr(JNIEnv*   env,   jstring   jstr)
{
	 char*   rtn   =   NULL;
	 jclass   clsstring   =   (*env)->FindClass(env,"java/lang/String");
	 jstring   strencode   =   (*env)->NewStringUTF(env,"GB2312");
	 jmethodID   mid   =   (*env)->GetMethodID(env,clsstring,   "getBytes",   "(Ljava/lang/String;)[B");
	 jbyteArray   barr=   (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode); // String .getByte("GB2312");
	 jsize   alen   =   (*env)->GetArrayLength(env,barr);
	 jbyte*   ba   =   (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
	 if(alen   >   0)
	 {
	  rtn   =   (char*)malloc(alen+1);         //new   char[alen+1]; "\0"
	  memcpy(rtn,ba,alen);
	  rtn[alen]=0;
	 }
	 (*env)->ReleaseByteArrayElements(env,barr,ba,0);  //释放内存

	 return rtn;
}


C语言方法 : 注意调用Jstring2CStr方法之后要强转, 否则会出错, Jstring2CStr方法要定义在该方法的前面, C语言中的方法要先声明才能使用;

jstring Java_shuliang_han_ndkparameterpassing_DataProvider_sayHelloInc(JNIEnv *env, jobject obj, jstring str)
{
	char *p = (char*)Jstring2CStr(env, str);
	//打印Java传递过来的数据
	LOGI("Java JNI string parameter is : %s", p);
	
	char *append = "append";
	
	//strcat(dest, source) 函数可以将source字符串 添加到dest字符串后面
	return (*env)->NewStringUTF(env, strcat(p, append));
}

-- 如果没有强转会出现下面的错误 : char *p = Jstring2CStr(env, str);

-- 将Jstring2CStr方法定义在主方法下面会出现下面错误 : 

 

Java源码 : 

case R.id.sayHelloInc:
				Toast.makeText(getApplicationContext(), dataProvider.sayHelloInc("Hello"), Toast.LENGTH_LONG).show();
				break;

 

编译之后运行结果 : 

 
 

5. 开发JNI程序流程

 

  • C语言类库接口 : 存在C语言类库, 调用接口为login_server(char* address, char* username, char* password);
  • Java定义本地方法 : public native void LoginServer(String address, String user, String pwd);
  • C语言JNI代码 : Java_包名_类名_LoginServer(JNIEnv* env, jobject obj, jstring address, jstring user, jstring pwd){...调C接口};

 

注意跨语言字符串转换: JNI方法中, 要将Java的String字符串转为C中的char*字符串;

 

首先验证C码农提供的代码是否可用 : 验证该api是否可用, 在一个 int main() 函数中进行测试, 根据该测试代码查看方法执行相关的情况;

 

6. 数组参数处理

模块讲解 : 在该模块中, Java语言传递一个int数组参数给C语言, C语言将这一组参数读取出来, 并且输出到Android的LogCat中, 这里涉及到了两个重要的JNI方法, 一个数 获取数组长度方法, 一个是 获取数组中每个元素的方法;

 

获取数组长度方法 : jni中定义 - jsize (*GetArrayLength)(JNIEnv*, jarray);

创建数组相关方法 : 

jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    jlongArray    (*NewLongArray)(JNIEnv*, jsize);
    jfloatArray   (*NewFloatArray)(JNIEnv*, jsize);
    jdoubleArray  (*NewDoubleArray)(JNIEnv*, jsize);


获取数组元素相关方法 : 

jboolean*   (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*);
    jbyte*      (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);
    jchar*      (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*);
    jshort*     (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*);
    jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
    jlong*      (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*);
    jfloat*     (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*);
    jdouble*    (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*);

 

C语言代码 : 

jintArray Java_shuliang_han_ndkparameterpassing_DataProvider_intMethod(JNIEnv *env, jobject obj, jintArray arr)
{
	//获取arr大小
	int len = (*env)->GetArrayLength(env, arr);
	
	//在LogCat中打印出arr的大小
	LOGI("the length of array is %d", len);
	
	//如果长度为0, 返回arr
	if(len == 0)
		return arr;
		
	//如果长度大于0, 那么获取数组中的每个元素
	jint* p = (*env)->GetIntArrayElements(env, arr, 0);
	
	//打印出数组中每个元素的值
	int i = 0;
	for(; i < len; i ++)
	{
		LOGI("arr[%d] = %d", i, *(p + i));
	}
	
	return arr;
	
}


Java语言代码 : 

case R.id.intMethod:
				int[] array = {1, 2, 3, 4, 5};
				dataProvider.intMethod(array);
				break;


执行结果 : 上面的那种LogCat竟然启动失败, 只能将就着用这个了;

 

.

7. 上传代码到GitHub

 

创建新项目 : han1202012/NDKParameterPassing ;

-- SSH地址 : git@github.com:han1202012/NDKParameterPassing.git ;

-- HTTP地址 : https://github.com/han1202012/NDKParameterPassing.git ;

.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!