android NDK开发2

概述

Android NDK就是谷歌借助java JNI来调用本地native代码。具体过程依靠NDK工具与lib库,使用各平台gcc编译工具,make管理工具,交叉编译连接ndklib将c/c++编译为特定平台程序或动态连接库(linux系统不同cpu架构下的elf)。可以手写make构建生成。

而ndk-build就是提供可以在android-SDK生成的项目下,通过更加集成的makefile执行上述流程。而过去的方法中的Android.mk就是给ndk-build识别使用的。

cmake提供的makefile方式更人性化点
AS中Gradle是总得项目管理器,它调用一切(包括cmake),并将.so打包进apk中。

前备知识

  1. make与gcc:http://xn--74q78i15hxv3arigm4e.cn/2018/04/30/gcc%E4%B8%8Emake/
  2. jni与基本俩种ndk使用:http://xn--74q78i15hxv3arigm4e.cn/2018/04/20/android-NDK%E5%BC%80%E5%8F%91/

NDK详解

组件

ARM,x86,MIPS交叉编译器
构建系统
jni
各种c++的android库

目录

  1. build:构建系统的所有模块
  2. platforms:不同目标平台与android版本的头文件与库文件
  3. sources:可导入的现有模块
  4. toolchains:不同目标平台的构建工具
  5. ndk-build:构建用
  6. ndk-gdb:调试用
  7. ndk-stack:排错看栈用

俩个旧mk的使用

详细可参看谷歌https://developer.android.com/ndk/guides/android_mk
这里举个例子:

1
2
3
4
5
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := native-lib.cpp
include $(BUILD_SHARED_LIBRARY)

按行号看:

  1. 表示源文件的位置,my-dir宏表示当前目录下
  2. 清理其它LOCAL_变量,防止冲突
  3. 目标名字,会自动加前后缀(根据5的目标要求)
  4. 源文件名,可多个
  5. 具体构建什么,可以有:共享库,静态库,可执行程序等

ps.还可以指定各种变量用于引入库等

cmake

还是推荐查看谷歌:https://developer.android.com/ndk/guides/cmake
同时上一篇中讲的基本够了

jni:java与c/c++的数据传递

win下用javah生成的:

1
JNIEXPORT void JNICALL Java_a_b_testHello(JNIEnv *env, jobject this)

AS生成的:注意as下如果是cpp文件extern “C” 必加
当你使用错误修正时是生成c版本的时自动不加,当是cpp文件中错误修正时自动加

1
2
3
4
5
extern "C" JNIEXPORT jstring
JNICALL
Java_com_example_imbaya_native2_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */)

JNICALL JNIEXPORT

注意这些都在jni_md中被定义,而这个文件存在与win32文件下,因此与平台有关

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif /* !_JAVASOFT_JNI_MD_H_ */

stdcall:右到左压参,被调用者修改堆栈 declspec(dllexport)微软dll的导出函数

在android-ndk-r16b\sysroot\usr\include\jni.h中的是这样

1
2
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL

JNIEnv

JNI函数表的接口指针,通过这个使用虚拟机功能,来与java进行数据交互

C:
env是指向JNINativeInterface的指针,而传入的是JNIEnv的指针,所以在c下需要解引用并将其作为环境传入。
typedef const struct JNINativeInterface_ *JNIEnv;
C与C++头文件中定义不同。
例子:

1
(*env)->NewStringUTF(env,"123");

C++:
在C++中JNIEnv是C++类
例子:

1
env->NewStringUTF("123");

jobject/jclass

这里用于访问调用该方法的对象/类的数据
实例方法传递jobject
静态方法传递jclass

extern “C”

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

数据结构对照

jni通过提供jni数据类型让原生访问java数据类型

基本数据结构

基本数据结构可以直接通过对照接受处理返回,基本数据类型java对原生公开

java JNI C/C++ 大小
Boolean Jboolean unsigned char 无符号8位
Byte Jbyte char 有符号8位
Char Jchar unsigned short 无符号16位
Short Jshort short 有符号16位
Int Jint int 有符号32位
Long Jlong long long 有符号64位
Float Jfloat float 32位
Double Jdouble double 64位

引用数据类型

引用数据类型不对原生公开。
各类(jclass,jstring,jxxxx)都是typedef jobject jxxxx

1
2
3
4
5
6
7
8
#ifdef __cplusplus
class _jobject {}; //声明并定义
...
#else
struct _jobject; //声明未定义不可实例化
typedef struct _jobject *jobject;
...

各类数组(jxxxArray)都是typedef jarry jxxxArray
jarry表示一般对象数组,typedef jobject jarry

对引用数据类型的操作

基本数据结构可以直接通过对照接受处理返回
引用类型不透明,必须通过JNIEnv使用引用类型。
谨记java引用对象一直都归虚拟机中管理

字符串

C->java

1
2
jstring js=(*env)->NewStringUTF(env,"c字串");
//还可以构建Unicode编码格式(不加UTF)

java->c

1
2
jbyte *str=(*env)->GetStringUTFChars(env,jstring: javastring,Jboolean*: iscopy);
//还可以去UTF变为Unicode编码的jstring,第三个参数决定返回的c字串指向副本还是堆中的固定对象

