得益于安卓原生应用API,从安卓API9级(安卓2.3,姜饼)开始,用纯原生代码编写安卓应用成为可能。也就是说,不需要一行Java代码。Android原生API定义在
此外,安卓NDK还提供了一个名为的静态库,帮助创建和管理本地活动。这个库的源代码可以在sources/android/native_app_glue/目录下找到。
在本章中,我们将首先介绍使用native_acitivity.h提供的简单回调模型创建原生活动,以及原生appglue库启用的更复杂但更灵活的双线程模型。然后,我们将讨论安卓NDK的窗口管理,我们将从本地代码中在屏幕上绘制一些东西。接下来介绍输入事件处理和传感器访问。最后,我们将介绍资产管理,它管理我们项目的assets文件夹下的文件。请注意,本章中介绍的API可以用来完全摆脱Java代码,但我们不必这样做。安卓NDK公司的资产管理方法提供了一个在混合代码安卓项目中使用资产管理应用编程接口的例子。
在我们开始之前,重要的是要记住,尽管在本地活动中不需要Java代码,但安卓应用仍然在达尔维克虚拟机上运行,并且许多安卓平台功能都是通过JNI访问的。Android原生应用API只是为我们隐藏了Java世界。
Android原生应用API允许我们创建一个原生活动,这使得用纯原生代码编写Android应用成为可能。这个食谱介绍了如何用纯C/C++代码编写一个简单的安卓应用。
以下步骤创建一个简单的安卓NDK应用,无需一行Java代码:
在我们的例子中,我们创建了一个简单的“纯”本地应用,当安卓框架调用我们定义的回调函数时,它会输出日志。“纯”本机应用并不是真正的纯本机应用。虽然我们没有编写一行Java代码,但是Android框架仍然在DalvikVM上运行一些Java代码。
总之,NativeActivity是一个包装器,为我们的原生代码隐藏了托管的安卓Java世界,并公开了native_activity.h中定义的原生接口。
anativactivity数据结构:本机代码中的每个回调方法都接受一个ANativeActivity结构的实例。安卓NDK在native_acitivity.h中定义ANativeActivity数据结构如下:
typedefstructANativeActivity{structANativeActivityCallbacks*callbacks;JavaVM*vm;JNIEnv*env;jobjectclazz;constchar*internalDataPath;constchar*externalDataPath;int32_tsdkVersion;void*instance;AAssetManager*assetManager;}ANativeActivity;前面代码的各种属性解释如下:
native_activity.h接口提供了一个简单的单线程回调机制,允许我们在没有Java代码的情况下编写一个活动。然而,这种单线程方法意味着我们必须从我们的本机回调方法中快速返回。否则,应用将变得对用户动作没有响应(例如,当我们触摸屏幕或按下菜单按钮时,应用没有响应,因为图形用户界面线程正忙于执行回调功能)。
解决这个问题的一个方法是使用多线程。例如,许多游戏需要几秒钟才能加载。我们需要将加载卸载到一个后台线程,这样用户界面就可以显示加载进度并响应用户输入。安卓NDK自带一个名为android_native_app_glue的静态库,帮助我们处理此类案件。这个库的细节包含在使用安卓原生应用胶水配方创建原生活动中。
在Java活动中也存在类似的问题。例如,如果我们编写一个Java活动,在onCreate搜索整个设备的图片,应用将变得无响应。我们可以使用AsyncTask在后台搜索加载图片,让主UI线程显示一个进度条,响应用户输入。
前面的配方描述了native_activity.h中定义的接口如何允许我们创建本地活动。但是定义的所有回调都是用主UI线程调用的,这意味着我们不能在回调中做繁重的处理。
安卓SDK提供AsyncTask、Handler、Runnable、Thread等,帮助我们在后台处理事情,与主UI线程进行沟通。安卓NDK提供了一个名为android_native_app_glue的静态库,帮助我们在一个单独的线程中执行回调函数和处理用户输入。本食谱将详细讨论android_native_app_glue库。
android_native_app_glue库建立在native_activity.h界面之上。因此,建议读者先阅读用native_activity.h界面创建一个原生活动食谱,然后再阅读本书。
以下步骤基于android_native_app_glue库创建一个简单的安卓NDK应用:
本食谱演示了如何使用android_native_app_glue库创建本地活动。
使用android_native_app_glue库应遵循以下步骤:
在我们的例子中,我们实现了一个名为handle_activity_lifecycle_events的简单函数,并将android_app->onAppCmd函数指针指向它。该函数只打印cmd值和与android_app数据结构一起传递的用户数据。cmd在android_native_app_glue.h中定义为enum。比如app启动时,cmd值为10、11、0、1、6,分别对应APP_CMD_START、APP_CMD_RESUME、APP_CMD_INPUT_CHANGED、APP_CMD_INIT_WINDOW、APP_CMD_GAINED_FOCUS。
安卓_原生_app_glue库内部构件:库的源代码可以在安卓NDK的sources/android/native_app_glue文件夹下找到。它只由两个文件组成,即android_native_app_glue.c和android_native_app_glue.h。让我们首先描述代码的流程,然后详细讨论一些重要的方面。
由于提供了native_app_glue的源代码,我们可以在必要时修改它,尽管在大多数情况下没有必要。
android_native_app_glue建在native_activity.h界面的上面。如下图代码所示(摘自sources/android/native_app_glue/android_native_app_glue.c)。实现ANativeActivity_onCreate功能,在这里注册回调函数,调用android_app_create函数。请注意,返回的android_app实例由本机活动的instance字段指向,该字段可以传递给各种回调函数:
voidANativeActivity_onCreate(ANativeActivity*activity,void*savedState,size_tsavedStateSize){LOGV("Creating:%p\n",activity);activity->callbacks->onDestroy=onDestroy;activity->callbacks->onStart=onStart;activity->callbacks->onResume=onResume;……activity->callbacks->onNativeWindowCreated=onNativeWindowCreated;activity->callbacks->onNativeWindowDestroyed=onNativeWindowDestroyed;activity->callbacks->onInputQueueCreated=onInputQueueCreated;activity->callbacks->onInputQueueDestroyed=onInputQueueDestroyed;activity->instance=android_app_create(activity,savedState,savedStateSize);}android_app_create函数(如下代码片段所示)初始化android_app数据结构的一个实例,该数据结构在android_native_app_glue.h中定义。该函数为线程间通信创建一个单向管道。之后,它会生成一个新线程(我们称之为后台线程)来运行android_app_entry函数,并将初始化后的android_app数据作为输入参数。主线程将等待后台线程启动,然后返回:
staticvoid*android_app_entry(void*param){structandroid_app*android_app=(structandroid_app*)param;……//AttachlifecycleeventqueuewithidentifierLOOPER_ID_MAINandroid_app->cmdPollSource.id=LOOPER_ID_MAIN;android_app->cmdPollSource.app=android_app;android_app->cmdPollSource.process=process_cmd;android_app->inputPollSource.id=LOOPER_ID_INPUT;android_app->inputPollSource.app=android_app;android_app->inputPollSource.process=process_input;ALooper*looper=ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);ALooper_addFd(looper,android_app->msgread,LOOPER_ID_MAIN,ALOOPER_EVENT_INPUT,NULL,&android_app->cmdPollSource);android_app->looper=looper;pthread_mutex_lock(&android_app->mutex);android_app->running=1;pthread_cond_broadcast(&android_app->cond);pthread_mutex_unlock(&android_app->mutex);android_main(android_app);android_app_destroy(android_app);returnNULL;}下图显示了主线程和后台线程如何协同工作来创建多线程本地活动:
我们以活动生命周期事件队列为例。主线程调用回调函数,回调函数只是写入管道的写端,而在android_main函数中实现的真循环将轮询事件。一旦检测到事件,函数就调用事件处理程序,该程序从管道的读取端读取确切的命令并处理它。android_native_app_glue库为我们实现了所有的mainthread素材和部分backgroundthread素材。我们只需要提供轮询循环和事件处理程序,如示例代码所示。
管道:主线程通过调用pipe方法在android_app_create函数中创建一个单向管道。此方法接受两个整数的数组。函数返回后,第一个整数被设置为引用管道读端的文件描述符,第二个整数被设置为引用管道写端的文件描述符。
不同的操作系统对管道有不同的实现。安卓系统实现的管道是“半双工”,通信是单向的。也就是说,一个文件描述符只能写,另一个文件描述符只能读。某些操作系统中的管道是“全双工”的,其中两个文件描述符都可以读写。
Looper是一个事件跟踪工具,它允许我们为一个线程的事件循环附加一个或多个事件队列。每个事件队列都有一个关联的文件描述符。事件是文件描述符上可用的数据。为了使用活套,我们需要包含android/looper.h头文件。
该库为我们将在后台线程中创建的事件循环附加了两个事件队列,包括活动生命周期事件队列和输入事件队列。为了使用活套,应执行以下步骤:
本章前面的食谱提供了简单的例子,只有logcat输出。本食谱将讨论如何管理安卓NDK系统的原生窗口。
建议读者在阅读本书之前阅读以下食谱:
以下步骤创建示例应用:
执行以下步骤在手机屏幕上绘制一个正方形:
输入事件对于安卓应用中的用户交互至关重要。本食谱讨论如何在安卓NDK系统中检测和处理输入事件。
我们将进一步发展上一个食谱中的例子。请阅读在安卓NDK管理原生窗口食谱,然后再看这一个。
以下步骤创建一个示例应用,它在本机代码中检测和处理输入事件:
本食谱讨论了安卓NDK的android_native_app_glue库的输入事件处理。
Android_native_app_glue中的输入事件队列:android_native_app_glue默认为我们附加输入事件队列。
事件处理程序:在handle_input_events功能中,我们首先c调用AInputEvent_getType获取输入事件类型。android/input.h头文件定义了两种输入事件类型,即AINPUT_EVENT_TYPE_KEY和AINPUT_EVENT_TYPE_MOTION。第一种事件类型指示输入事件是按键事件,而第二种事件类型指示它是运动事件。
我们调用AKeyEvent_getAction、AKeyEvent_getFlags和AKeyEvent_getKeyCode来获取一个关键事件的动作、标志和关键代码,并打印一个字符串来描述它。另一方面,我们调用AMotionEvent_getAction和AMotionEvent_getX来获取动作事件的动作和x位置。请注意,AMotionEvent_getX函数需要第二个输入参数作为指针索引。指针索引通过使用以下代码获得:
pointer_index=(action&AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)>>AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;还有很多输入事件功能,可以在andoid/input.h找到。
许多安卓设备都内置了传感器来检测和测量运动、方向和其他环境条件。可以访问安卓NDK系统中的传感器。这个食谱将详细讨论如何做。
本配方中提供的示例基于前两个配方中的示例代码。建议读者先阅读:
以下步骤开发了示例安卓应用,演示了如何从安卓NDK系统访问传感器:
在我们的例子中,我们使用加速度传感器来检测手机抖动。然后,根据手机的晃动速度,我们将红色矩形移动到手机屏幕的一侧。一旦矩形到达手机屏幕的一个边缘,它就开始移动到另一个边缘。
示例代码提供了一个简单的算法来确定是否发生了晃动。存在更复杂和更精确的算法,并且可以实现。我们也可以调整SHAKE_TIMEOUT和SHAKE_COUNT_THRESHOLD常量来微调算法。
该示例的重要部分是如何访问传感器。我们来总结一下步骤:
资产为安卓应用提供了一种包含各种类型文件的方式,包括文本、图像、音频、视频等。本食谱讨论如何从安卓NDK系统加载资产文件。
以下步骤描述了示例应用的开发方式:
在示例中,我们从assets文件夹加载.png文件,并将其用作OpenGL纹理。您可以使用以下步骤将改为assets:
cppintAAsset_read(AAsset*asset,void*buf,size_tcount);
输入参数buf指的是读取后数据放置的位置,count表示我们要读取的字节数。实际读取的字节数是返回的,可能与count不同。