Android源码学习之如何使用eclipse+NDK【三】

来源:互联网 发布:自制51单片机最小系统 编辑:程序博客网 时间:2024/06/06 19:35







Android NDK开发技巧二  

1. 在JNI中打印Logcat,首先我们需要在cpp文件中加入 #include <android/log.h> 这个头文件,NDK有关android自己的就给我们这个唯一的文件log.h,其他的需要我们自己hack diy来解决。

jstring jlog;  //从Java传来需要打印的字符
jboolean isCopy;
const char * szLog = (*env)->GetStringUTFChars(env, jlog, &isCopy); //将java的unicode字符转化为utf8字符
 
__android_log_print(ANDROID_LOG_WARN, “android123-cwj”, "from ndk = %s", szLog); //打印logcat
 
(*env)->ReleaseStringUTFChars(env, jlog, szLog); // 释放内存
}

上面这段比较简单,其中使用__android_log_print函数打印Logcat,第一个参数为log的level,在log.h头文件中定义了
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

等类型,第二个参数为tag标签,第三个为需要打印的字符。整个例子比较简单,但方便了很多调试。

2. Android NDK给我们提供了zlib库的支持,可以通过本地的方法解压缩zip文件。

3. 有关C语言运行库的一些方法,在string.h文件中描述的比较清楚,可以方便的操作字符串 ,比如

extern void*  memccpy(void *, const void *, int, size_t);
extern void*  memchr(const void *, int, size_t);
extern void*  memrchr(const void *, int, size_t);
extern intmemcmp(const void *, const void *, size_t);
extern void*  memcpy(void *, const void *, size_t);
extern void*  memmove(void *, const void *, size_t);
extern void*  memset(void *, int, size_t);
extern void*  memmem(const void *, size_t, const void *, size_t);
extern void   memswap(void *, void *, size_t);

extern char*  strchr(const char *, int);
extern char*  strrchr(const char *, int);

extern size_t strlen(const char *);
extern intstrcmp(const char *, const char *);
extern char*  strcpy(char *, const char *);
extern char*  strcat(char *, const char *);

extern intstrcasecmp(const char *, const char *);
extern intstrncasecmp(const char *, const char *, size_t);
extern char*  strdup(const char *);

extern char*  strstr(const char *, const char *);
extern char*  strcasestr(const char *haystack, const char *needle);
extern char*  strtok(char *, const char *);
extern char*  strtok_r(char *, const char *, char**);

extern char*  strerror(int);
extern intstrerror_r(int errnum, char *buf, size_t n);

extern size_t strnlen(const char *, size_t);
extern char*  strncat(char *, const char *, size_t);
extern char*  strndup(const char *, size_t);
extern intstrncmp(const char *, const char *, size_t);
extern char*  strncpy(char *, const char *, size_t);

相信这些肯定比Java效率快上不少,至少有指针用,在处理字符串等方面效率可能是几百倍几千倍的提升。

4. NDK在I/O处理上会更有效率,比如提供了Socket和File的本地读写,在socket.h文件中包含了标准Socket的各种方法,可以处理TCP和UDP报文,这样和C++服务器的互通,通过NDK解决,不用再为Java的类型字节对齐以及编码而烦恼。

__socketcall int socket(int, int, int);
__socketcall int bind(int, const struct sockaddr *, int);
__socketcall int connect(int, const struct sockaddr *, socklen_t);
__socketcall int listen(int, int);
__socketcall int accept(int, struct sockaddr *, socklen_t *);
__socketcall int getsockname(int, struct sockaddr *, socklen_t *);
__socketcall int getpeername(int, struct sockaddr *, socklen_t *);
__socketcall int socketpair(int, int, int, int *);
__socketcall int shutdown(int, int);
__socketcall int setsockopt(int, int, int, const void *, socklen_t);
__socketcall int getsockopt(int, int, int, void *, socklen_t *);
__socketcall int sendmsg(int, const struct msghdr *, unsigned int);
__socketcall int recvmsg(int, struct msghdr *, unsigned int);

extern  ssize_t  send(int, const void *, size_t, unsigned int);
extern  ssize_t  recv(int, void *, size_t, unsigned int);

