Android bluetooth start discovery analyse

一. start_discovery的上层一系列的调用
1. 界面上的"search for device"
在./device/softwinner/common/packages/TvdSettings/src/com/android/settings/bluetooth/BluetoothSettings.java中
当点击“searching devices"或者打开时就会去扫描蓝牙设备
  1. public boolean onOptionsItemSelected(MenuItem item) {
  2.         switch (item.getItemId()) {
  3.             case MENU_ID_SCAN:
  4.                 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) {
  5.                     startScanning();
  6.                 }
  7.                 return true;
  8. }
在device/softwinner/common/packages/TvdSettings/src/com/android/settings/bluetooth/BluetoothSettings.java中
  1. private void startScanning() {
  2.         if (!mAvailableDevicesCategoryIsPresent) {
  3.             getPreferenceScreen().addPreference(mAvailableDevicesCategory);
  4.         }
  5.         mLocalAdapter.startScanning(true);
  6.     }
在./device/softwinner/common/packages/TvdSettings/src/com/android/settings/bluetooth/LocalBluetoothAdapter.java中
  1. void startScanning(boolean force) {
  2.         if (!mAdapter.isDiscovering()) {
  3.             if (!force) {
  4.                 if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) {
  5.                     return;
  6.                 }
  7.                 A2dpProfile a2dp = mProfileManager.getA2dpProfile();
  8.                 if (a2dp != null && a2dp.isA2dpPlaying()) {
  9.                     return;
  10.                 }
  11.             }

  12.             if (mAdapter.startDiscovery()) {
  13.                 mLastScan = System.currentTimeMillis();
  14.             }
  15.         }
  16.     }
2. frameworks中的调用
在frameworks/base/core/java/android/bluetooth/BluetoothAdapter.java中
  1. public boolean startDiscovery() {
  2.         if (getState() != STATE_ON) return false;
  3.         try {
  4.             synchronized(mManagerCallback) {
  5.                 if (mService != null) return mService.startDiscovery();
  6.             }
  7.         } catch (RemoteException e) {Log.e(TAG, "", e);} 
  8.         return false;
  9.     }
mService 用binder调用 startDiscovery
3. packages中的调用
在./packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java中
  1. private static class AdapterServiceBinder extends IBluetooth.Stub {
  2. public boolean startDiscovery() {
  3.             if (!Utils.checkCaller()) {
  4.                 Log.w(TAG,"startDiscovery(): not allowed for non-active user");
  5.                 return false;
  6.             }

  7.             AdapterService service = getService();
  8.             if (service == null) return false;
  9.             return service.startDiscovery();
  10.         }
  11. }
3.1 server的
在./packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java中
  1. public class AdapterService extends Service {
  2. boolean startDiscovery() {
  3.         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, “Need BLUETOOTH ADMIN permission");
  4.         return startDiscoveryNative();
  5.     }
  6. }
