Android传感器框架分析

以原相心率传感器为例进行分析: 

原相心率传感器驱动pixart_hc_driver.c,主要通过采集寄存器数据放入_ppg_mems_data结构体然后通过input输入子系统将事件上报。

input_report_abs(ofndata.pah8001_input_dev, ABS_X,
                     *(uint32_t
                       *) (_ppg_mems_data
                           [_write_index].HRD_Data));
            input_report_abs(ofndata.pah8001_input_dev, ABS_Y,
                     *(uint32_t
                       *) (_ppg_mems_data
                           [_write_index].HRD_Data +
                           4));
            input_report_abs(ofndata.pah8001_input_dev, ABS_Z,
                     *(uint32_t
                       *) (_ppg_mems_data
                           [_write_index].HRD_Data +
                           8));
            input_sync(ofndata.pah8001_input_dev);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

最后所有上报的事件都会被写入传感器的设备文件,通过监听设备文件即可得到上报的事件。 
具体的hal层实现由厂商自己实现,而原相心率传感器hal层代码在HerateSensorPixart.cpp中实现。该层主要功能是读取设备文件中的事件,即_ppg_mems_data结构体中的13个数据,心率数据的合成由这13个数据以及gsensor的三个数据组成。因此还打开了gsensor传感器并获取3个gsensor的数据。并实现一系列的方法供上层调用; 
首先定义了一个传感器类型