__socketcall ssize_t sendto(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
__socketcall ssize_t recvfrom(int, void *, size_t, unsigned int, const struct sockaddr *, socklen_t *);

5. 当然了,对于我们开发最爽的还要属OpenGL ES了,在NDK中所有GL的函数,可以在gl.h和glext.h中查找到,最新版本NDK支持最新的OpenGL ES版本,可以方便移植iPhone上的3D游戏了。Android123已经成功将Cube例子用NDK改造运行,确实比Java来的更方便和亲切。

最后还是一句话,对于Java这种解释型语言不爽的Android开发者NDK才是你最明智的选择,如果你有C/C++的基础,可以解决很多开源项目的移植工作,实现高级的功能,将Java程序员甩在脑后面,开发出色的UI,完全可以拍拖View的舒服,让OpenGL来写常规应用相信更有竞争力。









【Android】JNI开发高级篇  


有关Android JNI开发中比较强大和有用的功能就是从JNI层创建、构造Java的类或执行Java层的方法获取属性等操作。
  一、类的相关操作
  1. jclass FindClass(JNIEnv *env, const char *name);  查找类
  该函数可能做过Java开发的不会陌生,这个是JNI层的实现,需要注意的是第二个参数为const char*类型的,我们如果从Java从层传入unicode编码的jstring类型需要使用GetStringUTFChars函数转换成utf8的const char*,如果成功返回这个Java类的对象jclass,相关的异常可能有
  (1. ClassFormatError 类的数据格式无效
  (2. ClassCircularityError 该类或接口是自身的超类或超接口
  (3. NoClassDefFoundError 没有找到指定名称的类或接口
  (4. OOM内存不足错误,即OutOfMemoryError
  2. jclass GetSuperclass(JNIEnv *env, jclass clazz);  获取父类或者说超类
  该函数的第二个参数为jclass类,我们调用时传入的是子类,否则返回将是NULL
  3. jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1,jclass clazz2);  判断class1对象能否安全的强制转换为class2对象
  如果可以将返回 JNI_TRUE,JNI_TRUE的定义值为1,否则返回JNI_FALSE即0 ,这里Android123详细说明下哪些情况可能返回真:
  (1  这两个类参数引用同一个 Java 类
  (2  第一个类是第二个类的子类
  (3  第二个类是第一个类的某个接口
  4.  jclass GetObjectClass(JNIEnv *env, jobject obj); 通过对象获取这个类
  该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL。
  5.  jboolean IsInstanceOf(JNIEnv *env, jobject obj,jclass clazz); 判断对象是否为某个类的实例
  这个函数是JNI层的实现,相信大家都不陌生,Android开发网提醒大家需要注意的是返回值可能产生异议,就是如果传入的第二个参数为NULL对象,NULL对象可以强制转换为各种类,所以这种情况也将会返回JNI_TRUE,所以一定判断传入的对象是否为空。
  6. jboolean IsSameObject(JNIEnv *env, jobject ref1,jobject ref2);  判断两个对象是否引用同一个类
  需要注意的是如果两个对象均为空,返回的值也会是JNI_TRUE所以使用时判断对象为空。
  二、调用Java方法
  首先说下有关签名sig相关的比如 "Ljava/lang/String;"
  1. jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);  获取一个Java方法的ID
  这个函数将返回非静态类或接口实例方法的方法 ID。这个方法可以是某个clazz 的超类中定义,也可从clazz 继承,最后一个参数为签名,最后两个参数是const char*类型,是utf8类型。需要注意的是Android123提醒大家执行GetMethodID()函数将导致未初始化的类初始化,如果要获得构造函数的方法ID,使用 <init> 作为方法名,同时将 void (V) 作为返回类型,如果找不到指定的ID将返回NULL,同时异常可能有:
  (1  NoSuchMethodError 找不到指定的Java方法。
  (2  ExceptionInInitializerError 如果由于异常而导致类初始化程序失败
  (3  OutOfMemoryError 内存不足
  2 . NativeType CallXXXMethod (JNIEnv *env, jobject obj,jmethodID methodID, va_list args); 调用XXX类型的Java方法
  执行Java类中某个方法,需要注意的是这个里的java类是非静态的,由于Java的方法的类型比较多,所以该函数可能有以下几种形式,如CallObjectMethod,CallBooleanMethod,CallByteMethod,CallCharMethod,CallShortMethod,CallIntMethod,CallLongMethod,CallFloatMethod,CallDoubleMethod和CallVoidMethod,需要注意的是,该函数的第三个参数为通过GetMethodID函数获取的方法ID,最后一个参数为这个方法的参数表,最后的va_list宏可以通过搜索获取具体的使用方法,这里Android开发网不再赘述。
  3.NativeType CallNonvirtualXXXMethod (JNIEnv *env, jobject obj,jclass clazz, jmethodID methodID, jvalue *args);
  CallNonvirtualXXXMethod函数和上面的CallXXXMethod 不同之处是多了一个jclass参数,CallXXXMethod是根据对象来调用方法,而CallNonvirtualXXXMethod是根据类的实例调用,区别在这点。
  上面的三个均为非静态类的获取,执行调用,需要实例化这个类才可以执行,下面的为静态调用。
  4.  jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
  5.  NativeType CallStaticXXXMethod(JNIEnv *env, jclass clazz,jmethodID methodID, ...);
  三、访问Java对象的域
  Java对象的域或者说字段、属性(Field) 类似方法的执行
  1. jfieldID GetFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig);  获取实例对象的域ID
  需要注意的是,非静态的实例化后的对象,可能产生的异常有
  (1 NoSuchFieldError  找不到指定的域
  (2 ExceptionInInitializerError 因为异常而导致类初始化失败
  (3 OutOfMemoryError内存不足。
  2. NativeType GetXXXField(JNIEnv *env, jobject obj,jfieldID fieldID);
  类似GetXXXMethod函数,可能有的类型有 GetObjectField,GetBooleanField,GetByteField,GetCharField,GetShortField,GetIntField,GetLongField,GetFloatField,GetDoubleField。
  3. void SetXXXField(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value);
  Java的域可以赋值的,可能有的类型有 SetObjectField,SetBooleanField,SetByteField,SetCharField,SetShortField,SetIntField,SetLongField,SetFloatField,SetDoubleField。
  上面3种情况均为非静态对象的域,对于不需要实例化对象的域,可以直接使用下面的。
  4. jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
  5. NativeType GetStaticXXXField(JNIEnv *env, jclass clazz,jfieldID fieldID);
  6. void SetStaticXXXField(JNIEnv *env, jclass clazz,jfieldID fieldID, NativeType value);
  四、实例代码,Android123给网友准备了一个例子,帮助大家实战Android JNI开发,大家可以移植到Android NDK环境中执行。
  最后有关Android JNI最后的终极内容,Android开发网主要说明下JVM和JNI的全局引用相关内容,比如本地全局引用LocalGlobalRef,弱全局引用WeakGlobalRef,JNI中线程处理的高级方法比如AttachCurrentThread,以及JNI中的NIO的相关特性将在明天继续讲解,更多的有关Android平台NDK开发内容可以查看我们 Android NDK开发技巧系列文章

===================================================

Google Android JNI 使用方法

在做Android底层开发的时候,偶尔也会浏览上层应用的代码,但对于很多向我一样连java编程语言都不懂的Linux驱动开发者来说实在很是头痛.于是厚着脸皮看了Google SDK文档的小部分,对android JNI的使用有了一点初步的了解,当然这些理解都是我一个java外行来看java的JNI使用方法,请java行家不吝赐教,大致总结如下:
 1.如果要在本地CPP代码中访问java代码中的一个对象(某个class的实例)的某个域的值,需要经历以下三个步骤:
  步骤1)用FindClass()函数找到该java类(如android.os.Binder)的实例对象的引用:
  jclass clazz = env->FindClass(kBinderPathName) = env->FindClass("android.os.Binder")
  步骤2)用GetFieldID()函数获取到要访问的域(field: 实际上就是该java class中的某个成员变量的名字)的ID:
  gBinderOffsets.mObject = env->GetFieldID(clazz, "mObject", "I") // mObject为java class "Binder"里的一个成员变量
  -> 注意,这里将要访问的那个java对象的成员mObject的ID保存到了全局变量gBinderOffsets.mObject中,这样做的前提和优点如下:
  前提: android里面,每个java进程中只允许有一个java虚拟机(sun公司原始的java架构中,一个进程中可以有多个java虚拟机)
  优点: 除了第一次,以后每次要访问该java对象的成员mObject就非常快了(不用再去FindClass()和GetFieldID())
  步骤2)用GetMethodID()函数获取到要访问的方法(Method: 实际上就是该java class中的某个成员函数的名字)的ID:
  gBinderOffsets.mExecTransact = env->GetMethodID(clazz, "execTransact", "(IIII)Z") // execTransact为java class "Binder"里的一个成员函数
  -> 注意,这里将要访问的那个java对象的成员execTransact的ID保存到了全局变量gBinderOffsets.mExecTransact,这样做的前提和优点如下:
  前提: android里面,每个java进程中只允许有一个java虚拟机(sun公司原始的java架构中,一个进程中可以有多个java虚拟机)
  优点: 除了第一次,以后每次要访问该java对象的成员mExecTransact就非常快了(不用再去FindClass()和GetMethodID())
  步骤3)用类似于GetIntField()的函数获取到该java对象的那个域(即成员)的值:
  IBinder* target = (IBinder*)env->GetIntField(obj, 
  
gBinderProxyOffsets.mObject) // 获取java android.os.Binder类型对象里面的成员mObject的值 
  步骤3)用类似于CallBooleanMethod()的函数调用到该java对象的那个成员函数:
  jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, code, (int32_t)&data, (int32_t)reply, flags)
 2.android java调用CPP函数: 原理 => 相当于java的那个class里面有的函数使用CPP代码来实现了
  1)通过结构JNINativeMethod描述java代码调用函数和CPP函数的对应关系:
  typedef struct {
  const char* name; // java代码调用CPP函数的入口
  const char* signature; // CPP函数的返回值
  void* fnPtr; // CPP的函数名
  } JNINativeMethod;
  => 例如: java代码通过IBinder.transact()来调用CPP的函数android_os_BinderProxy_transact()
  {"transact", "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z", (void*)android_os_BinderProxy_transact},
  2)将CPP函数注册到java的某个class中: 使用函数AndroidRuntime::registerNativeMethods()来注册
  => 这之后,java代码就可以调用CPP函数了
  3)java代码调用CPP函数方法:
  IBinder.transact()
 3.andorid CPP调用java函数: 原理 => 相当于CPP代码找到java的那个class里面的函数的入口地址,然后在CPP代码中调用java代码
  1)通过结构JNINativeInterface描述CPP代码调用java函数的对应关系:
  CallStaticVoidMethod
  2)到java的那个class(如android.os.Binder)中找到java函数(如execTransact())的入口:
  jclass clazz = env->FindClass(kBinderPathName) // const char* const kBinderPathName = "android/os/Binder";
  gBinderOffsets.mExecTransact = env->GetMethodID(clazz, "execTransact", "(IIII)Z")
  3)在CPP代码中调用java函数:
  env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, code, (int32_t)&data, (int32_t)reply, flags)









