Bluedroid中的线程介绍
来源:互联网 发布:2017年的同志网络剧 编辑:程序博客网 时间:2024/05/21 01:58
Bluedroid中的线程介绍
版权所有,转载时请注明出处
luowh0822@outlook.com
本文基于android O的代码进行分析。通过本文档,能够了解bluedroid的线程结构和协议栈架构。
线程的基本用法
osi/src/thread.cc
对posix的线程函数进行了封装
thread_t* thread_new(const char* name);bool thread_post(thread_t* thread, thread_fn func, void* context);
使用thread_new()创建一个线程,参数为线程名字的字符串。
使用thread_post()把子线程的处理函数传递给子线程。
Bluedroid封装的后的thread函数与POSIX的thread有点不同。
- thread_new不需要传入子线程的入口函数,内部会创建一个默认的入口函数,等待用户传入任务。
- thread_post传递用户真正需要的执行函数给子线程。
线程详解
struct thread_t { std::atomic_bool is_joined{false}; //是否joined pthread_t pthread; //pthread的handle pid_t tid; //线程id char name[THREAD_NAME_MAX + 1]; //线程名 reactor_t* reactor; //很重要的一个参数,循环epoll所有注册的queue,当queue中有数据就会调用相应的函数处理 fixed_queue_t* work_queue; //处理函数队列,用户调用thread_post()把处理函数及参数放到这个队列中,在reactor中处理。}; thread_t* thread_new_sized(const char* name, size_t work_queue_capacity) { thread_t* ret = static_cast<thread_t*>(osi_calloc(sizeof(thread_t))); ret->reactor = reactor_new(); ret->work_queue = fixed_queue_new(work_queue_capacity); // Start is on the stack, but we use a semaphore, so it's safe struct start_arg start; start.start_sem = semaphore_new(0); strncpy(ret->name, name, THREAD_NAME_MAX); start.thread = ret; start.error = 0; pthread_create(&ret->pthread, NULL, run_thread, &start); semaphore_wait(start.start_sem); semaphore_free(start.start_sem);}static void* run_thread(void* start_arg) { struct start_arg* start = static_cast<struct start_arg*>(start_arg); thread_t* thread = start->thread; thread->tid = gettid(); semaphore_post(start->start_sem); int fd = fixed_queue_get_dequeue_fd(thread->work_queue); void* context = thread->work_queue; reactor_object_t* work_queue_object = reactor_register(thread->reactor, fd, context, work_queue_read_cb, NULL); reactor_start(thread->reactor); ...}
- 当用户调用thread_new后,内部首先创建线程的reactor和work_queue,semaphore用来创建线程时跟父线程同步用的。
- 然后调用pthread_create(&ret->pthread, NULL, run_thread, &start)函数,创建并执行run_thread()接口函数。
- 在run_thread()函数里,调用reactor_register(),把work_queue加入到epoll的集合里。具体细节是work_queue有semaphore,而semaphore是使用eventfd实现的,eventfd可以使用epoll来监听。当有数据放到queue里面,semaphore就会改变,epoll就可以被唤醒。
- reactor_start(thread->reactor),函数里面有死循环,通过epoll,监听所有)通过reactor_register()传入的queue。当queue有数据传入,执行fixed_queue_register_dequeue()传入的对应处理函数。
创建线程后,线程会把自己内部的work_queue注册到reactor,当用户调用thread_post,传入函数指针和参数后,子线程就会处理。当创建线程后,至少会调用一次thread_post。我们可以看到btif_a2dp_sink.cc和btif_a2dp_source.cc中多次调用thread_post()的情况。以btif_a2dp_source.cc为例,当创建线程时,会把cmd_msg_queue注册到reactor中,用来接收a2dp的控制命令,另外当a2dp工作时,会设置一个周期的定时器,当超时,会调用thread_post,做音频数据编码再发送的工作,另外a2dp停止时,也会调用thread_post去停止。
thread还有另一种工作方式。用户调用fixed_queue_register_dequeue(),把外部的queue传给thread。这时候,向queue里添加元素(Event),就可以处理。后面bta_btu_thread会有详解的讲解。
bluedroid中的线程列表
在/system/bt/目录下”grep -rns thread_new *”就可以列出所有的thread。
下面列出的的是去掉测试代码中的线程后的线程列表:
另外,Android N及之前的版本有bt transport layer的线程,用来读uart的数据。在Android O的时候,这部分的工作在hidl里完成了。
主要线程分析
接下来分析bluedroid流程分析时最重要的三个线程,从下往上分别是:hci_thread, bta_btu_thread, jni_thread。
hci_thread:靠近底层传输层,完成HCI的拆包和组包工作,处理HCI命令,事件及数据包。
bta_btu_thread:实现协议栈的核心功能
jni_thread:衔接协议栈及上层JNI
jni_thread
- 上层的java service调用bluredroid接口时,切换到这个线程执行调用程序,并跟下层(协议栈核心模块,bta_btu_thread)交互
- bta_btu_thread向上发送的event,切换到这个线程处理event。
btif_core.ccbt_status_t btif_init_bluetooth() { bt_jni_workqueue_thread = thread_new(BT_JNI_WORKQUEUE_NAME); thread_post(bt_jni_workqueue_thread, run_message_loop, nullptr);}bt_status_t btif_transfer_context(tBTIF_CBACK* p_cback, uint16_t event, ...)
创建线程后,会执行run_message_loop的入口函数。这个跟Android N不同。Android O引入chrominum的MessageLoop机制。创建MessageLoop后,调用PostTask,就可以往这个线程里发任务(处理函数和数据)让这个线程处理。跟thread_post功能类似,还没想到为什么要用MessageLoop。
btif_transfer_context()是其他线程(jni上层和bta_btu_thread)需要把task切换到btif线程处理时,调用的函数。可以看到这个函数最终会调用到PostTask()。
jni_thread会调用上层初始化时注册的callback,向上层发送event.
jni_thread通过bta_sys_sendmsg()来向bta_btu_thread发送消息。实现的细节是向btu_bta_msg_queue里添加消息。bta_btu_thread会处理这个queue中的消息。
bta_btu_thread
蓝牙核心协议栈的处理都在这个线程里完成。
void BTU_StartUp(void) { btu_bta_msg_queue = fixed_queue_new(SIZE_MAX); btu_general_alarm_queue = fixed_queue_new(SIZE_MAX); bt_workqueue_thread = thread_new(BT_WORKQUEUE_NAME); thread_post(bt_workqueue_thread, btu_task_start_up, NULL);}void btu_task_start_up(UNUSED_ATTR void* context) { ... fixed_queue_register_dequeue(btu_bta_msg_queue, thread_get_reactor(bt_workqueue_thread), btu_bta_msg_ready, NULL); fixed_queue_register_dequeue(btu_hci_msg_queue, thread_get_reactor(bt_workqueue_thread), btu_hci_msg_ready, NULL); alarm_register_processing_queue(btu_general_alarm_queue, bt_workqueue_thread);}
这个线程比较简单,创建线程后,该线程就epoll三个队列。有事件后就处理,处理完后再epoll等待。
1. btu_bta_msg_queue: jni_thread发送过来的事件
2. btu_hci_msg_queue: hci发送的事件,主要是hci的event和l2cap层的数据
3. btu_general_alarm_queue: 协议栈中用到的定时器超时产生的事件
协议栈下行和上行的处理流程如下:
下行:jni_thread通过bta_sys_sendmsg()发送消息给bta_btu_thread, bta_btu_thread处理完后,最终会调用hci_layer中的enqueue_packet(),把完整的包发给hci_thread,在hci_thread会进行拆包并发送(fragment_and_dispatch())。
上行:hci_thread组成一个完整的包后,送到btu_hci_msg_queue(搜索fixed_queue_enqueue(btu_hci_msg_queue, event)), bta_btu_thread处理完成后,再通过btif_transfer_context()发送给jni_thread。
hci_thread
hci_layer.cc
hci_thread跟jni_thread类似,也是使用MessageLoop来工作的。
主要的工作是收发命令(包括命令的超时管理),接收组包和发送拆包等工作。
向下发送HCI命令时,调用enqueue_command(),切换到hci_thread处理。
向下发送数据时,调用enqueue_packet(),切换到hci_thread处理。
向上发送组包完成后的数据或Event时,会调用fixed_queue_enqueue(upwards_data_queue, packet),这个upwards_data_queue是初始化时bta_btu传进来的,其实就是hci_msg_queue。
分析问题时怎么跟协议栈的代码流程
由于多线程的存在,并且协议栈有很多的状态机,不了解代码框架的话,很容易就糊涂了。下面讲解一下怎么从jni_thread跳转到bta_btu_thread工作的。
前面介绍过的,jni_thread跳转到bta_btu_thread是通过bta_sys_sendmsg()发送event,来切换线程的。
接下来重点介绍event的定义和处理方式。
模块定义
bluedroid定义了一个system manager的模块。它把协议栈分成了很多个小模块(包括蓝牙的各个profile)。看下面的定义。
bta/sys/bta_sys.h#define BTA_ID_SYS 0 /* system manager *//* BLUETOOTH PART - from 0 to BTA_ID_BLUETOOTH_MAX */#define BTA_ID_DM 1 /* device manager */#define BTA_ID_DM_SEARCH 2 /* device manager search */#define BTA_ID_DM_SEC 3 /* device manager security */#define BTA_ID_DG 4 /* data gateway */#define BTA_ID_AG 5 /* audio gateway */#define BTA_ID_OPC 6 /* object push client */#define BTA_ID_OPS 7 /* object push server */#define BTA_ID_FTS 8 /* file transfer server */#define BTA_ID_CT 9 /* cordless telephony terminal */#define BTA_ID_FTC 10 /* file transfer client */#define BTA_ID_SS 11 /* synchronization server */#define BTA_ID_PR 12 /* Printer client */#define BTA_ID_BIC 13 /* Basic Imaging Client */#define BTA_ID_PAN 14 /* Personal Area Networking */#define BTA_ID_BIS 15 /* Basic Imaging Server */#define BTA_ID_ACC 16 /* Advanced Camera Client */#define BTA_ID_SC 17 /* SIM Card Access server */#define BTA_ID_AV 18 /* Advanced audio/video */#define BTA_ID_AVK 19 /* Audio/video sink */#define BTA_ID_HD 20 /* HID Device */#define BTA_ID_CG 21 /* Cordless Gateway */#define BTA_ID_BP 22 /* Basic Printing Client */#define BTA_ID_HH 23 /* Human Interface Device Host */#define BTA_ID_PBS 24 /* Phone Book Access Server */#define BTA_ID_PBC 25 /* Phone Book Access Client */#define BTA_ID_JV 26 /* Java */#define BTA_ID_HS 27 /* Headset */#define BTA_ID_MSE 28 /* Message Server Equipment */#define BTA_ID_MCE 29 /* Message Client Equipment */#define BTA_ID_HL 30 /* Health Device Profile*/#define BTA_ID_GATTC 31 /* GATT Client */#define BTA_ID_GATTS 32 /* GATT Client */#define BTA_ID_SDP 33 /* SDP Client */#define BTA_ID_BLUETOOTH_MAX 34 /* last BT profile *//* GENERIC */ #define BTA_ID_PRM 38 #define BTA_ID_SYSTEM 39 /* platform-specific */#define BTA_ID_SWRAP 40 /* Insight script wrapper */#define BTA_ID_MIP 41 /* Multicase Individual Polling */#define BTA_ID_RT 42 /* Audio Routing module: This module is always on. */#define BTA_ID_CLOSURE 43 /* Generic C++ closure *//* JV */#define BTA_ID_JV1 44 /* JV1 */#define BTA_ID_JV2 45 /* JV2 */#define BTA_ID_MAX (44 + BTA_DM_NUM_JV_ID)
模块内的event定义
接着,我们可以看到system manager给每个子模块划分了8bit来定义子模块的event。每个event值是模块ID向右位移8位后加上模块内部的枚举值。模块内的event定义文件在bta/xx/bta_xx_int.h中,xx代表一个子模块。 如下面的代码所示。用dm search模块来具体说明。
#define BTA_SYS_EVT_START(id) ((id) << 8) dm search模块的event跟dm模块都定义在bta/dm/bta_dm_int.h中,带API的是jni_thread发下来的请求,不带的话是从下层或本层发送的event。/* DM search events */enum { /* DM search API events */ //每个模块的event的都是从模块ID向右位移8位作为起始id。 BTA_DM_API_SEARCH_EVT = BTA_SYS_EVT_START(BTA_ID_DM_SEARCH), BTA_DM_API_SEARCH_CANCEL_EVT, BTA_DM_API_DISCOVER_EVT, BTA_DM_INQUIRY_CMPL_EVT, BTA_DM_REMT_NAME_EVT, BTA_DM_SDP_RESULT_EVT, BTA_DM_SEARCH_CMPL_EVT, BTA_DM_DISCOVERY_RESULT_EVT, BTA_DM_API_DI_DISCOVER_EVT, BTA_DM_DISC_CLOSE_TOUT_EVT};
当event发到bta_btu_thread后,system manager是怎么处理的呢?
我们接着往下看。
当event 发到bta_btu_thread后,会执行btu_bta_msg_ready(),调用bta_sys_event()。
void bta_sys_event(BT_HDR* p_msg) { uint8_t id; /* get subsystem id from event */ id = (uint8_t)(p_msg->event >> 8); /* verify id and call subsystem event handler */ if ((id < BTA_ID_MAX) && (bta_sys_cb.reg[id] != NULL)) { freebuf = (*bta_sys_cb.reg[id]->evt_hdlr)(p_msg); } else { }}
从上面的代码,我们可以看到,在bta_sys_event()里,会根据具体的event值,移位操作得到子模块的id,再根据这个id值,得到子模块注册时候传入的evt_hdlr函数,然后调用这个函数去处理。我们再以dm search模块来分析。
tBTA_STATUS BTA_EnableBluetooth(tBTA_DM_SEC_CBACK* p_cback) { bta_sys_register(BTA_ID_DM_SEARCH, &bta_dm_search_reg);}static const tBTA_SYS_REG bta_dm_search_reg = {bta_dm_search_sm_execute, bta_dm_search_sm_disable};/* registration structure */typedef struct { tBTA_SYS_EVT_HDLR* evt_hdlr; tBTA_SYS_DISABLE* disable;} tBTA_SYS_REG;bool bta_dm_search_sm_execute(BT_HDR* p_msg) { tBTA_DM_ST_TBL state_table; uint8_t action; int i; /* look up the state table for the current state */ //查找当前状态的状态转换表 state_table = bta_dm_search_st_tbl[bta_dm_search_cb.state]; //处理完成后,要跳转的状态 bta_dm_search_cb.state = state_table[p_msg->event & 0x00ff][BTA_DM_SEARCH_NEXT_STATE]; /* execute action functions */ //查找状态表中的action进行处理。多个action的话依次执行。 for (i = 0; i < BTA_DM_SEARCH_ACTIONS; i++) { action = state_table[p_msg->event & 0x00ff][i]; if (action < BTA_DM_SEARCH_IGNORE) { (*bta_dm_search_action[action])((tBTA_DM_MSG*)p_msg); } else { break; } } return true;}
从上面的代码我们可以看到,在蓝牙启动时(BTA_EnableBluetooth)会在system manager注册dm search的处理函数。从结构体的定义可以看出,bta_system_evt()用到的evt_hdlr(dm search模块)就是bta_dm_search_execute()。我们下面再分析这个函数。
在函数内部,我们可以看到p_msg->event & 0x00ff这个操作,它的作用就是去掉event中模块id的值,那么剩下来的值就是模块内部的event id了。
在bta_dm_search_sm_execute中有状态机的工作的代码。在bluedroid中会有很多这样的状态机,搞明白一个后,其他的都一样,这是非常典型的c语言状态机的实现,查找状态表的时间复杂度为O(1)。
/* state machine action enumeration list */enum { BTA_DM_API_SEARCH, /* 0 bta_dm_search_start */ BTA_DM_API_SEARCH_CANCEL, /* 1 bta_dm_search_cancel */ BTA_DM_API_DISCOVER, /* 2 bta_dm_discover */ BTA_DM_INQUIRY_CMPL, /* 3 bta_dm_inq_cmpl */ BTA_DM_REMT_NAME, /* 4 bta_dm_rmt_name */ BTA_DM_SDP_RESULT, /* 5 bta_dm_sdp_result */ BTA_DM_SEARCH_CMPL, /* 6 bta_dm_search_cmpl*/ BTA_DM_FREE_SDP_DB, /* 7 bta_dm_free_sdp_db */ BTA_DM_DISC_RESULT, /* 8 bta_dm_disc_result */ BTA_DM_SEARCH_RESULT, /* 9 bta_dm_search_result */ BTA_DM_QUEUE_SEARCH, /* 10 bta_dm_queue_search */ BTA_DM_QUEUE_DISC, /* 11 bta_dm_queue_disc */ BTA_DM_SEARCH_CLEAR_QUEUE, /* 12 bta_dm_search_clear_queue */ BTA_DM_SEARCH_CANCEL_CMPL, /* 13 bta_dm_search_cancel_cmpl */ BTA_DM_SEARCH_CANCEL_NOTIFY, /* 14 bta_dm_search_cancel_notify */ BTA_DM_SEARCH_CANCEL_TRANSAC_CMPL, /* 15 bta_dm_search_cancel_transac_cmpl */ BTA_DM_DISC_RMT_NAME, /* 16 bta_dm_disc_rmt_name */ BTA_DM_API_DI_DISCOVER, /* 17 bta_dm_di_disc */ BTA_DM_CLOSE_GATT_CONN, /* 18 bta_dm_close_gatt_conn */ BTA_DM_SEARCH_NUM_ACTIONS /* 19 */};/* action function list */const tBTA_DM_ACTION bta_dm_search_action[] = { bta_dm_search_start, /* 0 BTA_DM_API_SEARCH */ bta_dm_search_cancel, /* 1 BTA_DM_API_SEARCH_CANCEL */ bta_dm_discover, /* 2 BTA_DM_API_DISCOVER */ bta_dm_inq_cmpl, /* 3 BTA_DM_INQUIRY_CMPL */ bta_dm_rmt_name, /* 4 BTA_DM_REMT_NAME */ bta_dm_sdp_result, /* 5 BTA_DM_SDP_RESULT */ bta_dm_search_cmpl, /* 6 BTA_DM_SEARCH_CMPL */ bta_dm_free_sdp_db, /* 7 BTA_DM_FREE_SDP_DB */ bta_dm_disc_result, /* 8 BTA_DM_DISC_RESULT */ bta_dm_search_result, /* 9 BTA_DM_SEARCH_RESULT */ bta_dm_queue_search, /* 10 BTA_DM_QUEUE_SEARCH */ bta_dm_queue_disc, /* 11 BTA_DM_QUEUE_DISC */ bta_dm_search_clear_queue, /* 12 BTA_DM_SEARCH_CLEAR_QUEUE */ bta_dm_search_cancel_cmpl, /* 13 BTA_DM_SEARCH_CANCEL_CMPL */ bta_dm_search_cancel_notify, /* 14 BTA_DM_SEARCH_CANCEL_NOTIFY */ bta_dm_search_cancel_transac_cmpl, /* 15 BTA_DM_SEARCH_CANCEL_TRANSAC_CMPL*/ bta_dm_disc_rmt_name, /* 16 BTA_DM_DISC_RMT_NAME */ bta_dm_di_disc, /* 17 BTA_DM_API_DI_DISCOVER */ bta_dm_close_gatt_conn};#define BTA_DM_SEARCH_IGNORE BTA_DM_SEARCH_NUM_ACTIONS/* state table information */#define BTA_DM_SEARCH_ACTIONS 2 /* number of actions */#define BTA_DM_SEARCH_NEXT_STATE 2 /* position of next state */#define BTA_DM_SEARCH_NUM_COLS 3 /* number of columns in state tables *//* DM search state */enum { BTA_DM_SEARCH_IDLE, BTA_DM_SEARCH_ACTIVE, BTA_DM_SEARCH_CANCELLING, BTA_DM_DISCOVER_ACTIVE};typedef const uint8_t (*tBTA_DM_ST_TBL)[BTA_DM_SEARCH_NUM_COLS];/* state table */const tBTA_DM_ST_TBL bta_dm_search_st_tbl[] = { bta_dm_search_idle_st_table, bta_dm_search_search_active_st_table, bta_dm_search_search_cancelling_st_table, bta_dm_search_disc_active_st_table};const uint8_t bta_dm_search_disc_active_st_table[][BTA_DM_SEARCH_NUM_COLS] = { /* Event Action 1 Action 2 Next State */ /* API_SEARCH */ {BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* API_SEARCH_CANCEL */ {BTA_DM_SEARCH_CANCEL_NOTIFY, BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_CANCELLING}, /* API_SEARCH_DISC */ {BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* INQUIRY_CMPL */ {BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* REMT_NAME_EVT */ {BTA_DM_DISC_RMT_NAME, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* SDP_RESULT_EVT */ {BTA_DM_SDP_RESULT, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* SEARCH_CMPL_EVT */ {BTA_DM_SEARCH_CMPL, BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IDLE}, /* DISCV_RES_EVT */ {BTA_DM_DISC_RESULT, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* API_DI_DISCOVER_EVT */ {BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}, /* DISC_CLOSE_TOUT_EVT */ {BTA_DM_SEARCH_IGNORE, BTA_DM_SEARCH_IGNORE, BTA_DM_DISCOVER_ACTIVE}};
首先看上面代码的state machine action enumeration list 和action function list。这两个数组是一一对应的。用法就是状态机表中action这一列填的是enum的值,查表获得这个enum值后,可以对应到具体的action函数。
接下来,这个状态机有4个状态,所以bta_dm_search_st_tbl[]里有包含4张状态表,4张状态表的格式都是完全一样的,区别就是里面的action和next state的值不同。我只贴了其中一个状态表来说明。为了看得更明白,我把状态表的格式做了一下调整。
下面再来分析状态表的细节。
状态机的工作方式就是当处于某个特定状态时,收到event后,会执行一些操作,然后再跳转到下一个状态。假设我们现在是BTA_DM_DISCOVER_ACTIVE状态,那么会选中bta_dm_search_disc_active_st_table这张表(下文说的表默认为bta_dm_search_disc_active_st_table)。在表中,我们看到有event,action, next state这些列。表的行数由event的数目决定,就是前面定义的DM search event的枚举。一般状态机只需要一个action,所有可能没有Action2列。
我们再结合代码来看看怎么查状态机。
假设当前在bta_dm_search_cb.state保存的状态是BTA_DM_DISCOVER_ACTIVE。这时候来了SEARCH_CMPL_EVT事件。
首先根据当前状态,查到的state_table为bta_dm_search_disc_active_st_table。
接着查状态表可知下一个状态为BTA_DM_SEARCH_IDLE。保存到bta_dm_search_cb.state中。
最后用for循环依次查找action的函数,依次执行,此时执行bta_dm_search_cmpl,action2为空。执行完后就返回。
当有新事件来时,再按照上面的顺序执行。
以上就是文档的所有内容,希望对你研究bluedroid,解决蓝牙问题时有帮助。如有问题请及时指正。谢谢。
- Bluedroid中的线程介绍
- BlueDroid介绍
- BlueDroid介绍
- BlueDroid介绍
- BlueDroid介绍
- BlueDroid介绍
- BlueDroid介绍
- BlueDroid介绍
- Bluedroid 框架介绍
- Bluedroid - Android M - 平台介绍
- Android下bluedroid、bluetooth apk介绍
- bluedroid中的start discovery代码流程
- Android BlueDroid分析: Linux中的Eventfd
- Android BlueDroid分析: OSI中的HashMap的实现
- 介绍 — Java 6中的线程优化
- Tomcat 中的线程相关类介绍
- Bluedroid 筆記
- bluedroid 框架
- BASE64编码之javascript类库BASE64.js
- typescript开发node对数据库层的封装
- [SCOI2010]生成字符串
- java-储物柜难题
- 上新
- Bluedroid中的线程介绍
- 【转】Windows下使用libsvm中的grid.py和easy.py进行参数调优
- HDU 4511 AC自动机+DP
- hdu 6153 A Secret KMP&&扩展KMP
- 简单OPENCV人脸检测识别原理
- 有些故事,无关感情
- Centos下mysql数据库安装、创建数据库、utf8编码设置、外部访问授权、导入sql执行、开机启动(系列3)
- Xcode9无线调试的配置
- 旧版本的platform_device和platform_driver和新版本中的操作解释