会调用jni的startDiscoveryNative
4. jni中的调用
在./packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp中
  1. static jboolean startDiscoveryNative(JNIEnv* env, jobject obj) {
  2.     ALOGV("%s:",__FUNCTION__);

  3.     jboolean result = JNI_FALSE;
  4.     if (!sBluetoothInterface) return result;

  5.     int ret = sBluetoothInterface->start_discovery();
  6.     result = (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
  7.     return result;
  8. }
sBluetoothInterface是bluetooth.default.so的接口,即协议栈的接口


二.协议栈中的start_discovery
到了协议栈了,协议栈是在./external/bluetooth/bluedroid/中
2.1在./btif/src/bluetooth.c中
  1. static int start_discovery(void)
  2. {
  3.     /* sanity check */
  4.     if (interface_ready() == FALSE)
  5.         return BT_STATUS_NOT_READY;

  6.     return btif_dm_start_discovery();
  7. }
2.2在btif/src/btif_dm.c中
  1. bt_status_t btif_dm_start_discovery(void)
  2. {
  3.     tBTA_DM_INQ inq_params;
  4.     tBTA_SERVICE_MASK services = 0;
  5.     inq_params.mode = BTA_DM_GENERAL_INQUIRY;
  6.     inq_params.duration = BTIF_DM_DEFAULT_INQ_MAX_DURATION;
  7.     inq_params.max_resps = BTIF_DM_DEFAULT_INQ_MAX_RESULTS;
  8.     inq_params.report_dup = TRUE;
  9.     inq_params.filter_type = BTA_DM_INQ_CLR;
  10.     btif_dm_inquiry_in_progress = FALSE;
  11.     BTA_DmSearch(&inq_params, services, bte_search_devices_evt);
  12.     return BT_STATUS_SUCCESS;
  13. }
2.3在bta/dm/bta_dm_api.c中
  1. void BTA_DmSearch(tBTA_DM_INQ *p_dm_inq, tBTA_SERVICE_MASK services, tBTA_DM_SEARCH_CBACK *p_cback)
  2. {
  3.     tBTA_DM_API_SEARCH *p_msg;
  4.     if ((p_msg = (tBTA_DM_API_SEARCH *) GKI_getbuf(sizeof(tBTA_DM_API_SEARCH))) != NULL)
  5.     {
  6.         memset(p_msg, 0, sizeof(tBTA_DM_API_SEARCH));
  7.         p_msg->hdr.event = BTA_DM_API_SEARCH_EVT;
  8.         memcpy(&p_msg->inq_params, p_dm_inq, sizeof(tBTA_DM_INQ));  //将这个inq复制到了inq_params中
  9.         p_msg->services = services;
  10.         p_msg->p_cback = p_cback;
  11.         p_msg->rs_res = BTA_DM_RS_NONE;
  12.         bta_sys_sendmsg(p_msg);
  13.     }
  14. }
其作用是调用bta_sys_sendmsg是向btu_task发送一个命令:p_msg->hdr.event = BTA_DM_API_SEARCH_EVT,参数是:inq_params
2.4 在stack/btu/btu_task.c中
  1. BTU_API UINT32 btu_task (UINT32 param)
  2. {
  3.     event = GKI_wait (0xFFFF, 0);  //从GKI_wait返回的event是4,这个不重要不管它了
  4.     if (event & TASK_MBOX_2_EVT_MASK)
  5.         while ((p_msg = (BT_HDR *) GKI_read_mbox(TASK_MBOX_2)) != NULL) //从这里面读取的才是真正发送的msg
  6.            bta_sys_event(p_msg);   //进行msg的处理,这里的msg才是BTA_DmSearch中发送的msg
  7. }
2.5在bta/sys/bta_sys_main.c中
  1. BTA_API void bta_sys_event(BT_HDR *p_msg)
  2. {
  3.     id = (UINT8) (p_msg->event >> 8);
  4.     if ((id < BTA_ID_MAX) && (bta_sys_cb.reg[id] != NULL))
  5.     {
  6.         freebuf = (*bta_sys_cb.reg[id]->evt_hdlr)(p_msg);
  7.     }
  8. }
又一个函数指针数组,那么这个evt_hdlr是在哪个地方初始化的呢?
2.6.evt_hdlr的初始化
bta/dm/bta_dm_api.c
  1. static const tBTA_SYS_REG bta_dm_search_reg =
  2. {
  3.     bta_dm_search_sm_execute,
  4.     bta_dm_search_sm_disable
  5. };

  6. tBTA_STATUS BTA_EnableBluetooth(tBTA_DM_SEC_CBACK *p_cback)
  7. {
  8.     bta_sys_register (BTA_ID_DM_SEARCH, &bta_dm_search_reg );
  9. }
下面就进入函数bta_dm_search_sm_execute
2.7 在bta/dm/bta_dm_main.c中,此时的msg还是BTA_DmSearch中发送的msg
  1. BOOLEAN bta_dm_search_sm_execute(BT_HDR *p_msg)
  2. {
  3.     tBTA_DM_ST_TBL state_table;
  4.     UINT8 action;
  5.     int i;
  6.     state_table = bta_dm_search_st_tbl[bta_dm_search_cb.state];
  7.     bta_dm_search_cb.state = state_table[p_msg->event & 0x00ff][BTA_DM_SEARCH_NEXT_STATE];

  8.     for (= 0; i < BTA_DM_SEARCH_ACTIONS; i++)
  9.     {
  10.         if ((action = state_table[p_msg->event & 0x00ff][i]) != BTA_DM_SEARCH_IGNORE)
  11.         {
  12.             (*bta_dm_search_action[action])( (tBTA_DM_MSG*) p_msg); //为什么要搞得这么复杂?
  13.         }
  14.         else
  15.         {
  16.             break;
  17.         }
  18.     }
  19.     return TRUE;
  20. }
这个函数指针数组看起来很复杂,但实际执行起来就是调用了bta_dm_search_action[0],
即: bta_dm_search_start
2.8 在bta/dm/bta_dm_act.c中
  1. void bta_dm_search_start (tBTA_DM_MSG *p_data)
  2. {
  3.     bta_dm_search_cb.p_search_cback = p_data->search.p_cback;
  4.     bta_dm_search_cb.services = p_data->search.services;

  5.     result.status = BTM_StartInquiry( (tBTM_INQ_PARMS*)&p_data->search.inq_params,
  6.                         bta_dm_inq_results_cb,
  7.                         (tBTM_CMPL_CB*) bta_dm_inq_cmpl_cb);

  8.     if (result.status != BTM_CMD_STARTED)
  9.     {
  10.         result.num_resp = 0;
  11.         bta_dm_inq_cmpl_cb ((void *)&result);
  12.     }
  13. }
a.发送的msg是一个tBTA_DM_API_SEARCH结构体,
而tBTA_DM_API_SEARCH结构体又属于 tBTA_DM_MSG (enum)类型的search,
所以这儿就用了 p_data - > search 来代替msg,但这个msg所指的内容,还是 BTA_DmSearch 所发送的msg
b. 同时BTM_StartInquiry 中的第一个参数中inq_params,也就是 btif_dm_start_discovery 发送的参数
2.9 在stack/btm/btm_inq.c中
  1. tBTM_STATUS BTM_StartInquiry (tBTM_INQ_PARMS *p_inqparms, tBTM_INQ_RESULTS_CB *p_results_cb,
  2.                               tBTM_CMPL_CB *p_cmpl_cb)
  3. {
  4.     tBTM_STATUS status;
  5.     tBTM_INQUIRY_VAR_ST *p_inq = &btm_cb.btm_inq_vars;
  6.  
  7.     if (p_inq->inq_active || p_inq->inqfilt_active)
  8.         return (BTM_BUSY);

  9.     if (!BTM_IsDeviceUp())
  10.         return (BTM_WRONG_MODE);

  11.     if ((p_inqparms->mode & BTM_BR_INQUIRY_MASK)!= BTM_GENERAL_INQUIRY &&
  12.         (p_inqparms->mode & BTM_BR_INQUIRY_MASK)!= BTM_LIMITED_INQUIRY)
  13.         return (BTM_ILLEGAL_VALUE);

  14.     p_inq->inqparms = *p_inqparms;
  15.     p_inq->inqparms.mode = (UINT8)(<< (p_inqparms->mode & BTM_BR_INQUIRY_MASK));
  16.     p_inq->state = BTM_INQ_ACTIVE_STATE;
  17.     p_inq->p_inq_cmpl_cb = p_cmpl_cb;
  18.     p_inq->p_inq_results_cb = p_results_cb;
  19.     p_inq->inq_cmpl_info.num_resp = 0; /* Clear the results counter */
  20.     p_inq->inq_active = (UINT8)(<< (p_inqparms->mode & BTM_BR_INQUIRY_MASK));

  21.     switch (p_inqparms->filter_cond_type)
  22.     {
  23.     case BTM_CLR_INQUIRY_FILTER:
  24.         p_inq->state = BTM_INQ_SET_FILT_STATE;
  25.         break;

  26.     case BTM_FILTER_COND_DEVICE_CLASS:
  27.     case BTM_FILTER_COND_BD_ADDR:
  28.         p_inq->state = BTM_INQ_CLR_FILT_STATE;
  29.         p_inqparms->filter_cond_type = BTM_CLR_INQUIRY_FILTER;
  30.         break;

  31.     default:
  32.         return (BTM_ILLEGAL_VALUE);
  33.     }

  34.     if ((status = btm_set_inq_event_filter (p_inqparms->filter_cond_type, &p_inqparms->filter_cond)) != BTM_CMD_STARTED)
  35.         p_inq->state = BTM_INQ_INACTIVE_STATE;
  36.     return (status);
  37. }
3.0 与/dev/ttyS0进行通信
btm_set_inq_event_filter
    -->btsnd_hcic_set_event_filter
        -->btu_hcif_send_cmd
            --> HCI_CMD_TO_LOWER  
            --> bte_main_hci_send    // 发送了一个msg
     --> bt_hc_worker_thread    //接收者
        --> hci_h4_send_msg     //向/dev/ttyS2写入
  1. void hci_h4_send_msg(HC_BT_HDR *p_msg)
  2. { 
  3.     bytes_to_send = p_msg->len + 1;
  4.     //下面就是向/dev/ttyS2写入msg 
  5.     bytes_sent = userial_write(event,(uint8_t *) p, bytes_to_send);
  6.     return;
  7. }
当/dev/ttyS2有数据返回时,其监听线程会从select_read中返回
在hci/src/userial.c中
  1. static void *userial_read_thread(void *arg)
  2. {
  3.     while (userial_running)
  4.     {
  5.        rx_length = select_read(userial_cb.fd, p, READ_LIMIT);
  6.         if (rx_length > 0)
  7.         {
  8.             p_buf->len = (uint16_t)rx_length;
  9.             utils_enqueue(&(userial_cb.rx_q), p_buf);
  10.             bthc_signal_event(HC_EVENT_RX);
  11.         }        
  12.     }   
  13.     return NULL; // Compiler friendly
  14. }
 select_read不仅监听,当有数据时还会读取数据,当数据读取后会返回,
并用信号HC_EVENT_RX唤醒线程bt_hc_worker_thread
在hci/src/bt_hci_bdroid.c中
  1. static void *bt_hc_worker_thread(void *arg)
  2. { 
  3.     if (events & HC_EVENT_RX)
  4.        p_hci_if->rcv();
  5. }
又会调用hci/src/hci_h4.c中的hci_h4_receive_msg
当hci_h4_receive_msg中接收完所有的msg之后
  1. uint16_t hci_h4_receive_msg(void)
  2. {
  3.     if (msg_received)
  4.     {
  5.         if ((bt_hc_cbacks) && (intercepted == FALSE))
  6.          bt_hc_cbacks->data_ind((TRANSAC) p_cb->p_rcv_msg, \
  7.          (char *) (p_cb->p_rcv_msg + 1), p_cb->p_rcv_msg->len + BT_HC_HDR_SIZE);
  8.     }
  9. }
这个bt_hc_cbacks->data_ind就是调用main/bte_main.c中的data_ind
而main/bte_main.c中的data_ind就是发送了一个GKI_send_msg给BTU_TASK
在stack/./btu/btu_task.c中btu_task
btu_task收到了这个BT_EVT_TO_BTU_HCI_EVT,然后会调用
stack/./btu/btu_hcif.c:btu_hcif_process_event
    --> btu_hcif_command_status_evt   //当收到状态改变消息时会调用这个
    --> btu_hcif_inquiry_rssi_result_evt  //当查找到设备时会调用这个(buf中是以字符串形式存着:mac地址,类型等信息)
        --> btm_process_inq_results      //解析字符串,分析出bd_addr, dev_class等信息
            --> bta_dm_inq_results_cb     //bta/dm/bta_dm_act.c
                --> BTM_InqDbRead          //stack/btm/btm_inq.c 在记录的database中查看这个mac的记录是否存在
                --> 回调函数bte_search_devices_evt //这个就是在刚开始 btif_dm_start_discovery 时传来的回调函数           
在main/../btif/src/btif_dm.c中
  1. static void bte_search_devices_evt(tBTA_DM_SEARCH_EVT event, tBTA_DM_SEARCH *p_data)
  2. {
  3.     btif_transfer_context (btif_dm_search_devices_evt , (UINT16) event, (void *)p_data, param_len,
  4.         (param_len > sizeof(tBTA_DM_SEARCH)) ? search_devices_copy_cb : NULL);
  5. }
在./btif/src/btif_dm.c中是回调函数
  1. static void btif_dm_search_devices_evt (UINT16 event, char *p_param)
  2. {
  3.     btif_storage_add_remote_device(&bdaddr, num_properties, properties);
  4.     HAL_CBACK(bt_hal_cbacks, device_found_cb, num_properties, properties);
  5. }
把数据保存在cfg中,但这时候还不会写入/data/misc/bluedroid/bt_config.xml中,当与设备进行connect之后才会写入
最后调用HAL_CBACK,即调用./packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp中的函数,
./packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp又会调用java层的方法。
然后在CachedBluetoothDevice中添加了一个设备




附一: 与上层的交互
btif_dm_cb_create_bond时,会有状态变化:由原先的BT_BOND_STATE_NONE变为BT_BOND_STATE_BONDING
此时会调用bond_state_changed,上层就会收到bond_state_changed的消息,下面详细分析一下:
1.1 在btif/src/btif_dm.c中
    bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_BONDING);
  1. static void bond_state_changed(bt_status_t status, bt_bdaddr_t *bd_addr, bt_bond_state_t state)
  2. {
  3.     HAL_CBACK(bt_hal_cbacks, bond_state_changed_cb, status, bd_addr, state);
  4. }