——————————————————————————再来一遍吧

用eclipse编译JAVA和C项目  

ubuntu 11.10
eclipse 3.7(indego) for java
jdk 6
android sdk 2.2
andrid ndk r7

 

当然,在windows环境下通过cygwin等工具也是可以实现gdb调试的,我也确实实现过。但是性能实在太低,卡的根本没法用。Linux下直接用gdb调试本地方法是很流畅的。

再确定安装并配置好开发环境之后,就可以开始了。

首先得确定自己能够正常的运行一个ndk工程,连运行都成问题的话,也就谈不上什么调试了。

 

新建一个android项目,选择crete project form existing source,源代码位于

/home/shaodx/android/android-ndk-r7/samples/hello-jni,即ndk目录的samples/hello-jni。

 

Next>

Sdk target 就选2.2吧 ,其他的虽然没测试,应该也没什么问题。

 

Finsh

 

Hellojni项目极其简单,看代码就懂了。在activitie下建立一个textview,然后调用一个natvie方法来返回一个字符串,然后把textview的text设置为这个字符串。

为了方便看到调试后的效果和局部变量的变化情况,我加了几行对程序结果没影响的代码。

Java代码:

C++:

当然,这个时候直接运行的话,程序肯定会崩溃的。因为动态链接库还没编译好。Ndk根目录下的ndk-build负责编译so文件。首先进入项目所在目录,然后运行ndk-build即可。

