android5.0(Lollipop) BLE Peripheral深入理解系统篇之提高篇
上一篇文章讲到了广播之前系统需要进行的准备工作,那接下来我们就来真正的启动广播。 首先还是先看一下上一篇文章结束的地方: @Override
public void onClientRegistered(int status, int clientIf) {
Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
synchronized (this) {
if (status == BluetoothGatt.GATT_SUCCESS) {
mClientIf = clientIf;
try {
mBluetoothGatt.startMultiAdvertising(mClientIf, mAdvertisement,
mScanResponse, mSettings);
return;
} catch (RemoteException e) {
Log.e(TAG, "failed to start advertising", e);
}
}
// Registration failed.
mClientIf = -1;
notifyAll();
}
}
现在让我们继续追踪mBluetoothGatt.startMultiAdvertising,但是我们发现mBluetoothGatt是通过AIDL來定义的: private final IBluetoothGatt mBluetoothGatt;
到这里,我们就不去看IBluetoothGatt的内容了,因为都是一些接口函数,我们比较关心的是IBluetoothGatt是谁的接口呢,在android如果遇到这种情况,我们肯定要去找service了,那我们现在基本可以确定,我们找的是GattService了,那我们去验证一下,到底是不是这样的: private static class BluetoothGattBinder extends IBluetoothGatt.Stub implements IProfileServiceBinder
看到这里我们应该可以确认tartMultiAdvertising的实现是在这里,那我们找一下它的实现: @Override
public void startMultiAdvertising(int clientIf, AdvertiseData advertiseData,
AdvertiseData scanResponse, AdvertiseSettings settings) {
GattService service = getService();
if (service == null) return;
service.startMultiAdvertising(clientIf, advertiseData, scanResponse, settings);
}
终于找到它的实现了,它却有用了GattService自己内部的实现,那我们继续看一下: void startMultiAdvertising(int clientIf, AdvertiseData advertiseData,
AdvertiseData scanResponse, AdvertiseSettings settings) {
enforceAdminPermission();
mAdvertiseManager.startAdvertising(new AdvertiseClient(clientIf, settings, advertiseData,
scanResponse));
}
到这里,我们有可以比较顺利的追踪了,那我们继续看一下AdvertiserManager是怎样定义它的: void startAdvertising(AdvertiseClient client) {
if (client == null) {
return;
}
Message message = new Message();
message.what = MSG_START_ADVERTISING;
message.obj = client;
mHandler.sendMessage(message);
}
到这里,发现竟然只是发了一个handler message,那我们就去看看这个handler是怎么处理
@Override
public void handleMessage(Message msg) {
logd("message : " + msg.what);
AdvertiseClient client = (AdvertiseClient) msg.obj;
switch (msg.what) {
case MSG_START_ADVERTISING:
handleStartAdvertising(client);
break;
case MSG_STOP_ADVERTISING:
handleStopAdvertising(client);
break;
default:
// Shouldn't happen.
Log.e(TAG, "recieve an unknown message : " + msg.what);
break;
}
}
private void handleStartAdvertising(AdvertiseClient client) {
Utils.enforceAdminPermission(mService);
int clientIf = client.clientIf;
if (mAdvertiseClients.contains(clientIf)) {
postCallback(clientIf, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED);
return;
} if (mAdvertiseClients.size() >= maxAdvertiseInstances()) {
postCallback(clientIf,
AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS);
return;
}
if (!mAdvertiseNative.startAdverising(client)) {
postCallback(clientIf, AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
return;
}
mAdvertiseClients.add(client);
postCallback(clientIf, AdvertiseCallback.ADVERTISE_SUCCESS);
}
到这里了,我们也看到了关键函数mAdvertiseNative.startAdvertising函数: boolean startAdverising(AdvertiseClient client) {
if (!mAdapterService.isMultiAdvertisementSupported() &&
!mAdapterService.isPeripheralModeSupported()) {
return false;
}
if (mAdapterService.isMultiAdvertisementSupported()) {
return startMultiAdvertising(client);
}
return startSingleAdvertising(client);
}
这里我们就选择任意一个函数就讲解,就选择startSingleAdvertising来讲好了: boolean startSingleAdvertising(AdvertiseClient client) {
logd("starting single advertising");
resetCountDownLatch();
enableAdvertising(client);
if (!waitForCallback()) {
return false;
}
setAdvertisingData(client, client.advertiseData, false);
return true;
}
这里面有两个关键点,一个是使能Advertising,另一个是设置AdvertisingData要发的数据。我们先来看一下enableAdvertising: private void enableAdvertising(AdvertiseClient client) {
int clientIf = client.clientIf;
int minAdvertiseUnit = (int) getAdvertisingIntervalUnit(client.settings);
int maxAdvertiseUnit = minAdvertiseUnit + ADVERTISING_INTERVAL_DELTA_UNIT;
int advertiseEventType = getAdvertisingEventType(client);
int txPowerLevel = getTxPowerLevel(client.settings);
int advertiseTimeoutSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(
client.settings.getTimeout());
if (mAdapterService.isMultiAdvertisementSupported()) {
Log.i(TAG, "gattClientEnableAdvNative(" + clientIf + ",...);");
gattClientEnableAdvNative(
clientIf,
minAdvertiseUnit, maxAdvertiseUnit,
advertiseEventType,
ADVERTISING_CHANNEL_ALL,
txPowerLevel,
advertiseTimeoutSeconds);
} else {
Log.i(TAG, "gattAdvertiseNative(" + client.clientIf + ",true);");
gattAdvertiseNative(client.clientIf, true);
}
}
这里面主要是在设置一些参数,比如最大广播间隔设置的是10ms+设置的最小间隔,TxPower的级别等等。这里面对于多广播和单广播调用的底层是不一样的。我们这里看看单广播gattAdvertiseNative(client.clientIf, true);实现如下: static void gattAdvertiseNative(JNIEnv *env, jobject object,
jint client_if, jboolean start)
{
if (!sGattIf) return;
sGattIf->client->listen(client_if, start);
}
这里的映射关系我就不再介绍了,上一篇文章已经都介绍一次了,我们这里直接看看listen函数: static bt_status_t btif_gattc_listen(int client_if, bool start)
{
CHECK_BTGATT_INIT();
btif_gattc_cb_t btif_cb;
btif_cb.client_if = (uint8_t) client_if;
btif_cb.start = start ? 1 : 0;
return btif_transfer_context(btgattc_handle_event, BTIF_GATTC_LISTEN,
(char*) &btif_cb, sizeof(btif_gattc_cb_t), NULL);
}
直接看实现:
static void btgattc_handle_event(uint16_t event, char* p_param)
{
……
switch (event)
{ case BTIF_GATTC_LISTEN:
#if (defined(BLE_PERIPHERAL_MODE_SUPPORT) && (BLE_PERIPHERAL_MODE_SUPPORT == TRUE))
BTA_GATTC_Listen(p_cb->client_if, p_cb->start, NULL);
#else
BTA_GATTC_Broadcast(p_cb->client_if, p_cb->start);
#endif
break;
……
目前我用的设备BLE_PERIPHERAL_MODE_SUPPORT是true,所以我们进入BTA_GATTC_Listen: void BTA_GATTC_Listen(tBTA_GATTC_IF client_if, BOOLEAN start, BD_ADDR_PTR target_bda)
{
tBTA_GATTC_API_LISTEN *p_buf; if ((p_buf = (tBTA_GATTC_API_LISTEN *) GKI_getbuf((UINT16)(sizeof(tBTA_GATTC_API_LISTEN) + BD_ADDR_LEN))) != NULL)
{
p_buf->hdr.event = BTA_GATTC_API_LISTEN_EVT; p_buf->client_if = client_if;
p_buf->start = start;
if (target_bda)
{
p_buf->remote_bda = (UINT8*)(p_buf + 1);
memcpy(p_buf->remote_bda, target_bda, BD_ADDR_LEN);
}
else
p_buf->remote_bda = NULL; bta_sys_sendmsg(p_buf);
}
return;
}
这里是开始进入广播且监听一个client设备的连接请求。这里要注意一下event时间,这个时间会被触发: BOOLEAN bta_gattc_hdl_event(BT_HDR *p_msg)
{
……
case BTA_GATTC_API_LISTEN_EVT:
bta_gattc_listen(p_cb, (tBTA_GATTC_DATA *) p_msg);
break;
……
[cpp] view plaincopy
void bta_gattc_listen(tBTA_GATTC_CB *p_cb, tBTA_GATTC_DATA * p_msg)
{
tBTA_GATTC_RCB *p_clreg = bta_gattc_cl_get_regcb(p_msg->api_listen.client_if);
tBTA_GATTC cb_data;
UNUSED(p_cb); cb_data.reg_oper.status = BTA_GATT_ERROR;
cb_data.reg_oper.client_if = p_msg->api_listen.client_if; if (p_clreg == NULL)
{
APPL_TRACE_ERROR("bta_gattc_listen failed, unknown client_if: %d",
p_msg->api_listen.client_if);
return;
}
/* mark bg conn record */
if (bta_gattc_mark_bg_conn(p_msg->api_listen.client_if,
(BD_ADDR_PTR) p_msg->api_listen.remote_bda,
p_msg->api_listen.start,
TRUE))
{
if (!GATT_Listen(p_msg->api_listen.client_if,
p_msg->api_listen.start,
p_msg->api_listen.remote_bda))
{
APPL_TRACE_ERROR("Listen failure");
(*p_clreg->p_cback)(BTA_GATTC_LISTEN_EVT, &cb_data);
}
else
{
cb_data.status = BTA_GATT_OK; (*p_clreg->p_cback)(BTA_GATTC_LISTEN_EVT, &cb_data); if (p_msg->api_listen.start)
{
/* if listen to a specific target */
if (p_msg->api_listen.remote_bda != NULL)
{ /* if is a connected remote device */
if (L2CA_GetBleConnRole(p_msg->api_listen.remote_bda) == HCI_ROLE_SLAVE &&
bta_gattc_find_clcb_by_cif(p_msg->api_listen.client_if,
p_msg->api_listen.remote_bda,
BTA_GATT_TRANSPORT_LE) == NULL)
{ bta_gattc_init_clcb_conn(p_msg->api_listen.client_if,
p_msg->api_listen.remote_bda);
}
}
/* if listen to all */
else
{
APPL_TRACE_ERROR("Listen For All now");
/* go through all connected device and send
callback for all connected slave connection */
bta_gattc_process_listen_all(p_msg->api_listen.client_if);
}
}
}
}
}
到这里,剩下的全是连接相关,我自己都有点绕,所以暂时就不讲解了。这块后期再补上。 private void setAdvertisingData(AdvertiseClient client, AdvertiseData data,
boolean isScanResponse) {
if (data == null) {
return;
}
boolean includeName = data.getIncludeDeviceName();
boolean includeTxPower = data.getIncludeTxPowerLevel();
int appearance = 0;
byte[] manufacturerData = getManufacturerData(data); byte[] serviceData = getServiceData(data);
byte[] serviceUuids;
if (data.getServiceUuids() == null) {
serviceUuids = new byte[0];
} else {
ByteBuffer advertisingUuidBytes = ByteBuffer.allocate(
data.getServiceUuids().size() * 16)
.order(ByteOrder.LITTLE_ENDIAN);
for (ParcelUuid parcelUuid : data.getServiceUuids()) {
UUID uuid = parcelUuid.getUuid();
// Least significant bits first as the advertising UUID should be in
// little-endian.
advertisingUuidBytes.putLong(uuid.getLeastSignificantBits())
.putLong(uuid.getMostSignificantBits());
}
serviceUuids = advertisingUuidBytes.array();
}
if (mAdapterService.isMultiAdvertisementSupported()) {
Log.i(TAG, "gattClientSetAdvDataNative(" + client.clientIf + ",...);");
gattClientSetAdvDataNative(client.clientIf, isScanResponse, includeName,
includeTxPower, appearance,
manufacturerData, serviceData, serviceUuids);
} else {
Log.i(TAG, "gattSetAdvDataNative(" + client.clientIf + ",...);");
gattSetAdvDataNative(client.clientIf, isScanResponse, includeName,
includeTxPower, 0, 0, appearance,
manufacturerData, serviceData, serviceUuids);
}
} static void gattSetAdvDataNative(JNIEnv *env, jobject object, jint client_if,
jboolean setScanRsp, jboolean inclName, jboolean inclTxPower, jint minInterval,
jint maxInterval, jint appearance, jbyteArray manufacturerData, jbyteArray serviceData,
jbyteArray serviceUuid)
{
if (!sGattIf) return;
jbyte* arr_data = env->GetByteArrayElements(manufacturerData, NULL);
uint16_t arr_len = (uint16_t) env->GetArrayLength(manufacturerData); jbyte* service_data = env->GetByteArrayElements(serviceData, NULL);
uint16_t service_data_len = (uint16_t) env->GetArrayLength(serviceData); jbyte* service_uuid = env->GetByteArrayElements(serviceUuid, NULL);
uint16_t service_uuid_len = (uint16_t) env->GetArrayLength(serviceUuid); sGattIf->client->set_adv_data(client_if, setScanRsp, inclName, inclTxPower,
minInterval, maxInterval, appearance, arr_len, (char*)arr_data,
service_data_len, (char*)service_data, service_uuid_len,
(char*)service_uuid); env->ReleaseByteArrayElements(manufacturerData, arr_data, JNI_ABORT);
env->ReleaseByteArrayElements(serviceData, service_data, JNI_ABORT);
env->ReleaseByteArrayElements(serviceUuid, service_uuid, JNI_ABORT);
}
那我们就来主要看看set_adv_data函数: static bt_status_t btif_gattc_set_adv_data(int client_if, bool set_scan_rsp, bool include_name,
bool include_txpower, int min_interval, int max_interval, int appearance,
uint16_t manufacturer_len, char* manufacturer_data,
uint16_t service_data_len, char* service_data,
uint16_t service_uuid_len, char* service_uuid)
{
CHECK_BTGATT_INIT();
bt_status_t status =0; btif_adv_data_t adv_data; btif_gattc_adv_data_packager(client_if, set_scan_rsp, include_name,
include_txpower, min_interval, max_interval, appearance, manufacturer_len,
manufacturer_data, service_data_len, service_data, service_uuid_len, service_uuid,
&adv_data); status = btif_transfer_context(btgattc_handle_event, BTIF_GATTC_SET_ADV_DATA,
(char*) &adv_data, sizeof(btif_adv_data_t), NULL); if (NULL != adv_data.p_service_data)
GKI_freebuf(adv_data.p_service_data); if (NULL != adv_data.p_service_uuid)
GKI_freebuf(adv_data.p_service_uuid); if (NULL != adv_data.p_manufacturer_data)
GKI_freebuf(adv_data.p_manufacturer_data); return status;
}
其中btif_gattc_adv_data_packager直接到了GKI,所以我们来看一下btgattc_handle_event触发的event事件: case BTIF_GATTC_SET_ADV_DATA:
{
btif_adv_data_t *p_adv_data = (btif_adv_data_t*) p_param;
int cbindex = CLNT_IF_IDX;
if (cbindex >= 0 && NULL != p_adv_data)
{
btgatt_multi_adv_common_data *p_multi_adv_data_cb = btif_obtain_multi_adv_data_cb();
if (!btif_gattc_copy_datacb(cbindex, p_adv_data, false))
return; if (!p_adv_data->set_scan_rsp)
{
BTA_DmBleSetAdvConfig(p_multi_adv_data_cb->inst_cb[cbindex].mask,
&p_multi_adv_data_cb->inst_cb[cbindex].data, bta_gattc_set_adv_data_cback);
}
else
{
BTA_DmBleSetScanRsp(p_multi_adv_data_cb->inst_cb[cbindex].mask,
&p_multi_adv_data_cb->inst_cb[cbindex].data, bta_gattc_set_adv_data_cback);
}
break;
}
}
这里我们主要是走了BTA_DmBleSetAdvConfig: void BTA_DmBleSetAdvConfig (tBTA_BLE_AD_MASK data_mask, tBTA_BLE_ADV_DATA *p_adv_cfg,
tBTA_SET_ADV_DATA_CMPL_CBACK *p_adv_data_cback)
{
tBTA_DM_API_SET_ADV_CONFIG *p_msg; if ((p_msg = (tBTA_DM_API_SET_ADV_CONFIG *)
GKI_getbuf(sizeof(tBTA_DM_API_SET_ADV_CONFIG))) != NULL)
{
p_msg->hdr.event = BTA_DM_API_BLE_SET_ADV_CONFIG_EVT;
p_msg->data_mask = data_mask;
p_msg->p_adv_data_cback = p_adv_data_cback;
p_msg->p_adv_cfg = p_adv_cfg; bta_sys_sendmsg(p_msg);
}
}
这里实际会走到
void bta_dm_ble_set_adv_config (tBTA_DM_MSG *p_data)
{
tBTA_STATUS status = BTA_FAILURE; if (BTM_BleWriteAdvData(p_data->ble_set_adv_data.data_mask,
(tBTM_BLE_ADV_DATA *)p_data->ble_set_adv_data.p_adv_cfg) == BTM_SUCCESS)
{
status = BTA_SUCCESS;
} if (p_data->ble_set_adv_data.p_adv_data_cback)
(*p_data->ble_set_adv_data.p_adv_data_cback)(status);
}
tBTM_STATUS BTM_BleWriteAdvData(tBTM_BLE_AD_MASK data_mask, tBTM_BLE_ADV_DATA *p_data)
{
tBTM_BLE_LOCAL_ADV_DATA *p_cb_data = &btm_cb.ble_ctr_cb.inq_var.adv_data;
UINT8 *p;
tBTM_BLE_AD_MASK mask = data_mask; BTM_TRACE_EVENT ("BTM_BleWriteAdvData "); if (!HCI_LE_HOST_SUPPORTED(btm_cb.devcb.local_lmp_features[HCI_EXT_FEATURES_PAGE_1]))
return BTM_ILLEGAL_VALUE; memset(p_cb_data, 0, sizeof(tBTM_BLE_LOCAL_ADV_DATA));
p = p_cb_data->ad_data;
p_cb_data->data_mask = data_mask; p_cb_data->p_flags = btm_ble_build_adv_data(&mask, &p, p_data); p_cb_data->p_pad = p; if (mask != 0)
{
BTM_TRACE_ERROR("Partial data write into ADV");
} p_cb_data->data_mask &= ~mask; if (btsnd_hcic_ble_set_adv_data((UINT8)(p_cb_data->p_pad - p_cb_data->ad_data),
p_cb_data->ad_data))
return BTM_SUCCESS;
else
return BTM_NO_RESOURCES; }
BOOLEAN btsnd_hcic_ble_set_adv_data (UINT8 data_len, UINT8 *p_data)
{
BT_HDR *p;
UINT8 *pp; if ((p = HCI_GET_CMD_BUF(HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1)) == NULL)
return (FALSE); pp = (UINT8 *)(p + 1); p->len = HCIC_PREAMBLE_SIZE + HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1;
p->offset = 0; UINT16_TO_STREAM (pp, HCI_BLE_WRITE_ADV_DATA);
UINT8_TO_STREAM (pp, HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1); memset(pp, 0, HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA); if (p_data != NULL && data_len > 0)
{
if (data_len > HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA)
data_len = HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA; UINT8_TO_STREAM (pp, data_len); ARRAY_TO_STREAM (pp, p_data, data_len);
}
btu_hcif_send_cmd (LOCAL_BR_EDR_CONTROLLER_ID, p); return (TRUE);
}
后面已经直接在btu通信了,我们也就不深入了。
所以步骤跑完,在callback就会获得onSuccess中。 广播就结束了,后面我会继续更新Central相关流程。
android5.0(Lollipop) BLE Peripheral深入理解系统篇之提高篇的更多相关文章
- android5.0(Lollipop) BLE Peripheral牛刀小试
转载请表明作者:http://blog.csdn.net/lansefeiyang08/article/details/46468743 知道Android L对蓝牙对了一些改进.包含加入A2dp s ...
- Android5.0(Lollipop) BLE蓝牙4.0+浅析demo连接(三)
作者:Bgwan链接:https://zhuanlan.zhihu.com/p/23363591来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. Android5.0(L ...
- Android5.0(Lollipop) BLE蓝牙4.0+浅析code(二)
作者:Bgwan链接:https://zhuanlan.zhihu.com/p/23347612来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. Android5.0(L ...
- android5.0(Lollipop) BLE Central牛刀小试
转载请表明作者:http://blog.csdn.net/lansefeiyang08/article/details/46482073 昨天写了android L BLE Peripheral的简单 ...
- Android5.0(Lollipop) BLE蓝牙4.0+浅析概念(四)
作者:Bgwan链接:https://zhuanlan.zhihu.com/p/23679793来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 置顶:此文转载CSDN博 ...
- Android5.0(lollipop)新特性介绍(一)
今年6月的Google I/O大会上.Android L的初次见面我相信让会让非常多android粉丝有些小激动和小期待.当然作为开发人员的我来说,激动不言而喻,毕竟这是自08年以来改变最大的一个版本 ...
- android5.0 BLE 蓝牙4.0+浅析demo搜索(一)
作者:Bgwan链接:https://zhuanlan.zhihu.com/p/23341414来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 作者:Bgwan 莳萝花 ...
- Android5.0以上系统的移动网络开关
笔者近期遇到一个非常有意思的bug,贴出来和大家分享下. 那是一个温暖的早晨,阳光晒得人非常舒服.一封bug邮件像一片叶子飘到我的邮箱. 一番交流.笔者确认负责的Widget开关在Android5.0 ...
- Android5.0通知变化浅析
目前在Android中通知的使用还是很常见的,为了做版本兼容,常用兼容包NotificationCompat.Builder和 Notification.Builder. NotificationCo ...
随机推荐
- [C/C++基础]读写文件
1.打开.关闭文件: FILE* fp = fopen(string.c_str(), FLAG); string.c_str():需用C语言字符串形式: FLAG说明: r: 只读方式打开: w: ...
- python成长之路16
阅读(72) 一:jQuery是一个兼容多浏览器的javascript类库,核心理念是write less,do more(写得更少,做得更多),对javascript进行了封装,是的更加便捷的开发, ...
- asp.net 一般处理程序小优化
使用asp.net mvc习惯了,最近项目中又开始使用asp.net,有大量的ajax方法调用,之前有两种方法来处理: Switch case :方法少还行,如果很多,就太蛋疼了,而且方法堆在一块,也 ...
- Publisher/Subscriber 订阅-发布模式
Publisher/Subscriber 订阅-发布模式 本博后续将陆续整理这些年做的一些预研demo,及一些前沿技术的研究,与大家共研技术,共同进步. 关于发布订阅有很多种实现方式,下面主要介绍WC ...
- 转:你真的懂得JS吗?
题目1 if (!("a" in window)) { var a = 1; } alert(a); // undefined, ~~~所有全局变量都是window的属性,声明语句 ...
- Internet Explorer 11(IE11)无法切换第三方输入法
Windows 8.1搭载了新的IE11版本,还发布了IE11 for Windows 7. IE11除了支持全尺寸Win设备以外,还比IE10更快速流畅,支持3D等高性能的浏览体验.全新F12开发者 ...
- java的JCombobox实现中国省市区三级联动
源代码下载:点击下载源代码 用xml存储中国各大城市的数据. xml数据太多了就不贴上了,贴个图片: 要解释xml,添加了一个jdom.jar,上面的源代码下载里面有. 解释xml的类: packag ...
- Java集合中对象排序
集合中的对象排序需求还是比較常见的.当然我们能够重写equals方法,循环比較:同一时候Java为我们提供了更易使用的APIs.当须要排序的集合或数组不是单纯的数字型时,通常能够使用Comparato ...
- 【Eclipse】离线插件安装
1.在eclipse里根目录里,dropins里建一个目录,名字叫sonar(这个名字随便),再在svn下面建一个目录eclipse.在根目录里建一个目录links目录,并建一个sonar.link文 ...
- PHP缓存主要技术
1.普遍缓存技术: 数据缓存:这里所说的数据缓存是指数据库查询PHP缓存机制,每次访问页面的时候,都会先检测相应的缓存数据是否存在,如果不存在,就连接数据库,得到数据,并把查询结果序列化后保存到文件中 ...