最近在项目中遇到一个问题, 要对某个节点(dev/xxx)进行写操作, 但这个设备节点只允许root用户才能进行写操作, 因此不能通过Java或者JNI方式直接去访问, 因此想到了两种方法:
- 通过在init.rc中监听一个系统属性的值, 当属性变为某个值时, 触发一个可执行文件进行读写
- 编写一个Native Service, 然后以root的身份运行, 通过跨进程调用, 在Service中进行写操作
最后通过第一种方式解决了问题, 原因是写的频率很低, 基本一个手机就一次, 所以没必要弄成服务, 但本着学习的态度, 当然要了解下第二种方式的实现方法, 因此就有了这篇文章, 废话就到这, 开始正文.
定义Binder接口
要实现跨进程, 自然是使用Binder了, 因此我们首先要定义一个用于跨进程的接口, 我们通过一个读取和设置蓝牙地址的例子为例, 来讲解具体实现方法, 接口名为IDeviceMac
, 代码如下:
IDeviceMac.h
#ifndef XTC_IDEVICEMAC_H
#define XTC_IDEVICEMAC_H
#include
#include
#include
#include
#include
#ifdef TAG
#undef TAG
#endif
#define TAG "DeviceMac"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)
namespace android {
class IDeviceMac : public IInterface {
public:
enum {
SET_BT_MAC = IBinder::FIRST_CALL_TRANSACTION,
GET_BT_MAC,
};
virtual int setBTMac(String8 bt) = 0;
virtual String8 getBTMac() = 0;
DECLARE_META_INTERFACE(DeviceMac);
};
//-------------------------------------------
class BnDeviceMac : public BnInterface {
public:
virtual status_t onTransact( uint32_t code,
const Parcel& data,
Parcel* reply,
uint32_t flags);
};
} // end namespace android
#endif
代码很简单, 定义一个类继承自IInterface
, 里面接口就是我们自己要用到的, 其中DECLARE_META_INTERFACE(DeviceMac);
是一个宏定义, 用来定义继承IInterface
必须实现的两个方法, 具体是什么方法后面接口实现部分讲.
可以看到我们定义IDeviceMac
后, 还定义了一个类BnDeviceMac
,这个是Binder调用的一个规范, 即定义Ixxx
接口后, Bpxxx
表示Client端接口, Bnxxx
表示Service端接口, Bpxxx
和Bnxxx
都需要我们去实现具体内容, 并且Bnxxx
和Bpxxx
中的方法和Ixxx
中的方法是一一对应的.
实现BpDeviceMac和BnDeviceMac::onTransact()
IDeviceMac.cpp
#include "IDeviceMac.h"
namespace android {
class BpDeviceMac : public BpInterface {
public:
BpDeviceMac(const sp& impl) : BpInterface(impl)
{
}
int setBTMac(String8 bt) {
LOGI("Bp setBT");
Parcel data, reply;
data.writeInterfaceToken(IDeviceMac::getInterfaceDescriptor());
data.writeString8(bt);
remote()->transact(SET_BT_MAC, data, &reply);
return reply.readInt32();
}
String8 getBTMac() {
LOGI("Bp getBT");
Parcel data, reply;
data.writeInterfaceToken(IDeviceMac::getInterfaceDescriptor());
remote()->transact(GET_BT_MAC, data, &reply);
return reply.readString8();
}
};
IMPLEMENT_META_INTERFACE(DeviceMac, "DeviceMac");
/* Macro above expands to code below.
const android::String16 IDeviceMac::descriptor("DeviceMac");
const android::String16& IDeviceMac::getInterfaceDescriptor() const {
return IDeviceMac::descriptor;
}
android::sp IDeviceMac::asInterface(const android::sp& obj) {
android::sp intr;
if (obj != NULL) {
intr = static_cast(obj->queryLocalInterface(IDeviceMac::descriptor).get());
if (intr == NULL) {
intr = new BpDeviceMac(obj);
}
}
return intr;
}
*/
//---------------------------------------------------
status_t BnDeviceMac::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
CHECK_INTERFACE(IDeviceMac, data, reply);
LOGI("Bn onTransact code:%d", code);
switch(code) {
case SET_BT_MAC:
reply->writeInt32(setBTMac(data.readString8()));
return NO_ERROR;
case GET_BT_MAC:
reply->writeString8(getBTMac());
return NO_ERROR;
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
} // end namespace android
上面代码中IMPLEMENT_META_INTERFACE(DeviceMac, "DeviceMac");
下面注释掉的内容就是这个宏定义代表的实际代码, 也是就是说IDeviceMac.h
中的那个宏定义其实就是定义这两个方法.
BpDeviceMac
里面的内容就是把相关参数写到Parcel
中, 这是一个用来读写跨进程参数的类, 然后调用remote()->transact()
, 就调用到BnDeviceMac::onTransact()
中,BnDeviceMac::onTransact()
函数中已经跨过进程了, 具体怎么做到的, 这就涉及到IPC原理了,这里不做讨论, onTransact
做的事情也很简单, 就是从Parcel
中将Client传过来的数据读出来, 然后调用BnDeviceMac
中对应的实现方法, 这里需要注意, 由于BnDeviceMac::onTransact()
代码和BpDeviceMac
写在了同一个文件中, 看起来有点像BnDeviceMac
调用BpDeviceMac
的 一样, 其实是BnDeviceMac::onTranscat()
中调用的setBTMac() getBTMac()
是在调用BnDeviceMac
中实现的方法, 接下来就讲BnDeviceMac
的实现.
BnDeviceMac实现(Service)
DeviceMacService.h
#ifndef XTC_DEVICEMACSERVICE_H
#define XTC_DEVICEMACSERVICE_H
#include "IDeviceMac.h"
#define SERVER_NAME "DeviceMacService"
namespace android {
class DeviceMacService : public BnDeviceMac {
public:
DeviceMacService();
virtual ~DeviceMacService();
//IDeviceMac
virtual int setBTMac(String8 bt);
virtual String8 getBTMac();
};
} // end namespace android
#endif
DeviceMacService.cpp
#include "DeviceMacService.h"
namespace android {
DeviceMacService::DeviceMacService() {
}
DeviceMacService::~DeviceMacService() {
}
int DeviceMacService::setBTMac(String8 bt) {
LOGI("Bn setBT, bt:%s", bt.string());
return NO_ERROR;
}
String8 DeviceMacService::getBTMac() {
LOGI("Bn getBT");
return String8("4a:4b:4c:3a:3b:3c");
}
} // end namespace android
DeviceMacService
这个类继承了BnDeviceMac
, 实现了其中的方法, 所以BnDeviceMac::onTransact()
方法中相关调用就会调到DeviceMacService
, 在DeviceMacService
中, 我们就能做我们实际想做的事情了.
启动Service和Client端调用
main_server.cpp
#include "DeviceMacService.h"
#include
#include
#include
using namespace android;
sp getService() {
sp sm = defaultServiceManager();
if (sm == NULL) {
LOGE("can not get service manager");
}
sp binder = sm->getService(String16(SERVER_NAME));
if (binder == NULL) {
LOGE("can not get service");
}
sp service = interface_cast(binder);
if (service == NULL) {
LOGE("can not cast interface");
}
return service;
}
int main(int argc, char** argv) {
if (argc == 1) {
LOGI("start DeviceMacService");
defaultServiceManager()->addService(String16(SERVER_NAME), new DeviceMacService());
android::ProcessState::self()->startThreadPool();
IPCThreadState::self()->joinThreadPool();
} else if (argc == 2) {
sp devMacServer = getService();
devMacServer->setBTMac(String8("1a:1b:1c:1a:1b:1c"));
String8 bt = devMacServer->getBTMac();
LOGI("get bt mac:%s", bt.string());
}
return 0;
}
添加服务的代码很简单, 三行代码, 固定的操作, 获取服务过程用, 有个interfa_cast
的函数, 会将IBinder
作为参数 new
一个BpDeviceMac
对象, 我们通过这个对象进行相关接口调用, 最终调用到DeviceMacService
.
注: 为了测试方便, 我将添加Service和调用Service写在了同一个可执行文件中, 实际项目都是分开的.
编译
现在万事具备, 只等编译运行了, Android.mk
代码如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := macserver
LOCAL_MODULE_TAGS := optional
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include \
frameworks/native/include \
system/core/include
LOCAL_SRC_FILES := IDeviceMac.cpp DeviceMacService.cpp main_server.cpp
LOCAL_SHARED_LIBRARIES := libutils libcutils libbinder libhardware
include $(BUILD_EXECUTABLE)
整个代码目录结构如下:
编译方法:
- 确保当前Android源码全部编译通过(有些依赖需先编译好)
- 将service目录放到Android源码目录中(比如vendor/qcom/service)
- 在Android源码根目录执行
mmm vendor/qcom/service
- 执行完后编译的可执行文件在
out/target/product/xxx/system/bin/
下面(xxx为lunch的product) - 将编译好的可执行文件
macserver
通过adb push 到手机system/bin/下面(adb需要root, 即执行 adb root , adb remount) - 执行
adb shell chmod 777 /system/bin/macserver
加上可执行权限, 然后启动服务, 执行adb shell /system/bin/macserver
(会阻塞当前窗口) - 重新开一个窗口执行adb命令
adb shell /system/bin/macserver 1
即可调用Service, 可以通过logcat过滤``DeviceMac```来查看log.
如果你想在开机后就自动启动服务, 并且指定Service所属的用户组, 可在init.rc中加入如下代码
service macserver /system/bin/macserver
class main
user root
group root
注: 如果要把这个可执行文件编译到系统中,还需在相关的product的配置mk中添加PRODUCT_PACKAGES += macserver
另一种写法
上述流程是一个完整的Native Service实现过程, 以及调用方式, 其实还有一种简洁的方式, 就是写一个类继承自BBinder
, 然后实现onTransact()
方法, 定义如下:
class NativeService : public BBinder
{
public:
NativeService();
virtual ~NativeService();
virtual status_t onTransact(uint32_t, const Parcel&, Parcel*, uint32_t);
};
这样就不用管Bn和Bp端了, 相当于只用实现Service端, 但在Client端调用的时候, 通过sp
获取引用后, 就不用转为相关定义的接口了, 因为你根本没定义接口, 这时候调用只能调用其transact()
方法, 通过第一个参数区分是那种情况的调用, 参数传递也是通过写到Parcel
中Service端去读, 本质上和上面BnBp架构一样, 只是可以少写点代码, 但缺点也很明显, 作为功能接口这样写肯定不好, 调用者使用起来很不方便.
个人理解, 这种方法和上述讲的方法区别只是一个封装的问题, Bn Bp方式只是对接口写法的一个规范, 让接口使用者调用起来更加清晰明了.
Java端调用
其实我们虽然是使用C++写的Native Service, 但Android系统为我们做了很多事, 我们其实也可以通过Java直接调用的, 方法如下:
public void testNativeService() {
IBinder service = ServiceManager.getService("DeviceMacService");
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
boolean res = service.transact(2, data, reply, 0);
if (!res) {
Log.e("Test", "transact fail");
}
String result = reply.readString();
data.recycle();
reply.recycle();
} catch (RemoteException e) {
e.printStackTrace();
}
}
但由于ServiceManager
这个类不是公开的, 你只能通过反射去调用, 或者是Android系统开发在源码中进行编译和调用, 另外Java使用了String
作为参数的话, C++就要使用String16
这个来与其对应.
编写AIDL
如果你既想少写点代码, 又想调用起来比较方便, 这个也有实现方法, 就是编写AIDL文件, 和Java里面的AIDL类似, 只不过你要放在Android源码里面进行编译, 系统会自动根据Ixxx.aidl
在编译过程中生成Ixxx.cpp
, 这个cpp文件中就和上面我们写的IDeviceMac.cpp
内容基本一致, 也就是说这部分代码可以自动生成了, 然后你只需要在Service端写一个类继承Bnxxx
然后实现AIDL文件中定义的方法即可, 使用非常方便, Android 7.1上面的ICameraService.aidl
就是以这种方式实现的, 部分代码如下, 可以参考一下:
frameworks/av/camera/aidl/android/hardware/ICameraService.aidl
/**
* Types for getNumberOfCameras
*/
const int CAMERA_TYPE_BACKWARD_COMPATIBLE = 0;
const int CAMERA_TYPE_ALL = 1;
/**
* Return the number of camera devices available in the system
*/
int getNumberOfCameras(int type);
/**
* Fetch basic camera information for a camera device
*/
CameraInfo getCameraInfo(int cameraId);
/**
* Default UID/PID values for non-privileged callers of
* connect(), connectDevice(), and connectLegacy()
*/
const int USE_CALLING_UID = -1;
const int USE_CALLING_PID = -1;
/**
* Open a camera device through the old camera API
*/
ICamera connect(ICameraClient client,
int cameraId,
String opPackageName,
int clientUid, int clientPid);
如果以这种方式实现的话, 编译的Android.mk中需要加入如下代码:
LOCAL_AIDL_INCLUDES := \
frameworks/av/camera/aidl \
LOCAL_SRC_FILES := \
aidl/android/hardware/ICameraService.aidl \
即要引入头文件路径和aidl源文件.
如果你想要看下自动生成的Ixxx.cpp
的代码, 其路径为:
out/target/product/xxx1/obj/xxx2/xxx3_intermediates/aidl-generated/
xxx1表示你lunch时选的product, xxx2表示你编译的模块类型, 通常是 SHARED_LIBRARIES 或者
EXECUTABLES, xxx3表示你编译的模块中LOCAL_MODULE定义的名字.
例如: out/target/product/msm8953/obj/SHARED_LIBRARIES/libcamera_client_intermediates/aidl-generated/src/aidl/android/hardware/ICameraService.cpp