效果如下:

出错了…..

Host 'awk' tool is outdated.

上网搜索之后的解决方案如下:

到/home/shaodx/android/android-ndk-r7/prebuilt/linux-x86/bin/目录下找到这个awk,file一下:

这个awk文件居然是64位版的,难怪之前说过期了。(google程序员粗心了吧)

解决方案居然是把这个awk删了就行了……………..

解决问题之后,换个姿势,再来一次

 

So文件存在于libs/armeabi/libhello-jni.so。

现在就可以运行程序了。

不过要是每一次修改c++代码都还要调用ndk-build的话,那也太麻烦了。介绍一个一劳永逸的方法。

首先,要给eclipse安装一个CDT,eclipse>help>install new software

下载地址为   http://download.eclipse.org/tools/cdt/releases/indigo/

反正我是把所有的选项都给安装了,也花不了太多的时间。

然后是安装sequoyah  地址为 http://download.eclipse.org/sequoyah/updates/2.0/

这个就只需要安装Sequoyah Android Native Code Support 一项就可以了。

然后把当前项目转换为C++项目。

File>new>other:

 

Convert to a c/c++ Project

依照图中所示设置:

Finish。

这个时候,项目中的c++代码也会被识别了。一般这个时候就会爆出一堆错误,主要是gcc找不到jni.h头文件,同样也识别不了来自jni的一些函数和数据类型:

 

