本文共 12530 字,大约阅读时间需要 41 分钟。
在Android中,设计的开发语言包括汇编、C、C++、java、Parcel、Bash、XML、IDL、Flash等。在原生的C/C++代码层,也涉及多线程的处理。
1.C语言与汇编语言的相互调用
C语言与汇编语言的相互调用,在应用层开发中并不常用,但在驱动开发层进程用到,另外,在一些对性能特别敏感的场景中,也时有引用。
(1)C语言对汇编语言的调用
在C语言中调用汇编语言之需要关键字__asm__将汇编代码封装即可。下面是Android中speex编解码的一个示例:
static inline spx_word32_t MAC16_32_Q15(spx_word_t c, spx_word16_t a, spx_word32_t b){
spx_word32_t res;
__asm__
(
"A1=%2.L*%1.L (M); \n\t"
"A1=A1 >>> 15; \n\t"
"%0=(A1 +=%2.L*%1.H); \n\t“
”%0=%0+%d; \n\t"
:"=&W" (res), "=&d" (b)
:"d" (a), "1" (b), "d" (c)
:"A1"
);
return res;
}
汇编代码在C语言中的实现形式因编译环境不同而有所不同,在Android中采用的是GCC编译器。
(2)汇编语言对C语言的调用汇编语言对C语言的调用需要通过关键字Bl进行,如果涉及参数传递,则实现比较复杂,下面是一个简单的示例:
对于C函数的实现如下:
int sum(int a, int b, int c, int d, int e)
{
return a+b+c+d+e;
}
调用sum()函数的汇编代码的实现如下:
EXPORT f
AREA f, CODE, READONLY
IMPORT sum :使用伪操作数IMPORT声明C程序sum()
STR lr, [sp, #-4]! :保存返回地址
ADD r1, r0, r0 :假设进入程序f时,r0中的值为i,r1的值设为2*i
ADD r2, r1, r0 :r2的值设为3*i
ADD r3, r1, r2 :r3的值设为5*i
STR r3, [sp, #-4]! :第五个参数5*i通过数据栈传递
ADD r3, r1, r1 :r4值设为4*i
BL sum :调用C程序sum()
ADD sp, sp, #4 :调整数据栈指针,准备返回
LDR pc, [sp], #4 :返回
END
对于C函数中的参数,在汇编语言中通过寄存器r0~r3传递前4个参数,更多的参数则需要通过数据栈进行传递。
2.C++与C语言的相互调用
在C++与C语言的相互调用中,注意关键字__cplusplus和extern “C”的用法即可,其中__cplusplus为C++的关键字,表示作用域内的代码为C++代码,而extern "C" 是为了保证C++和C是互通的。这是因为在编译生成汇编码时,两者的处理有所不同,读过《深度探索C++对象模型》的开发者都知道,在C++中,为了支持重载机制,在编译生成汇编码时,要对函数的名称进行一些处理,而在C语言中,则不需要如此。
(1)C++对C语言的调用
#ifdef __cplusplus
extern "C"
{
#endif
#define NUM_AMRSID_RXMODE_BITS 3
#define AMRSID_RXMODE_BIT_OFFSET 36
#define AMRSID_RXTYPE_BIT_OFFSET 35
extern const Word16 WmfDecBytesPerFrame[];
extern const Word16 If2DecBytesPerFrame[];
Word16 AMRDecode(void* state_data, enum Frame_Type_3GPP frame_type, UWord8 *speech_bits_ptr, Word16* raw_pcm_buffer, bitstream_format input_format);
#infdef __cplusplus
}
#endif
(2)C语言对C++的调用
在C语言调用C++时,由于C语言不能直接引用声明了extern “C”的C++头文件,因此在调用C++头文件中用extern “C”声明的函数时,需声明其为外部函数。C++头文件test.h的实现如下:
#ifndef __TEST_H__
#define __TEST_H__
extern "C" int sum(int x, int y);
#endif // __TEST_H__
CPP文件的实现如下:
#test.h
int sum(int x, int y){
return x+y;
}
调用C++的C文件的实现如下:
extern int sum(int x, int y);
int main(int argc, char* argv[]){
sum(2, 3);
return 0;
}
3.Java对C/C++的调用
在Java 1.1中,引入了JUI(Java Native Interface)技术。JNI支持Java和其他语言交互,即JNI支持Java对C/C++的调用,也支持C/C++调用Java。JNI的实现主要位于NDK中,目前NDK的最高版本为Android NDK r6b。
Java对本地语言的调用会导致程序可移植性的削弱,但在与驱动、操作系统进行交互以及遗留系统移植等场景中,这种方式却是十分必要的,毕竟原生代码在运行速度和I/O处理上更有效率,更重要的是原生代码还支持OpenGL ES。在Android中,JNI的应用也十分广泛,除以上场景外,考虑到Java的反编译技术的发展,涉及商业机密的设计和敏感数据的安全都需要在原生代码中维护。目前JNI的最新版本为JNI 1.6。
下面介绍如何加载共享库、定义JNI原生接口、定义数据类型、转换数据类型、获取环境变量路径、编译原生代码。
(1)加载共享库
在Android中,要与原生代码交互,通常需将原生代码编译成共享库的形式供Java调用,调用的方法如下:
public final class AmrImputStream extends InputStream
{
static{
System.loadLibrary("hello-jni"); //共享库通常位于Android应用的lib/armeabi中
}
}
除了原生共享库外,java.lang.System还支持对系统相关信息和资源的接入和获取,如系统时间、系统属性、环境变量等,方法如下:
System.currentTimeMillis() //系统时间
String oldUserHome=System.getProperty("user.home"); //获取系统属性
String symbols=System.getenv("ANDROID_SYMBOLS"); //环境变量
设置系统属性的方法如下:
Properties tProps=new Properties();
tProps.put("testIni", "99");
System.setProperties(tProps);
java.lang.System在Android中是非常重要的一个类,其在一些涉及底层通信的应用开发中使用很广泛。
(2)定义JNI原生接口
为了定义JNI原生接口,需要在原生代码中包含jni.h文件。在jni.h文件中定义了JNI数据类型和原生接口。需要注意的是JNI原生接口的接口名定义与通常的方法名、函数名定义不同。事实上,根据管用的场景不同,JNI接口的定义方式有两种,分别为普通调用和定向调用。其中普通调用定义的JNI接口可以在多个Java类中被调用,定向调用定义的JNI接口仅能在特定的Java类中被调用。在Android中,普通调用的JNI定义通常用于框架层,而定向调用的JNI定义通常用于应用层。
1)普通调用
在框架层,Android对JNI接口的定义和通常的方法定义类似,但需要向Dalvik虚拟机注册原生方法,以便在共享库加载过程中加载JNI接口。普通调用的JNI接口定义的步骤是:JNI接口定义---JNI接口映射---注册JNI接口映射---设置共享库加载时JNI接口映射。
下面介绍JNI接口映射的内容。JNI接口映射的结构体定义如下:
typedef struct{
const char* name; //Java中声明的JNI接口
const char* signature; //接口参数和返回值
void* fnPtr; JNI接口
}JNINativeMethod;
JNI接口映射的实现如下:
static JNINativeMethod nativeMethods[]={
{"_initialize", "(ILjava/lang/Object;)V", (void*)android_drm_DrmManagerClient_initialize},
{"_finalize", "(I)V", (void*)android_drm_DrmManagerClient_finalize},
}
注册JNI接口映射的过程如下:
static int registerNativeMethods(JNIEnv* env){
int result = -1;
jclass clazz=env->FindClass("android/drm/DrmManagerClient");
if(NULL != clazz){
if(env->RegisterNatives(clazz, nativeMethods, sizeof(nativeMethods)/sizeof(nativeMethod[0]))==JNI_OK){
result=0;
}
}
return result;
}
在共享库加载过程中,通过JNI_OnLoad()执行JNI接口映射的注册,过程如下:
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env=NULL;
jint result=-1;
if(vm->GetEnv((void**)&env, JNI_VERSION_1_4)==JNI_OK){
if(NULL!=env && registerNativeMethods(env)==0){ //判断JNI版本
result=JNI_VERSION_1_4; //执行接口映射注册
}
}
return result;
}
另外,如果希望在共享库卸载时做些处理,那么可以在JNI_OnLoad()中进行。通常在卸载时会做些清除痕迹的工作。
2)定向调用
在定向调用中,接口名的定义由3部分组成:Java声明、Java包名、函数名。原生方法stringFromJNI对应的JNI原生接口如下:
jstring Java_con_example_hellojni_HelloJni_stringFromJNI(JNIEnv * env, jobject thiz)
{ ...}
需要注意的是,上述代码中“Java”的“J”为大写,接口名中的字符串以“_”连接。在加载共享库时,如果无法找到对应原生方法的JNI接口,会抛出UnsatisfiedLinkError异常。
为了调用JNI接口,需要在Java中声明JNI接口,其方法如下:
public native String stringFromJNI(...); //通过native关键字表示JNI接口
另外,JNI还提供了JNIEXPORT、JNICALL、JNIIMPORT等关键字,这些关键字在Windows操作系统下是必须的,在Linux操作系统中是可选的。
3)数据类型定义
在Java中,数据的编码为Unicode,而在JNI中,数据的编码为UTF-8,而且不支持全部的UTF-8字符集。JNI中的数据类型和Java、C/C++中的数据类型均有不同。请参照下图:
其他JNI数据类型包括jclass、jobject、jmethodID、jfieldID、JNIEnv、JavaVM等,其中jclass表示句柄,jobject表示对象句柄,jmethodID表示方法句柄,jfieldID表示变量句柄,JNIEnv表示JNI环境变量,JavaVM表示Java虚拟机的全局环境。
4)数据类型转换
下面简单介绍字符串、数组和指针间的数据类型转换。
jstring向const char*的转换的方法如下:
jboolean iscopy;
jstring file_name=(*env)->NewStringUTF(env, "test.temp"); //创建jstring类型数据
const char* c_file_name=(*env)->GetStringUTFChars(env, file_name, &iscopy);
env->ReleaseStringUTFChars(file_name, c_file_name); //创建c_file_name后需要释放
jbyteArray数组的创建方法如下:
jbyteArray dataArray=env->NewByteArray(strlen(value));
env->SetByteArrayRegin(dataArray, 0, strlen(value), (jbyte*)value); //数组初始化
数组向指针的转化方法,jbyteArray向jbyte*转化如下:
jbyte* rawSavedState = NULL;
jsize rawSavedSize = 0;
if(savedState != NULL){
rawSavedState=evn->GetByteArrayElements(savedState, NULL);
rawSavedSize=env->GetArrayLength(savedState);
}
if(rawSavedState!=NULL){
env->ReleaseByteArrayElements(savedState, rawSavedState, 0);
}
5)获取环境变量路径
在C/C++中,要得到特定环境变量所指定的路径,方法非常简单,利用getenv()函数即可,实例如下:
const char* root=getenv("ANDROID_ROOT");
6)原生代码编译
JNI的编译非常简单,只需要设置LOCAL_PATH、LOCAL_MODULE、LOCAL_SRC_FILES等环境变量,然后根据需要指明是编译成共享库还是静态库即可。下面是编译原生代码的一般android.mk实现:
LOCAL_PATH :=$(call my-dir) //设置LOCAL_PATH为当前目录
include $(CLEAR_VARS) //清空环境变量
LOCAL_MODULE := hello-jni //指明模块名
LOCAL_SRC_FILES := hello-jni.c //指明源文件
include $(BUILD_SHARED_LIBRARY) //指明编译为共享库,静态库变量为BUILD_STATIC_LIBRARY
如果编译成共享库,那么上面的编译脚本生成的共享库为libhello-jni.so,主要在加载共享库时指定的是模块而非共享库名。
4.C/C++对Java的调用
在Java和原生代码的交互中,Android中的原生代码通常扮演基础框架的角色,对于UI层的实现依然以Java为主,部分场景可能涉及反向调用,这类场景包括UI层代码的调用、公开实现的Java算法的调用、数据管理等。JNI调用Java方法的原生接口在结构体JNINativeInterface中定义。JNI调用Java方法的过程如下:FindClass(查询Java类)---GetMethodID(获取Java方法句柄)---Call*Method(调用Java方法)。
JNI对Java的调用的支持非常强大,除此之外,JNI还支持对Java变量、私有类的操作。JNI操作Java变量的一般过程是:FindClass(查询Java类)---GetFieldID(获取Java变量句柄)---GetFieldID/SetFieldID(操作Java变量)。
对于BackupDataInput的私有类EntityHeader,查询方法如下:
jclass clazz=env->FindClass("android/app/backup/backupDataInput$EntityHeader");
(1)原生接口定义
根据Java方法的不同实现,在JNI中有不同的原生接口与其对应,同时针对交互的特殊需要,JNI还定义了一些特殊的原生接口。
常用Java方法及其JNI原生接口的对应关系如下图所示:
另外,还有其他的重要JNI接口,具体如下:
FindClass:用于查找相应的Java类,注意,其传递的类名参数为const char*类型,可能抛出的异常包括ClassFormatError、ClassCircularityError、NoClassDefFoundError和OutOfMemoryError等。
GetMethodID:用于查找Java类或接口中的非静态方法。其方法名和方法的参数均为const char*类型,可能抛出的异常包括NoSuchMethodError、ExceptionInInitializerError、OutOfMemoryError等。调用GetMethodID将导致未初始化的类被初始化。
GetFieldID:用于查找Java类中的非静态变量。可能抛出的异常包括NoSuchFieldError、ExceptionInInitalizerError、OutOfMemoryError等。
GetStaticMethodID:用于查找Java类中的静态方法。
GetStaticFieldID:用于查找Java类中的静态变量。
其他常用的JNI接口包括GetObjectClass、GetSuperclass、IsInstanceOf、IsAssignableFrom、IsSameObject等,另外JNI还提供了对Java反射机制的支持。
(2)接口参数和返回值
在JNI中,接口参数和返回值的声明有自己的规则,其格式为“(参数类型1:参数类型2)返回值类型“。
对于基本的数据类型,JNI提供了相应的缩写,如void的缩写为V、数组的缩写为[、char的缩写为C、short的缩写为S、int的缩写为I、long的缩写为J、float的缩写为F、byte的缩写为B、boolean的缩写为Z、double的缩写为D、合格类的缩写为L(在复杂数据类型前需加L表明是类)等。对于非基本的数据类型,则不提供缩写。
对于void ContextValue::put(String key, byte[] value)方法,其对应的JNI调用方法如下:
putId=env->GetMethodID(localRef, "put", "(Ljava/lang/String; [B]V");
对于private BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address, int port)方法,其对应的JNI调用方法如下:
BluetoothSocket_ctor=env->GetMethodID(clazz, "<init>", "(IIZZLJava/lang/String;I)V");
对于public BackgroundSuiface(Context context)方法,其对应的JNI调用方法如下:
ctor=env->GetMethodID(backgroundClass, "<init>", "(Landroid/Context;)V");
(3)Log日志的生成
在JNI中,支持Android全部等级的日志输出,为了书写方便,一般会定义为宏定义,方法如下:
#define DEBUG(args...) __android_log_print(ANDROID_LOG_DEBUG, "Hello", args)
为了使用Android日志,需要包含android\log.h文件。同时在Android.mk文件中设置LOCAL_LDLIBS:=-llog。另外还需注意,在默认情况下,编译出来的共享库并非调试版本,基于ANDROID_LOG_DEBUG等级的日志无法显示,故通常使用ANDROID_LOG_INFO等级。
(4)应用开发中的调用示例
在进行JNI原生接口的调用中,Java对象的获取有两种方式:第一,可以在Java代码创建Java对象,然后传递给JNI原生接口;第二,可以在JNI原生接口中直接创建Java对象。下面进行简单介绍。
1)传递Java对象
对于要传递的Java对象,其参数类型为jobject。下面是Android中的一个例子,其实现了传递一个名为headerObj的Java对象的功能。
static int readHeader_native(JNIEnv* env, jobject clazz, jobject headerObj, jobject fdObj){
env->SetIntField(headerObj, s_chunkSizeField, flattenedHeader,dataSize);
env->SetObjectField(headerObj, s_keyPrefixField, env->NewStringUTF(keyPrefix.string()));
return 0;
}
2)直接创建Java对象
如果希望将对Java对象的调用集中在原生代码中,就会涉及在原生代码中创建Java对象。为了创建Java对象,先要查找到相应Java类的句柄,然后通过GetMethodID接口查找到该类的构造函数,之后即可通过NewObject接口创建Java对象。下面是一个具体的示例:
static jobject createJavaMapFromHTTPHeaders(JNIEnv* env, const WebCore::HTTPHeaderMap& map){
jclass mapClass=env->FindClass("java/util/HashMap"); //查找Java类
jMethodID init =env->GetMethodID(mapClass, "<init>", "(I)V"); //查找构造函数,注意方法名为<init>
jobject hashMap=env->NewObject(mapClass, init, map.size()); //创建Java对象
...
env->DeleteLocalRef(mapClass); //释放本地引用
return hashMap;
}
注意在操作结束后,应释放Java类的引用。
3)调用Java方法
Java方法的调用非常简单,主要是通过Call*Method接口来完成的,示例如下:
static jobject android_drm_DrmManagerClient_getConstraintsFromContext(JNIEnv* env, jobject thiz, jint uniqueId, Jstring jpathm, jint usage){
jcalss localRef=env->FindClass("android/content/ContentValues"); //查找Java类
jobject constraints = NULL;
...
jmethodID constructorId=env->GerMethodID(localRef, "<init>", "()V"); //获取构造函数
constraints=env->NewObject(localRef, constructorId); //构造Java对象
jbyteArray dataArray=env->NewByteArray(strlen(value));
env->SetByteArrayRegin(dataArray, 0, strlen(value), (jbyte*)value);
//调用java方法
env->CallVoidMethod(condtraints, env->GetMethodID(localRef, "put", "(Ljava/lang/String;[B]V)"), env->NewStringUTF(key.string()), dataArray);
...
}
在调用Java方法时,需要注意,如果返回值不是基本数据类型,则使用CallObjectMethod()接口调用Java方法。
4)操作Java变量
为了设置Java变量,先要获取Java变量的句柄,然后根据Java变量的类型选择不同的JNI接口进行设置。
static sp<DrmManagerClientImpl> setDrmManagerClientImpl(JNIEnv* env, jobject thiz, const sp<DrmManagerClientImpl>& client){
jclass clazz=env->FindClass("android/drm/DrmManagerClient"); //查找Java类
jfieldID fieldId=env->GetFieldID("android/drm/DrmManagerClient"); //查找Java类
jfieldID fieldId=env->GetFieldID(clazz, "mNativeContext", "I"); //查找Java变量
sp<DrmManagerClientImpl>lod=(DrmManagerClientImpl*)env->GetIntField(thiz, fieldId);
if(client.get()){
client->incString(thiz);
}
if(old!=0){
old->decString(thiz);
}
//设置Java变量
env->SetIntField(thiz, fieldId, (int)client.get());
return old;
}
JNI针对不同的Java变量类型提供了不同的JNI接口,设置Java变量的主要的JNI接口包括SetObjectField、SetBooleanField、SetByteField、SetCharField、SetShortField、SetIntField、SetLongField、SetFloatField、SetDoubleField等。获取Java变量的主要接口包括GetObjectField、GetBooleamField、GetByteField、GetCharField、GetShortField、GetIntField、GetLongField、GetFloatField、GetDoubleField等。
从JNI对Java方法和Java变量的操作可以很容易看出,语言的单根设计的好处,除了体现在基本数据类型方面外,其他数据类型也均可通过对根类的操作来完成。
5)抛出异常
在JNI中,异常的抛出有两种情况,一种是C++实现中抛出C++异常,另一种是在C/C++中抛出Java异常。
抛出Java异常主要是通过jniThrowException接口来是实现的。下面是jniThrowException()接口的定义:
int jniThrowException(JNIEnv* env, const char* className, const char* msg)
下面是通过jniThrowException接口抛出异常的一个示例:
jniThrowException(env, "java/lang/RuntimeException", "Can't find android/graphics/Metrix");
与异常相关的其他JNI接口包括Throw、ThrowNew、ExceptionOccurred、ExceptionDescribe、ExceptionClear、FatalError、ExceptionCheck等,其中ExceptionCheck用于检测是否已经有异常抛出;ExceptionOccurred用于获取一个正在抛出异常的本地引用,JNI或者Java必须对其进行处理;ExceptionDescribe用于打印出异常的错误描述;ExceptionClear用于清除刚刚抛出的异常;FatalError用于抛出一个可导致JVM关闭的异常。下面是这些异常类应用的一个示例:
env->CallVoidMethod(fJavaInputStream, gInputStream_resetMethodID);
if(env->ExceptionCheck()){
env->ExceptionDescribe();
env->ExceptionClear();
return false;
}
(5)C与C++的JNI区别
由于C和C++在可执行文件布局上的不同,JNI接口的调用方式在JNIEnv指针的操作上也略有不同。下面通过NewStringUTF接口来介绍。如下是C语言的调用方式:
(*env)->NewStringUTF(env, "Hello from JNI !");
如下是C++的调用方式:
env->NewStringUTF("Hello from JNI !");