释放
获得的c字串需要原生中释放(因为在堆中分配的)

1
2
(*env)->ReleaseStringUTFChars(env,javastring,str);
//需要原生作为参数,编码不同函数不同

数组

创建java数组

1
2
jintArray javaArray=env->NewIntArray(10);
//Type可换,参数决定大小

java->c副本与c副本->java

1
2
3
4
5
jint a[10];
env->GetIntArrayRegion(javaArray,0,10,a);
//获取原生数组。类型可换,初,末,a为原生数组
env->SetIntArrayRegion(javaArray,0,10,a);
//向java数组提交更改。

c对java数组的直接操作

1
2
3
4
5
6
7
8
9
jboolean iscopy;
jni * cArray=env->GetIntArrayElements(javaArray,&iscopy);
//直接获取c数组,自己决定是否为复制
//必须释放:
env->ReleaseIntArrayElements(javaArrary,cArrary,0);
//第三个参是释放模式:
//0:内容复制回java并释放c
//JNI_COMMIT复制回但不释放
//JNI_ABORT:释放但不复制回

同时还提供了对NIO的操作
以上为特殊化常用函数
以下为一般化函数

对象的域

1.获取jclass

1
2
3
4
jclass clazz=env->GetObjectClass(jobject);
//通过java实例可以获得javaclass对象
jclass clazz=env->FindClass("类名完全版");
//通过类名获取class如:java/lang/Exception

2.获取域id

1
2
3
4
jfieldID id=env->GetFieldID(clazz,"字段名","字段类型");
//实例,字段类型通过字节码方式表达,比如string是Ljava/lang/String。int是I
jfieldID id=env->GetStaticFieldID(clazz,"字段名","字段类型");
//获取静态字段id

3.获取域

1
2
3
4
jstring js=env->GetObjectField(jobject,jfieldID);
//用来获取实例的字段,Type可变
jstring js=env->GetStaticObjectField(jclass,jfieldID);
//用来获取静态字段

ps:这些方法都是回到java中去获得数据,导致性能下降,建议提前传好。

调用方法

1.获取方法id
方法id可以被缓存,给予多个线程使用

1
2
3
4
jmethodID id=env->GetMethodID(jclass,"方法名","方法原");
//用于实例方法:方法原就是字节码中的样子:()Ljava/lang/String;
jmethodID id=env->GetStaticMethodID(jclass,"方法名","方法原");
//静态用

2.调用方法
传参只要加后面就行

1
2
3
4
jstring fanhui=env->CallStringMethod(jobject,jmethodID);
//实例方法用,参1为需要调用方法的java对象,参2是id
jstring fanhui=env->CallStaticStringMethod(jclass,jmethodID);
//静态用

ps:回调java方法开销大,慎用。

异常

c中捕获java方法中抛出的异常

1
2
3
4
jthrowable ex=env->ExceptionOccurred();
//查询虚拟机中挂起的异常 ex!=0时为有异常
env->ExceptionClear();
//清理虚拟机中的异常

c中抛出java异常

1
2
3
//首先获得javaclass对象
env->ThrowNew(javaclass,"消息");
//原生函数不受虚拟机控制,因此抛出后不会停止原生函数执行并转去异常处理,所以要释放资源,env获得的java引用是局部引用受虚拟机控制自动释放。

局部与全局引用

虚拟机通过追踪实例的引用决定释放对象的时机,在原生函数中通过显示的方式决定何时释放获取的引用。以下为原生通过env获得引用的种类
局部引用
局部引用的使用期限仅限于这个原生方法内(局部变量不会传到调用的函数里,函数调用栈的结构是一次函数调用产生一部分局部变量,但算在外层函数的执行期间,所以仍存在)不可缓存重用,返回后将释放。如FindClass.
手动释放:env->DeleteLocalRef(引用对象);
全局引用
全局引用在原生后续调用过程中依然有效,可以被其它原生函数与原生线程使用,只能手动释放
用env->NewGlobalRef(引用)将局部引用升级为全局引用,返回的是同类型的引用
但记得局部的也要及时释放
env->DeleteGlobalRef(引用)释放全局引用
弱全局引用
弱引用不阻止潜在的java对象被回收
env->NewWeakGlobalRef(引用)将局部引用升级为弱引用
JNI_FALSE a=env->IsSameObject(引用,NULL)用来检验弱引用是否存在
env->DeleteWeakGlobalRef(引用)释放弱引用

线程

局部引用不能被多线程共享。只有全局可以。
JNIEnv只能在方法的线程中使用,不可以被别的线程缓存使用(参数也是存于栈中的,不同线程各自维护一个栈,所以不可使用)
jni利用java对象同步

1
2
3
4
5
env->MonitorEnter(javaobject);
//获取对象锁
env->MonitorExit(javaobject);
//释放对象锁
可以和java的synchronized(obj)同步

原生线程开启与java沟通

1
2
3
4
5
6
7
8
JavaVM *c;
jint JNI_OnLoad (JavaVM* vm, void* reserved)
//这个函数在共享库加载时虚拟机自动调用,用来保存JavaVM;
JNIEvn *env;
c->AttaachCurrentThread(&env,NULL);
//获取java虚拟机的env
c->DetachCurrentThred();
//分离