于是就要给gcc添加一个环境变量让它来找到jni.h了,环境变量名称为C_INCLUDE_PATH。这个名字不能乱写,windows下类似的环境变量叫做INCLUDE。

然后多刷新几次工程,这些错误就消失了。

接下来,进行C++代码的编译配置。

进入工程属性页,build command设置为ndk下ndk-build的完整路径。

 

将Behavior选项页下的build处的all替换为空格

 

接下来就可以直接运行了,每次运行的时候,就会自动把C++代码编译成so文件。同时,可以在eclipse中编写java代码和C++代码,很是方便。

 

 

 

往下的工作就真的跟调试相关了。

首先进入工程目录,然后运行ndk目录下的ndk-gdb。执行这一步骤时,应当先确保有模拟器在运行。如果有人更习惯于命令式的方法来调试程序的话,就可以直接用这个ndk-gdb来调试程序了。接下来的步骤就是把这个gdb调试图形化。

 

在eclipse下进入debug>debug configuration,选择C/C++ Application,main选项卡下的C/C++ Application处填写为/home/shaodx/android/android-ndk-r7/samples/hello-jni/obj/local/armeabi/app_process,这个文件是专为调试而存在的,假如发现找不到这个文件的话,就应当先运行一次ndk-gdb。只运行ndk-build是不会产生这个文件的。Project选择当前的HelloJni。

最下面的 process launcher  select other 选择stuanard

然后进入Debugger选项卡,Stop on startup at 填写的是C++程序的入口函数,这个项目中就是Java_com_example_hellojni_HelloJni_stringFromJNI了,我觉得这个参数应该没什么用的,记得设置断点就行了。Gdb debugger 为:home/shaodx/android/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gdb,这个路径下有很多文件针对其他平台的,别选错了。Gdb command file为/home/shaodx/android/android-ndk-r7/samples/hello-jni/obj/local/armeabi/gdb2.setup,目前这个gdb2.setup文件还不存在,待会在创建。底下的Verbose console mode 一定要记得勾上,这样才能在eclipse控制台中用指令来与gdb交互。

 

 

进入debugger options的 connection子项,type为tcp,端口为5039:

 

设置完毕之后,apply。

接下来只需要修改两个文件即可。修改之前记得备份

首先是ndk-gdb,把最底下的一行 “$GDBCLIENT -x `native_path $GDBSETUP`”直接去掉,保存。

 

然后把'/home/shaodx/android/android-ndk-r7/samples/hello-jni/obj/local/armeabi'目录下的gdb.setup复制一份,命名为gdb2.setup。把gdb2.setup打开,去掉最后一行的“target remote :5039”,千万不要在以为把gdb.setup修改好,然后把之前的设置指向gdb.setup会起作用,因为每次调用ndk-gdb的时候,都会产生一个新的gdb.setup 来覆盖掉修改。

 

然后就可以给代码设置断点了,首先在调用本地方法之前记得有一个断点,之后也设置一个。

 

C++的代码在函数入口处设置一个断点即可。

下面开始正式的调试了,先运行项目的java调试。程序会再运行到第一个断点处停下来。

这个时候赶紧运行在命令行下进入工程目录,然后运行ndk根目录下的ndk-gdb。

运行之后是没有任何输出的。

 

然后启动C++的debug,即之前配置好的那个jni debug。

如图:

 

由于在C++程序之也设置了断点,继续摁F6就可以直接执行到C++程序中。

来个大图,熟悉eclipse的调试就没什么压力了。右上角可以直接看到局部变量的值,包括传进来的参数。

 

同时,可以在控制台直接与gdb通讯,要退出C++程序的调试的话,continue即可,程序又回到java代码:

 

 

 

完毕。








原创粉丝点击