1.2 其中HAL_CBACK是一个宏
  1. #define HAL_CBACK(P_CB, P_CBACK, ...)\
  2.     if (P_CB && P_CB->P_CBACK) { \
  3.         P_CB->P_CBACK(__VA_ARGS__); \
  4.     } \
  5.     else { \
  6.         ASSERTC(0, "Callback is NULL", 0); \
  7.     }
这儿展开后: bt_hal_cbacks ->bond_state_changed_cb (status, bd_addr, state);
其中status=BT_STATUS_SUCCESS,说明通知上层状态改变成功
bd_addr: 要pair的设备mac地址
state: 改变后的状态BT_BOND_STATE_BONDING
1.3 bt_hal_cbacks 是如何初始化的呢?
来到bt_hal_cbacks 的初始化函数中
在btif/src/bluetooth.c中
  1. static int init(bt_callbacks_t* callbacks )
  2. {
  3.     /* store reference to user callbacks */
  4.     bt_hal_cbacks = callbacks;
  5. }
1.4这个callbacks是如何来的呢?答案是在apk的jni层调用的
在./packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp中
  1. static bool initNative(JNIEnv* env, jobject obj) {
  2.     sJniCallbacksObj = env->NewGlobalRef(env->GetObjectField(obj, sJniCallbacksField));
  3.     if (sBluetoothInterface) {
  4.         sBluetoothInterface->init(&sBluetoothCallbacks);  
  5.     return JNI_FALSE;
  6. }
上面这个init就是调用bluetooth.default.so中的init,其中 sBluetoothCallbacks 的定义
  1. bt_callbacks_t sBluetoothCallbacks = {
  2.     sizeof(sBluetoothCallbacks),
  3.     adapter_state_change_callback,
  4.     adapter_properties_callback,
  5.     remote_device_properties_callback,
  6.     device_found_callback,
  7.     discovery_state_changed_callback,
  8.     pin_request_callback,
  9.     ssp_request_callback,
  10.     bond_state_changed_callback,
  11.     acl_state_changed_callback,
  12.     callback_thread_event,
  13. };
1.5 
在./packages/apps/Bluetooth/jni/com_android_bluetooth_btservice_AdapterService.cpp中
  1. static void bond_state_changed_callback(bt_status_t status, bt_bdaddr_t *bd_addr,
  2.                                         bt_bond_state_t state) {
  3.     jbyteArray addr;
  4.     addr = callbackEnv->NewByteArray(sizeof(bt_bdaddr_t));
  5.     
  6.     callbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte *)bd_addr);
  7.     //这儿是c调用java的方法
  8.     callbackEnv->CallVoidMethod(sJniCallbacksObj, method_bondStateChangeCallback, (jint) status, addr, (jint)state);
  9.     checkAndClearExceptionFromCallback(callbackEnv, __FUNCTION__);
  10.     callbackEnv->DeleteLocalRef(addr);
  11. }
