开发Android硬件访问服务
这篇文章主要研究两方面的知识,一个是我们在使用已开发好的动态链接库来访问硬件设备的访问权限问题,另一个就是为应用程序开发访问硬件设备的硬件访问服务,下面就进入正题。
Android硬件访问权限处理:
在上面的“硬件抽象层”一篇中,我们知道硬件访问服务会调用动态链接库文件中的helloworld_device_open这个函数方法来打开欲访问的硬件设备。可是,当我们执行这个方法之后,就会莫名的报出日志中的错误:failed to open /dev/helloworld ---permission denied,原因就是因为当前的用户没有访问这个硬件设备的权限。无可置疑,解决这个问题的办法就是改写这个设备访问的权限,使除了root之外的其他用户也可以拥有打开这个设备的权限。
在Android中提供了一种改变访问设备文件权限的uevent机制,通过这个机制我们可以在系统启动的时候来修改设备文件/dev/helloworld的访问权限。这个访问权限控制文件位于目录:system/core/rootdir中,通过在文件ueventd.rc中添加一行内容,
内容为:/dev/helloworld 0666 root root即可使所有用户都拥有访问这个设备文件的权限。当然,我们知道修改了源代码文件,如果想使其生效,我们就得重新编译修改的源代码。但在这里面,我们通过另一种方式来达到不需重新编译源代码就可以使其修改生效的方法。原因就是在编译源代码的时候,系统会将文件system/core/rootdir/ueventd.rc拷贝到目录out/target/product/generic/root中,这是一个根目录,表示在系统启动的时候,会把镜像文件ramdisk.img中的ueventd.rc文件安装在设备根目录中,并调用init进行来解析和修改更新相应的设备文件的访问权限,因此我们只需要修改ramdisk.img镜像文件中的ueventd.rc文件即可。下面即为修改方法:
A、解压ramdisk.img文件
镜像文件ramdisk.img文件是一个gzip格式文件,我们可以使用命令gunzip来进行解压,并保存在源代码目录Android中。
Android$ mv ./out/target/product/generic/ramdisk.img ./ramdisk.img.gz
Android$gunzip ./ramdisk.img.gz
B、还原ramdisk,img文件
解压之后的ramdisk.img文件是一个cpio格式的归档文件,我们需要对其使用cpio命令来解除归档,并将解档后的文件保存在目录Android/ramdisk/目录中。
Android$mkdir ramdisk
Android/ramdisk$cpio -i -F ../ramdisk.img
C、修改ueventd.rc文件
进入到Android/ramdisk/中,找到文件ueventd.rc文件,并在文件中添加一行内容:/dev/helloworld 0666 root root来赋予所有用户对这个设备的访问操作权限。
D、重新打包ramdisk.rc文件
重新打包就是对A、B的逆过程操作,操作如下:
Android/ramdisk$rm -f ../ramdisk.img
Android/ramdisk$find . | cpio -o -H newc > ../ramdisk.img.unzip
Android/ramdisk$cd ..
Android$gzip -c ./ramdisk.img.unzip > ./ramdisk.img.gz
Android$rm -f ./ramdisk.img.unzip
Android$rm -r ./ramdisk
Android$mv ./ramdisk.img.gz ./out/target/product/generic/ramdisk.img
经过上面的操作后,我们就成功得修改了镜像文件中ueventd.rc配置文件,为所有用户赋予了访问设备/dev/helloworld文件的权限,那么下面我们就可以编写对应的硬件访问服务来调用open函数方法访问设备了。
开发Android硬件访问服务:
开发好硬件抽象层动态链接库文件之后,我们需要在应用程序框架层中实现一个硬件访问服务,为应用程序访问设备提供服务接口。但需要注意的是框架层中的服务是由Java语言编写的,而抽象层中动态链接库文件是由C++语言编写的,因此,我们需要解决跨语言的桥接需求。很庆幸,Android系统提供了Java本地接口(Java Native Interface,JNI)机制来实现多语言之间的功能访问桥接(在后面文章中会分析这个JNI的实现机制)。
解决了多语言之间的桥接之后,我们还面临一个问题就是多进程间的通信了。原因是这样的,在Android中硬件访问服务一般是运行在系统进程system中的,而是用这些硬件访问服务的应用程序是运行在其他的进行中的,因此,需要解决多进程之间的通信和资源共享需求了。又是一个值得庆幸的地方,聪明的人们也为Android多进程间通信提供了一种高效而易理解的机制,即为IBinder进程间通信机制(在后面的文章中分析它的机制,因为它很重要)。应用程序就是通过这个机制来访问运行在系统进程system中的硬件访问服务的。值得注意的是,IBinder进程通信机制要求服务的一方必须为使用方提供一个访问接口来供调用方使用,这样使用方就可以访问这个硬件访问服务了,那么首先就需要定义一个服务方接口了。
定义硬件访问服务接口:
Android系统提供了一种用来解决跨进程访问服务的描述性语言---Android接口描述语言(Android Interface Definition Language,AIDL)。用AIDL语言定义的服务接口的文件后缀为.aidl,在解析编译的时候,系统会把这种类型的文件转换为Java文件,供虚拟机编译使用,那么下面就开始用AIDL 定义硬件访问服务接口文件IHelloWorldService.aidl。
在Android中,一般会将AIDL定义的硬件访问服务接口文件放入于目录:framworks/base/core/java/android/os中,那么我们也将IHelloWorldService.aidl放在这里。下面为具体定义:
IHelloWorldService.aidl:
Package android.os;
Interface IHelloWorld {
Void setValue(int val);
Int getValue();
}
定义完接口后,由于这个接口是使用AIDL语言编写,所以需要对其进行编译,然后才能得到对应的Java文件。具体操作如下:
进入到框架根目录:frameworks/base/中,打开里面的Android.mk编译配置文件,修改里面的LOCAL_SRC_FILES变量的值。
LOCAL_SRC_FILES += \
....
Core/java/android/os/IHelloWorld.aidl
修改保存之后,我们就需要使用命令mmm来对这个硬件访问服务接口定义文件进行编译了。
Android$mmm ./frameworks/base/
编译成功后,得到了一个framework.jar文件,下面对这个文件做简单的介绍:
在这个jar文件中就包含有接口文件IHelloWorldService接口了,并且这个接口继承自android.os.IInterface接口。在IHelloWorldService接口内部定义了一个IBinder本地对象类Stub,它同时也实现了接口IHelloWorldService,并继承了android.os.Binder类。此外,在IHelloWorldService中还定义了一个Binder代理对象类Proxy,它也实现了IHelloWorldService接口文件。
正如前面说的,我们使用AIDL定义了一个服务访问接口文件来实现进程间的通信,服务的一方称之为server进程端,使用服务一方则为client进程端了。另外,在server进程中,每一个服务都对应着一个IBinder本地对象,并借助一个通信点(Stub)来等待使用服务方发送进程间通信的请求。而client端在访问运行在sever中的服务时,首先要得到一个它的IBinder代理对象接口,然后通过这个代理对象接口来发送进程间通信的请求。下面即为具体实现(这个Java文件放在了frameworks/base/core/services/java/com/android/server中):
HelloWorldService.java:
package com.android.server;
import android.content.Context;
import android.os.IHelloworldService;
import android.util.Slog;
public class HelloworldService extends IHelloworldService.Stub {
private static final String TAG = "HelloworldService";
private int ptr = 0;
helloworldService() {
ptr = init_native();//调用JNI方法init_native打开设备,并得到对应设备句柄值
if (ptr == 0) {
Slog.e(TAG,"failed to init helloworld service.");
}
}
public void setVal(int val) {
if (ptr == 0) {
Slog.e(TAG,"helloworld service is not init.");
return;
}
setVal_native(ptr,val);//调用JNI方法setVal_native向硬件设备写入值
}
public int getVal() {
if (ptr == 0 ) {
Slog.e(TAG,"helloworld service is not init.");
return 0;
}
return getVal_native(ptr);//调用JNI方法setVal_native从硬件设备值取值
}
//初始化JNI相关方法,并传入值
private static native int init_native();
private static native void setVal_native(int ptr,int val);
private static native int getVal_native(int ptr);
}
编写完硬件访问服务之后,需要执行mmm命令来重新编译系统的services模块了。
Android$mmm ./frameworks/base/services/java/
编译成功之后,得到了一个service.jar程序包,当然这个包就包好有我们编写的HelloWorldService.java类了。下面继续分析硬件访问服务HelloWorldService的JNI实现了。
HelloWorldService的JNI实现:
在Android中,一般把硬件访问服务JNI方法放在目录:frameworks/base/services/jni/中,那么我们也将HelloWorldService的JNI实现文件放于这个目录中,同时,文件的命名规范也遵循着Android的命名规范习惯。
Com_android_server_HelloWorldService.cpp:
#define LOG_TAG "HelloWorldServiceJNI"
#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"
#include <utils/misc.h>
#include <utils/Log.h>
#include <hardware/hardware.h>
#include <hardware/helloworld.h>
#include <stdio.h>
namespace android
{
/*设置硬件设备的寄存器的值*/
static void helloworld_setVal(JNIEnv* env, jobject clazz, jint ptr, jint value) {
helloworld_device_t* device = (helloworld_device_t*)ptr;
if(!device) {
LOGE("device helloworld is not open.");
return;
}
int val = value;
LOGI("Set value %d to device helloworld.", val);
device->set_val(device, val);
}
/*读取硬件设备寄存器的值*/
static jint helloworld_getVal(JNIEnv* env, jobject clazz, jint ptr) {
helloworld_device_t* device = (helloworld_device_t*)ptr;
if(!device) {
LOGE("device helloworld is not open.");
return 0;
}
int val = 0;
device->get_val(device, &val);
LOGI("Get value %d from device helloworld.", val);
return val;
}
/*打开虚拟硬件设备*/
static inline int helloworld_device_open(const hw_module_t* module, struct helloworld_device_t** device) {
return module->methods->open(module, helloworld_HARDWARE_device_ID, (struct hw_device_t**)device);
}
/*初始化虚拟硬件设备*/
static jint helloworld_init(JNIEnv* env, jclass clazz) {
helloworld_module_t* module;
helloworld_device_t* device;
LOGI("Initializing HAL stub helloworld......");
//加载导入硬件抽象层模块helloworld
if(hw_get_module(helloworld_HARDWARE_MODULE_ID, (const struct hw_module_t**)&module) == 0) {
LOGI("device helloworld found.");
if(helloworld_device_open(&(module->common), &device) == 0) {
LOGI("device helloworld is open.");
return (jint)device;
}
LOGE("Failed to open device helloworld.");
return 0;
}
LOGE("Failed to get HAL stub helloworld.");
return 0;
}
/*Java本地接口方法列表*/
static const JNINativeMethod method_table[] = {
{"init_native", "()I", (void*)helloworld_init},
{"setVal_native", "(II)V", (void*)helloworld_setVal},
{"getVal_native", "(I)I", (void*)helloworld_getVal},
};
/*注册Java本地接口方法*/
int register_android_server_helloworldService(JNIEnv *env) {
return jniRegisterNativeMethods(env, "com/android/server/helloworldService", method_table, NELEM(method_table));
}
};
注意:在使用helloworld_setVal和helloworld_getVal方法时,使用者需要且必须先使用JNI方法helloworld_init来打开硬件设备,这样就可以获得对应的helloworld_devict_t接口。
具体可以参看上面的注释说明,当然,有些关于JNI的说明不是很多,这个没关系。在后面的文章中,我会继续分析JNI的实现。这里只是演示了实现的流程,把握Android系统的执行原理。
编写完硬件访问服务HelloWorldService的JNI实现之后,需要修改jni目录下的onload.cpp文件,因为这个文件的实现是在libandroid_servers模块中的。而当系统加载这个模块的时候,就会初始化执行onload.cpp文件中的JNI_Onload函数。这样就可以将定义的init_native、setVal_native和getVal_native三个方法注册到Java虚拟机中了。然后,我们切换到目录:frameworks/base/services/jni/中,打开文件Android.mk,修改变量:
LOCAL_SRC_FILES += \
...
Onload.cpp
同样修改保存之后,执行命令mmm进行编译工作:
Android$mmm ./fremeworks/base/services/jni/
编译后得到修改模块的jar包,那么这个包中就含有我们实现的方法init_native、setVal_native和getVal_native了。
搞定了多语言桥接之后,等着我们的问题就是如何解决跨进程通行的机制了。原因在上面已经说明了,就是因为硬件访问服务一般是运行在系统system进程中,而欲使用这个服务的应用程序进程是位于其他进程中,解决办法已在上面列出。接下来就是如何启动这个硬件访问服务了。具体如下:
我们在“Android系统体系结构分析”中也已分析了,系统进程system是由进程Zygote(我们称之为应用程序孵化器,这个会在以后的文章分析)来启动的。而这个孵化器是在系统启动的时候启动的,因此,就会把硬件访问服务运行在了系统进程system中,即实现开启启动了。
那么下面即为如何将硬件访问服务运行在系统进程system中,首先进入到frameworks/base/services/java/com/android/server/中,打开修改里面的ServerThread进程类。具体如下:
Public void run() {
...
If (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) {
...
Try {
Slog.i(TAG,”Helloworld SERVICE start”);
ServiceManager.addService(“helloworld”,new HelloWorldService());
}catch(Throwable a) {
Slog.e(TAG,”failed to start helloworld service”,e);
}
...
...
}
上面有提到一个ServiceManager这个类,它的作用就是管理和协调Android系统中的IBinder进程间通信机制的很重要的角色,它负责管理系统中的服务对象。注册到ServiceManager中的服务对象都有一个服务名字,这些服务名字就用来供应用程序使用来向ServiceManager申请对应的IBinder代理对象接口,以便可以访问对应的硬件服务。而硬件访问服务注册完成之后,它的启动就结束了。
最后,我们需要执行命令mmm 来编译services模块。
Android$mmm ./frameworks/base/services/java/
编译后得到的service.java文件包就含有我们自己编写的HelloWorldService访问服务了,并且在系统启动的时候,会将服务的执行放在系统进程system中。走到这里,我们的硬件访问服务就完全的编写完成了,我们可以使用make snod重新打包Android系统镜像文件system.img了,使我们的硬件访问服务运行在system中生效。
为了演示我们的整个流程思路,在下一篇会开发一个应用程序验证使用硬件访问服务访问设备的正确性。
本人刚创建了一个QQ群,目的是共同研究学习Android,期待兴趣相投的同学加入!!
群号:179914858