static sensor_t sSensor = {
        "Heart rate sensor",
        "PixArt",
        1,
        SENSORS_PIXART_HEART_RATE_HANDLE,
        SENSOR_TYPE_HEART_RATE,
        1100.0f,
        0.005f,
        0.005f,
        10000,
        { }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然后定义了一些全局变量并打开设备文件返回fd:

HeartRateSensorPixArt::HeartRateSensorPixArt()
    : SensorBase(PIXART_DEVICE_PATH, NULL),
      mExit(false),
      gsensor_fd(-1),
      mExist(false),
      mEnabled(false),
      mDelay(-1),
      mMainReadFd(-1),
      mMainWriteFd(-1),
      mWorkerReadFd(-1),
      mWorkerWriteFd(-1),
      mStartThread(false),
      mHeartRate(-1),
      mHeartRateStatus(0),
      mStatusChanged(false),
      mTouchStatus(false),
      mGradeChanged(false),
      mSensorName("PixArt Heart Rate"),
      mAlgHeartRatePixArt(NULL),
      mSensorListener(NULL)
{
    open_device();
    if (dev_fd >= 0) {
        mExist = initialize();
    }

    if (!mExist)
        LOGI("NO PixArt Heart Rate sensor!");
    else
        LOGI("PixArt Heart Rate sensor initialize done.");
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
并初始化了一个管道进行数据的传输;最后得到合成的心率数据并向管道中传输数据;
        if (heartRate >= 0)
            hs->mHeartRate = heartRate;

        if (hs->mStatusChanged || heartRateChange) {
            char msg = 'h';
            retval = write(hs->mWorkerWriteFd, &msg, 1);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里还有一个函数connectToSensorService(),这里是实现了传感器在SensorEventListener上的注册,只有注册了这个接口才会启动线程。

void HeartRateSensorPixArt::connectToSensorService()
{
    int retval = 0;
    mSensorListener = new SensorListener();
    if ((mSensorListener != NULL) && mSensorListener.get()) {

        retval = mSensorListener->initialize();
        if (retval != NO_ERROR) {
            mSensorListener.clear();
            mSensorListener = NULL;
        }
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

接下来由上层的Sensor_mpl.cpp new出一个

HeartRateSensorPixArt的对象获取里面的方法。
 if (!strcmp(gId, SENSORS_HARDWARE_POLL_FOR_IWDS)) {
        mHeartRateSensorPixArt = new HeartRateSensorPixArt();
        if (!mHeartRateSensorPixArt->isExist()) {
            delete mHeartRateSensorPixArt;
            mHeartRateSensorPixArt = NULL;
        } else {
            sensorsForIwds += mHeartRateSensorPixArt->populateSensorList(&sSensorListForIwds[sensorsForIwds]);
        }
    } else {
        mHeartRateSensorPixArt = NULL;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

通过mHeartRateSensorPixArt->getFd()方法得到管道的另一端读数据的fd,最后打开管道读取数据

else if (fd == pixart_heart_rate) {
                    if (mHeartRateSensorPixArt != NULL) {
                        mPollFds[i].revents = 0;
                        char buf[8];
                        int ret = read(mPollFds[i].fd, buf, sizeof(buf));
                        if (ret == -1) {
                            ALOGE("poll pixart heart pipe fd error!%d %c", ret,
                                    buf[0]);
                        } else {
                            nb = mHeartRateSensorPixArt->readEvents(data, count);
                            if (nb > 0) {
                                count -= nb;
                                nbEvents += nb;
                                data += nb;
                            }
                        }
                    }
                }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

open_sensor中包装了一系列操作方法,其中大部分都是调用到了HerateSensorPixart.cpp中的实现方法。

static int open_sensors(const struct hw_module_t* module, const char* id,
                        struct hw_device_t** device)
{
    FUNC_LOG;
    int status = -EINVAL;

    gId = strdup(id);

    sensors_poll_context_t *dev = new sensors_poll_context_t();

    memset(&dev->device, 0, sizeof(sensors_poll_device_t));

    dev->device.common.tag = HARDWARE_DEVICE_TAG;
    dev->device.common.version  = 0;
    dev->device.common.module   = const_cast<hw_module_t*>(module);
    dev->device.common.close    = poll__close;
    dev->device.activate        = poll__activate;
    dev->device.setDelay        = poll__setDelay;
    dev->device.poll            = poll__poll;
    dev->device.calibrate  = poll__calibrate;
    dev->device.setRightHand        = poll__setRightHand;

    *device = &dev->device.common;
    status = 0;

    return status;
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在接下来是JNI层的实现了,代码在SensorDevice.cpp中实现,这里面主要是实现动态链接库的访问,如打开动态链接库以及函数的转换;

SensorDevice::SensorDevice()
    :  mSensorDevice(0),
       mSensorModule(0)
{
    status_t err = hw_get_module(SENSORS_HARDWARE_MODULE_ID,
            (hw_module_t const**)&mSensorModule);

    ALOGE_IF(err, "couldn't load %s module (%s)",
            SENSORS_HARDWARE_MODULE_ID, strerror(-err));

    if (mSensorModule) {
        err = sensors_open(&mSensorModule->common, &mSensorDevice);

        ALOGE_IF(err, "couldn't open device for module %s (%s)",
                SENSORS_HARDWARE_MODULE_ID, strerror(-err));

        if (mSensorDevice) {
            sensor_t const* list;
            ssize_t count = mSensorModule->get_sensors_list(mSensorModule, &list);
            mActivationCount.setCapacity(count);
            Info model;
            for (size_t i=0 ; i<size_t(count) ; i++) {
                mActivationCount.add(list[i].handle, model);
                mSensorDevice->activate(mSensorDevice, list[i].handle, 0);
            }
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

通过hw_get_module这个接口来获取HAL层调用,关键是SENSORS_HARDWARE_MODULE_ID这个唯一标识;最后获得mSensorDevice这个结构体,里面包含了HAL层中的所有函数实现;包括对传感器是能。

接下来是就是Framwork层的分析了

总体关系调用图
这里我们把它分为客户端和服务端,服务端主要是从HAL读取数据并将数据写入到管道中,客户端主要是从管道中读出数据。 
客户端主要类 
SensorManager.Java 
从android4.1开始,把SensorManager定义为一个抽象类,定义了一些主要的方法,类主要是应用层直接使用的类,提供给应用层的接口 
SystemSensorManager.java 
继承于SensorManager,客户端消息处理的实体,应用程序通过获取其实例,并注册监听接口,获取sensor数据 
sensorEventListener接口 
用于注册监听的接口 
sensorThread 
是SystemSensorManager的一个内部类,开启一个新线程负责读取读取sensor数据,当注册了sensorEventListener接口的时候才会启动线程 
android_hardware_SensorManager.cpp 
负责与java层通信的JNI接口 
SensorManager.cpp 
sensor在Native层的客户端,负责与服务端SensorService.cpp的通信 
SenorEventQueue.cpp 
消息队列

服务端主要类 
SensorService.cpp 
服务端数据处理中心 
SensorEventConnection 
从BnSensorEventConnection继承来,实现接口ISensorEventConnection的一些方法,ISensorEventConnection在SensorEventQueue会保存一个指针,指向调用服务接口创建的SensorEventConnection对象 
Bittube.cpp 
在这个类中创建了管道,用于服务端与客户端读写数据 
SensorDevice 
负责与HAL读取数据

客户端主要实现: 
APK监听 
获取sensor service对象

mService = (SensorServiceManager) serviceClient.getServiceManagerContext();
    private void registerSensors() {
        /* 通过 getDefaultSensor 获取各个 Sensor */
        mHeartRateSensor = mService.getDefaultSensor(Sensor.TYPE_HEART_RATE);
        if (mHeartRateSensor != null) {
            mService.registerListener(mListener, mHeartRateSensor, 0);
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

监听数据变化获取心率数据

 private SensorEventListener mListener = new SensorEventListener() {

        /*
         * 监控 Sensor 数据变化
         */
        @Override
        public void onSensorChanged(SensorEvent event) {
            if (event.sensorType == Sensor.TYPE_HEART_RATE) {
                Log.d(SUB_TAG, "Update Heart Rate : " + event.values[0]);
                mHeartRateData = event.values[0];
            }
            mHeartRateText.setText("Heart Rate: " + mHeartRateData);
        }
初始化SystemSensorManager
    public SystemSensorManager(Context context,Looper mainLooper) {
        mMainLooper = mainLooper;       
              mContext = context;

        synchronized(sListeners) {
            if (!sSensorModuleInitialized) {
                sSensorModuleInitialized = true;

                nativeClassInit();

                // initialize the sensor list
                sensors_module_init();
                final ArrayList<Sensor> fullList = sFullSensorsList;
                int i = 0;
                do {
                    Sensor sensor = new Sensor();
                    i = sensors_module_get_next_sensor(sensor, i);

                    if (i>=0) {
                        //Log.d(TAG, "found sensor: " + sensor.getName() +
                        //        ", handle=" + sensor.getHandle());
                        fullList.add(sensor);
                        sHandleToSensor.append(sensor.getHandle(), sensor);
                    }
                } while (i>0);

                sPool = new SensorEventPool( sFullSensorsList.size()*2 );
                sSensorThread = new SensorThread();
            }
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

系统开机启动的时候,会创建SystemSensorManager的实例,在其构造函数中,主要做了四件事情: 
初始化JNI 
调用JNI函数nativeClassInit()进行初始化 
初始化Sensor列表 
调用JNI函数sensors_module_init,对Sensor模块进行初始化。创建了native层SensorManager的实例。 
获取Sensor列表 
调用JNI函数sensors_module_get_next_sensor()获取Sensor,并存在sHandleToSensor列表中 
构造SensorThread类 
构造线程的类函数,并没有启动线程,当有应用注册的时候才会启动线程 
启动SensorThread线程读取消息队列中数据

protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor,
            int delay, Handler handler) {

        synchronized (sListeners) {
            ListenerDelegate l = null;
            for (ListenerDelegate i : sListeners) {
                if (i.getListener() == listener) {
                    l = i;
                }
            }
            …….
            // if we don't find it, add it to the list
            if (l == null) {
                l = new ListenerDelegate(listener, sensor, handler);
                sListeners.add(l);
                  ……
                    if (sSensorThread.startLocked()) {
                        if (!enableSensorLocked(sensor, delay)) {
                          …….
                        }
                 ……
            } else if (!l.hasSensor(sensor)) {
                l.addSensor(sensor);
                if (!enableSensorLocked(sensor, delay)) {
                    ……
                }
            }
        }
        return result;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

当有应用程序调用registerListenerImpl()方法注册监听的时候,会调用SensorThread.startLoacked()启动线程,线程只会启动一次,并调用enableSensorLocked()接口对指定的sensor使能,并设置采样时间。

SensorThreadRunnable实现了Runnable接口,在SensorThread类中被启动
  boolean startLocked() {
            try {
                if (mThread == null) {
                    SensorThreadRunnable runnable = new SensorThreadRunnable();
                    Thread thread = new Thread(runnable, SensorThread.class.getName());
                    thread.start();
                    synchronized (runnable) {  //队列创建成功,线程同步
                        while (mSensorsReady == false) {
                            runnable.wait();
                        }
                    }

        }
private class SensorThreadRunnable implements Runnable {
            SensorThreadRunnable() {
            }
            private boolean open() {
                sQueue = sensors_create_queue();
                return true;
            }
            public void run() {
                …….
                if (!open()) {
                    return;
                }
                synchronized (this) {
                    mSensorsReady = true;
                    this.notify();
                }
                while (true) {
                    final int sensor = sensors_data_poll(sQueue, values, status, timestamp);
                    …….
            }
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

在open函数中调用JNI函数sensors_create_queue()来创建消息队列,然后调用SensorManager. createEventQueue()创建。 
在startLocked函数中启动新的线程后,做了一个while的等待while (mSensorsReady == false),只有当mSensorsReady等于true的时候,才会执行enableSensorLocked()函数对sensor使能。而mSensorsReady变量,是在open()调用创建消息队列成功之后才会true,所以我们认为,三个功能调用顺序是如下: 
调用open函数创建消息队列 
调用enableSensorLocked()函数对sensor使能 
调用sensors_data_poll从消息队列中读取数据,而且是在while (true)循环里一直读取 
我们首先来分析服务端的实现 
启动SensorService服务 
在SystemServer进程中的main函数中,通过JNI调用,调用到 
com_android_server_SystemServer.cpp的android_server_SystemServer_init1()方法,该方法又调用system_init.cpp中的system_init():

extern "C" status_t system_init()
{
    ……
    property_get("system_init.startsensorservice", propBuf, "1");
    if (strcmp(propBuf, "1") == 0) {
        // Start the sensor service
        SensorService::instantiate();
    }
    …..
    return NO_ERROR;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里创建了SensorService的实例。 
SensorService初始化 
SensorService创建完之后,将会调用SensorService::onFirstRef()方法,在该方法中完成初始化工作。 
首先获取SensorDevice实例,在其构造函数中,完成了对Sensor模块HAL的初始化:

SensorDevice::SensorDevice()
    :  mSensorDevice(0),
       mSensorModule(0)
{
    status_t err = hw_get_module(SENSORS_HARDWARE_MODULE_ID,
            (hw_module_t const**)&mSensorModule);

    if (mSensorModule) {
        err = sensors_open(&mSensorModule->common, &mSensorDevice);

        ALOGE_IF(err, "couldn't open device for module %s (%s)",
                SENSORS_HARDWARE_MODULE_ID, strerror(-err));

        if (mSensorDevice) {
            sensor_t const* list;
            ssize_t count = mSensorModule->get_sensors_list(mSensorModule, &list);
            mActivationCount.setCapacity(count);
            Info model;
            for (size_t i=0 ; i<size_t(count) ; i++) {
                mActivationCount.add(list[i].handle, model);
                mSensorDevice->activate(mSensorDevice, list[i].handle, 0);
            }
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

这里主要做了三个工作: 
调用HAL层的hw_get_modele()方法,加载Sensor模块so文件 
调用sensor.h的sensors_open方法打开设备 
调用sensors_poll_device_t->activate()对Sensor模块使能

再来看看SensorService::onFirstRef()方法:

void SensorService::onFirstRef()
{
    SensorDevice& dev(SensorDevice::getInstance());

    if (dev.initCheck() == NO_ERROR) {
        sensor_t const* list;
        ssize_t count = dev.getSensorList(&list);
        if (count > 0) {
            ……
            for (ssize_t i=0 ; i<count ; i++) {
                registerSensor( new HardwareSensor(list[i]) );
                ……
            }

            // it's safe to instantiate the SensorFusion object here
            // (it wants to be instantiated after h/w sensors have been
            // registered)
            const SensorFusion& fusion(SensorFusion::getInstance());

            if (hasGyro) {
               ……
            }
            ……
            run("SensorService", PRIORITY_URGENT_DISPLAY);
            mInitCheck = NO_ERROR;
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在这个方法中,主要做了4件事情: 
创建SensorDevice实例 
获取Sensor列表 
调用SensorDevice.getSensorList(),获取Sensor模块所有传感器列表 
为每个传感器注册监听器

registerSensor( new HardwareSensor(list[i]) );
void SensorService::registerSensor(SensorInterface* s)
{
    sensors_event_t event;
    memset(&event, 0, sizeof(event));

    const Sensor sensor(s->getSensor());
    // 添加到Sensor列表,给客户端使用
    mSensorList.add(sensor);
    // add to our handle->SensorInterface mapping
    mSensorMap.add(sensor.getHandle(), s);
    // create an entry in the mLastEventSeen array
    mLastEventSeen.add(sensor.getHandle(), event);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

HardwareSensor实现了SensorInterface接口。 
启动线程读取数据 
调用run方法启动新线程,将调用SensorService::threadLoop()方法。 
在新的线程中读取HAL层数据 
SensorService实现了Thread类,当在onFirstRef中调用run方法的后,将在新的线程中调用SensorService::threadLoop()方法。

bool SensorService::threadLoop()
{
    ……
    do {
        count = device.poll(buffer, numEventMax);

        recordLastValue(buffer, count);
        ……

        // send our events to clients...
        const SortedVector< wp<SensorEventConnection> > activeConnections(
                getActiveConnections());
        size_t numConnections = activeConnections.size();
        for (size_t i=0 ; i<numConnections ; i++) {
            sp<SensorEventConnection> connection(
                    activeConnections[i].promote());
            if (connection != 0) {
                connection->sendEvents(buffer, count, scratch);
            }
        }
    } while (count >= 0 || Thread::exitPending());
    return false;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在while循环中一直读取HAL层数据,再调用SensorEventConnection->sendEvents将数据写到管道中。 
客户端与服务端通信 
数据传送 
客户端与服务端通信的状态图
客户端服务端线程 
在图中我们可以看到有两个线程,一个是服务端的一个线程,这个线程负责源源不断的从HAL读取数据。另一个是客户端的一个线程,客户端线程负责从消息队列中读数据。 
创建消息队列 
客户端可以创建多个消息队列,一个消息队列对应有一个与服务器通信的连接接口 
创建连接接口 
服务端与客户端沟通的桥梁,服务端读取到HAL层数据后,会扫面有多少个与客户端连接的接口,然后往每个接口的管道中写数据 
创建管道 
每一个连接接口都有对应的一个管道。 
上面是设计者设计数据传送的原理,但是目前Android4.1上面的数据传送不能完全按照上面的理解。因为在实际使用中,消息队列只会创建一个,也就是说客户端与服务端之间的通信只有一个连接接口,只有一个管道传数据。那么数据的形式是怎么从HAL层传到JAVA层的呢?其实数据是以一个结构体sensors_event_t的形式从HAL层传到JNI层。看看HAL的sensors_event_t结构体:

typedef struct sensors_event_t {
    int32_t version;
    int32_t sensor;            //标识符
    int32_t type;             //传感器类型
    int32_t reserved0;
    int64_t timestamp;        //时间戳
    union {
        float           data[16];
        sensors_vec_t   acceleration;   //加速度
        sensors_vec_t   magnetic;      //磁矢量
        sensors_vec_t   orientation;     //方向
        sensors_vec_t   gyro;          //陀螺仪
        float           temperature;     //温度
        float           distance;        //距离
        float           light;           //光照
        float           pressure;         //压力
        float           relative_humidity;  //相对湿度
    };
    uint32_t        reserved1[4];
} sensors_event_t;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在JNI层有一个ASensorEvent结构体与sensors_event_t向对应, 
frameworks/native/include/Android/sensor.h:

typedef struct ASensorEvent {
    int32_t version;
    int32_t sensor;
    int32_t type;
    int32_t reserved0;
    int64_t timestamp;
    union {
        float           data[16];
        ASensorVector   vector;
        ASensorVector   acceleration;
        ASensorVector   magnetic;
        float           temperature;
        float           distance;
        float           light;
        float           pressure;
    };
    int32_t reserved1[4];
} ASensorEvent;
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在JNI层,只会将结构体数据中一部分的信息传到JAVA层:

经过前面的介绍,我们知道了客户端实现的方式及服务端的实现,但是没有具体讲到它两是如何进行通信的,这节我们专门介绍客户端与服务端之间的通信。 
这里主要涉及的是进程间通信,有IBind和管道通信。客户端通过IBind通信获取到服务端的远程调用,然后通过管道进行sensor数据的传输。 
管道是Linux 支持的最初Unix IPC形式之一,具有以下特点: 
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。 
服务端 
native层实现了sensor服务的核心实现,Sensor服务的主要流程的实现在sensorservice类中,下面重点分析下这个类的流程。

class SensorService :
        public BinderService<SensorService>,
        public BnSensorServer,
        protected Thread
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

看看sensorService继承的类: 
继承BinderService这个模板类添加到系统服务,用于Ibinder进程间通信。

template<typename SERVICE>
class BinderService
{
public:
    static status_t publish() {
        sp<IServiceManager> sm(defaultServiceManager());
        return sm->addService(String16(SERVICE::getServiceName()), new SERVICE());
    }

    static void publishAndJoinThreadPool() {
        sp<ProcessState> proc(ProcessState::self());
        sp<IServiceManager> sm(defaultServiceManager());
        sm->addService(String16(SERVICE::getServiceName()), new SERVICE());
        ProcessState::self()->startThreadPool();
        IPCThreadState::self()->joinThreadPool();
    }

    static void instantiate() { publish(); }
};
}; // namespace android
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在前面的介绍中,SensorService服务的实例是在System_init.cpp中调用SensorService::instantiate()创建的,即调用了上面的instantiate()方法,接着调用了publish(),在该方法中,我们看到了new SensorService的实例,并且调用了defaultServiceManager::addService()将Sensor服务添加到了系统服务管理中,客户端可以通过defaultServiceManager:getService()获取到Sensor服务的实例。

继承BnSensorServer这个是sensor服务抽象接口类提供给客户端调用:

class Sensor;
class ISensorEventConnection;

class ISensorServer : public IInterface
{
public:
    DECLARE_META_INTERFACE(SensorServer);
    //获取Sensor列表
virtual Vector<Sensor> getSensorList() = 0;
//创建一个连接的接口,这些都是提供给客户端的抽象接口,服务端实例化时候必须实现
    virtual sp<ISensorEventConnection> createSensorEventConnection() = 0;
};
class BnSensorServer : public BnInterface<ISensorServer>
{
public:
    //传输打包数据的通讯接口,在BnSensorServer被实现
    virtual status_t    onTransact( uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags = 0);
};
}; // namespace android
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

ISensorServer接口提供了两个抽象方法给客户端调用,关键在于 
createSensorEventConnection()方法,该在服务端被实现,在客户端被调用,并返回一个SensorEventConnection的实例,创建连接,客户端拿到SensorEventConnection实例之后,可以对sensor进行通信操作,仅仅作为通信的接口而已,它并没有用来传送Sensor数据,因为Sensor数据量比较打,IBind实现比较困难。真正实现Sensor数据传送的是管道,在创建SensorEventConnection实例中,创建了BitTube对象,里面创建了管道,用于客户端与服务端的通信。 
客户端 
客户端主要在SensorManager.cpp中创建消息队列

class ISensorEventConnection;
class Sensor;
class Looper;

// ----------------------------------------------------------------------------

class SensorEventQueue : public ASensorEventQueue, public RefBase
{
public:
            SensorEventQueue(const sp<ISensorEventConnection>& connection);
    virtual ~SensorEventQueue();
    virtual void onFirstRef();
    //获取管道句柄
    int getFd() const;
    //向管道写数据
    static ssize_t write(const sp<BitTube>& tube,
            ASensorEvent const* events, size_t numEvents);
    //向管道读数据
    ssize_t read(ASensorEvent* events, size_t numEvents);

    status_t waitForEvent() const;
    status_t wake() const;
    //使能Sensor传感器
    status_t enableSensor(Sensor const* sensor) const;
    status_t disableSensor(Sensor const* sensor) const;
    status_t setEventRate(Sensor const* sensor, nsecs_t ns) const;

    // these are here only to support SensorManager.java
    status_t enableSensor(int32_t handle, int32_t us) const;
    status_t disableSensor(int32_t handle) const;

private:
sp<Looper> getLooper() const;
//连接接口,在SensorService中创建的
sp<ISensorEventConnection> mSensorEventConnection;
//管道指针
    sp<BitTube> mSensorChannel;
    mutable Mutex mLock;
    mutable sp<Looper> mLooper;
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

SensorEventQueue类作为消息队列,作用非常重要,在创建其实例的时候,传入了SensorEventConnection的实例,SensorEventConnection继承于ISensorEventConnection。SensorEventConnection其实是客户端调用SensorService的createSensorEventConnection()方法创建的,它是客户端与服务端沟通的桥梁,通过这个桥梁,可以完成一下任务: 
获取管道的句柄 
往管道读写数据 
通知服务端对Sensor使能 
流程解析 
客户端获取SensorService服务实例 
客户端初始化的时候,即SystemSensorManager的构造函数中,通过JNI调用,创建native层SensorManager的实例,然后调用SensorManager::assertStateLocked()方法做一些初始化的动作。

status_t SensorManager::assertStateLocked() const {
    if (mSensorServer == NULL) {
        // try for one second
        const String16 name("sensorservice");
        ……
            status_t err = getService(name, &mSensorServer);
        ……
        mSensors = mSensorServer->getSensorList();
        size_t count = mSensors.size();
        mSensorList = (Sensor const**)malloc(count * sizeof(Sensor*));
        for (size_t i=0 ; i<count ; i++) {
            mSensorList[i] = mSensors.array() + i;
        }
    }
    return NO_ERROR;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

前面我们讲到过,SensorService的创建的时候调用了defaultServiceManager:getService()将服务添加到了系统服务管理中。现在我们又调用defaultServiceManager::geService()获取到SensorService服务的实例。在通过IBind通信,就可以获取到Sensor列表,所以在客户端初始化的时候,做了两件事情: 
n 获取SensorService实例引用 
n 获取Sensor传感器列表 
创建消息队列 
当客户端第一次注册监听器的时候,就需要创建一个消息队列,也就是说,android在目前的实现中,只创建了一个消息队列,一个消息队列中有一个管道,用于服务端与客户断传送Sensor数据。 
在SensorManager.cpp中的createEventQueue方法创建消息队列:

sp<SensorEventQueue> SensorManager::createEventQueue()
{
    sp<SensorEventQueue> queue;
    Mutex::Autolock _l(mLock);
while (assertStateLocked() == NO_ERROR) {
    //创建连接接口
        sp<ISensorEventConnection> connection =
                mSensorServer->createSensorEventConnection();
        if (connection == NULL) {
            // SensorService just died.
            LOGE("createEventQueue: connection is NULL. SensorService died.");
            continue;
        }
//创建消息队列
        queue = new SensorEventQueue(connection);
        break;
    }
    return queue;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

客户端与服务器创建一个SensorEventConnection连接接口,而一个消息队列中包含一个连接接口。 
创建连接接口:

sp<ISensorEventConnection> SensorService::createSensorEventConnection()
{
    sp<SensorEventConnection> result(new SensorEventConnection(this));
    return result;
}
SensorService::SensorEventConnection::SensorEventConnection(
        const sp<SensorService>& service)
    : mService(service), mChannel(new BitTube ())
{
}
关键在于BitTube,在构造函数中创建了管道:
BitTube::BitTube()
    : mSendFd(-1), mReceiveFd(-1)
{
    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) == 0) {
        int size = SOCKET_BUFFER_SIZE;
        setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
        setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
        setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
        setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
        fcntl(sockets[0], F_SETFL, O_NONBLOCK);
        fcntl(sockets[1], F_SETFL, O_NONBLOCK);
        mReceiveFd = sockets[0];
        mSendFd = sockets[1];
    } else {
        mReceiveFd = -errno;
        ALOGE("BitTube: pipe creation failed (%s)", strerror(-mReceiveFd));
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

其中:fds[0]就是对应的mReceiveFd,是管道的读端,sensor数据的读取端,对应的是客户端进程访问的。fds[1]就是对应mSendFd,是管道的写端,sensor数据写入端,是sensor的服务进程访问的一端。通过pipe(fds)创建管道,通过fcntl来设置操作管道的方式,设置通道两端的操作方式为O_NONBLOCK ,非阻塞IO方式,read或write调用返回-1和EAGAIN错误。 
总结下消息队列: 
客户端第一次注册监听器的时候,就需要创建一个消息队列,客户端创了SensorThread线程从消息队列里面读取数据。 
SensorEventQueue中有一个SensorEventConnection实例的引用,SensorEventConnection中有一个BitTube实例的引用。 
使能Sensor 
客户端创建了连接接口SensorEventConnection后,可以调用其方法使能Sensor传感器:

status_t SensorService::SensorEventConnection::enableDisable(
        int handle, bool enabled)
{
    status_t err;
    if (enabled) {
        err = mService->enable(this, handle);
    } else {
        err = mService->disable(this, handle);
    }
    return err;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

handle对应着Sensor传感器的句柄 
服务端往管道写数据

bool SensorService::threadLoop()
{
    ……
    do {
        count = device.poll(buffer, numEventMax);

        recordLastValue(buffer, count);
        ……

        // send our events to clients...
        const SortedVector< wp<SensorEventConnection> > activeConnections(
                getActiveConnections());
        size_t numConnections = activeConnections.size();
        for (size_t i=0 ; i<numConnections ; i++) {
            sp<SensorEventConnection> connection(
                    activeConnections[i].promote());
            if (connection != 0) {
                connection->sendEvents(buffer, count, scratch);
            }
        }
    } while (count >= 0 || Thread::exitPending());
    return false;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

前面介绍过,在SensorService中,创建了一个线程不断从HAL层读取Sensor数据,就是在threadLoop方法中。关键在与下面了一个for循环,其实是扫描有多少个客户端连接接口,然后就往没每个连接的管道中写数据。

status_t SensorService::SensorEventConnection::sendEvents(
        sensors_event_t const* buffer, size_t numEvents,
        sensors_event_t* scratch)
{
    // filter out events not for this connection
    size_t count = 0;
    if (scratch) {
      ……
    }
    ……
    if (count == 0)
        return 0;

    ssize_t size = mChannel->write(scratch, count*sizeof(sensors_event_t));
    ……
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

调用该连接接口的BitTube::write():

ssize_t BitTube::write(void const* vaddr, size_t size)
{
    ssize_t err, len;
    do {
        len = ::send(mSendFd, vaddr, size, MSG_DONTWAIT | MSG_NOSIGNAL);
        err = len < 0 ? errno : 0;
    } while (err == EINTR);
    return err == 0 ? len : -err;

} }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

到此,服务端就完成了往管道的写端写入数据。 
客户端读管道数据

ssize_t SensorEventQueue::read(ASensorEvent* events, size_t numEvents)
{
    return BitTube::recvObjects(mSensorChannel, events, numEvents);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

调用到了BitTube::read():

static ssize_t recvObjects(const sp<BitTube>& tube,
            T* events, size_t count) {
        return recvObjects(tube, events, count, sizeof(T));
    }
ssize_t BitTube::recvObjects(const sp<BitTube>& tube,
        void* events, size_t count, size_t objSize)
{
    ssize_t numObjects = 0;
    for (size_t i=0 ; i<count ; i++) {
        char* vaddr = reinterpret_cast<char*>(events) + objSize * i;
        ssize_t size = tube->read(vaddr, objSize);
        if (size < 0) {
            // error occurred
            return size;
        } else if (size == 0) {
            // no more messages
            break;
        }
        numObjects++;
    }
    return numObjects;
}

ssize_t BitTube::read(void* vaddr, size_t size)
{
    ssize_t err, len;
    do {
        len = ::recv(mReceiveFd, vaddr, size, MSG_DONTWAIT);
        err = len < 0 ? errno : 0;
    } while (err == EINTR);
    if (err == EAGAIN || err == EWOULDBLOCK) {
        return 0;
    }
    return err == 0 ? len : -err;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值