其中在classInitNative中被初始化:
method_bondStateChangeCallback = env->GetMethodID(jniCallbackClass, "bondStateChangeCallback", "(I[BI)V");
原来这儿是调用了java的bondStateChangeCallback这个方法。

1.6 java
在./packages/apps/Bluetooth/src/com/android/bluetooth/btservice/JniCallbacks.java中
  1. void bondStateChangeCallback(int status, byte[] address, int newState) {
  2.         mBondStateMachine.bondStateChangeCallback(status, address, newState);
  3.     }
1.7 
在 ./packages/apps/Bluetooth/src/com/android/bluetooth/btservice/BondStateMachine.java中
  1. void bondStateChangeCallback(int status, byte[] address, int newState) {
  2.         BluetoothDevice device = mRemoteDevices.getDevice(address);
  3.         infoLog("bondStateChangeCallback: Status: " + status + " Address: " + device + " newState: " + newState);

  4.         Message msg = obtainMessage(BONDING_STATE_CHANGE);
  5.         msg.obj = device;

  6.         if (newState == BOND_STATE_BONDED)
  7.             msg.arg1 = BluetoothDevice.BOND_BONDED;
  8.         else if (newState == BOND_STATE_BONDING)
  9.             msg.arg1 = BluetoothDevice.BOND_BONDING;
  10.         else
  11.             msg.arg1 = BluetoothDevice.BOND_NONE;
  12.         msg.arg2 = status;

  13.         sendMessage(msg);
  14.     }
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
PROCEDURE ANALYSE() 是一种MySQL语句,用于优化表结构时辅助参考分析语句。它可以根据表中的实际数据给出一些建议。如果你有一个INT字段作为主键,但是数据量不大,PROCEDURE ANALYSE()可能会建议你将该字段的类型改为MEDIUMINT。或者如果你使用了一个VARCHAR字段,但是数据不多,它可能会建议你将其改为ENUM。但需要注意的是,这些建议只有在表中有实际数据时才会变得准确有效。而且最终决策还是要由你自己做出。 举个例子,如果你想分析表中某个字段的建议,你可以使用类似以下的语句: SELECT * FROM table_name PROCEDURE ANALYSE(max_elements, max_memory); 其中,max_elements表示每列非重复值的最大值,当超过这个值时,MySQL不会推荐使用ENUM类型。而max_memory表示为每列找出所有非重复值所占用的最大内存大小。 例如,使用上面的语句查询表中的某个字段,MySQL会给出该字段的最小值、最大值、最小长度、最大长度、平均长度等信息,并给出优化建议。这些建议是根据当前数据情况下的分析结果给出的,只有当表中的数据越来越多时,这些建议才会变得更准确可靠。因此,在使用PROCEDURE ANALYSE()的建议时,要考虑表中的实际数据量。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [使用procedure analyse()分析mysql给出的关于表结构的优化建议](https://blog.csdn.net/weixin_33672109/article/details/91769161)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Mysql优化-Procedure_Analyse优化表结构](https://blog.csdn.net/ty_hf/article/details/54895466)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值