MDU某产品OMCI模块代码质量现状分析
说明
本文参考MDU系列某产品OMCI模块现有代码,提取若干实例以说明目前的代码质量,亦可作为甄别不良代码的参考。
本文旨在就事论事,而非否定前人(没有前人的努力也难有后人的进步)。希望以史为鉴,不破不立,最终产出高质量的代码。
一 质量现状
不考虑业务实现,现有的OMCI模块代码质量不甚理想。无论是理解上手、修改扩展和测试排障,可以用举步维艰形容。尤其是二层通道计算相关代码,堪比令史前动物无法自拔的“焦油坑”。
本节将不考虑流程设计,仅就函数粒度列举目前存在的较为突出的代码质量问题。
1.1 巨型函数
通过Source Monitor度量代码复杂度,如下图所示:
可见,相比业界推荐的5以下,OMCI模块代码复杂度非常之高。
复杂度最高(157)的函数位于Omci_me_layer2_data_path.c二层通道计算文件,选取其中一段函数调用链分析:
→OMCI_Me_Layer2_Calculate_TCI_List(复杂度:67,行数:282)
→OMCI_Me_Layer2_Calculate_TCI_According_VlanRuleList(复杂度:28,行数:182)
该函数对扩展Vlan存在递归调用。
→OMCI_Me_Layer2_Compare_TCI(复杂度:6,行数:38)
该函数位于三级while循环各级内,如下(原循环77行,为突出重点已删除次要细节):
更具震撼性的是,OMCI_Me_Layer2_Compare_TCI调用下面三个巨型函数:
→→OMCI_Me_Layer2_Compare_EthType(复杂度:26,行数:90)
→→OMCI_Me_Layer2_Compare_Vid(复杂度:149,行数:450)
→→OMCI_Me_Layer2_Compare_Pri(复杂度:157,行数:468)
以上仅示出调用链上的关键函数,且调用链自身仅隶属于通道计算的一环。评估其全局复杂度,这样的代码实际上是不可维护的。
此外,当Compare处理耗时超过直接Set时,将严重违背设计本意。
事实上,诸如此类的巨型函数在模块内随处可见。单论函数行数,目前发现的最巨型的函数参见Sub_SNGetMeValueFromAdapter(复杂度:59,行数:538行)。
1.2 设计缺陷
代码中存在较多设计欠佳之处,在此举例若干。
/**********************************************************************
* 函数名称: Omci_List_Pop_Front
* 功能描述: 从链表头部弹出一个节点
* 输入参数: OMCI_LIST *pList 链表指针
* 输出参数: void *pOutData 链表节点的数据
* 返 回 值:
***********************************************************************/
INT32U Omci_List_Pop_Front(OMCI_LIST *pList, void *pNodeData)
{
OMCI_LIST_NODE *pHeadNode = NULL; if (pList == NULL || pNodeData == NULL)
{
return OMCI_FUNC_RETURN_FAILURE;
} pHeadNode = pList->pHead;
if (NULL != pHeadNode)
{
if (NULL != pHeadNode->pNodeData)
{
memcpy(pNodeData, pHeadNode->pNodeData, pList->dwNodeDataSize);
}
else
{
return OMCI_FUNC_RETURN_FAILURE;
}
}
else
{
return OMCI_FUNC_RETURN_FAILURE;
} Omci_List_Remove(pList, pHeadNode); return OMCI_FUNC_RETURN_SUCCESS;
}
Omci_List_Pop_Front
Omci_List_Pop_Front函数和Omci_List_Push_Back函数成对出现,其代码风格在OMCI模块内属于比较优秀的。此处仅分析其设计缺陷:
1)从函数名称看其功能应为弹出栈元素,从“功能描述”看应为队列行为,从函数体看既不是栈也不是队列;
2)函数兼具取值和删除功能,但通常取值时不应删除,删除时无需取值。
上述问题直接导致Omci_List_Pop_Front函数接近于废弃,无处可用。
其实我们需要的不是Omci_List_Pop_Front函数,而是Locate或称Query函数。基于后者,删除链表节点时先Locate后Remove;而不是目前由调用者遍历链表后调用Omci_List_Remove,从而将链表细节暴露在外。
后来在XGPON代码里新增了Omci_List_Query函数,定义如下:
OMCI_LIST_NODE* Omci_List_Query(OMCI_LIST *pList, ...)
{
OMCI_LIST_NODE_KEY aKeyGroup[MAX_LIST_NODE_KEYS_NUM];
OMCI_LIST_NODE *pNode=NULL;
INT8U *pData=NULL, *pKeyValue=NULL;
INT8U ucKeyNum=, i;
INT32U iKeyOffset=, iKeyLen=;
VA_LIST tArgList; if(NULL==pList)
return NULL;
memset((INT8U*)aKeyGroup, , sizeof(OMCI_LIST_NODE_KEY)*MAX_LIST_NODE_KEYS_NUM);
VA_START(tArgList, pList);
while(TRUE)
{
pKeyValue=VA_ARG(tArgList, INT8U*);
if(LIST_END==pKeyValue)
break;
iKeyOffset=VA_ARG(tArgList, INT32U);
iKeyLen=VA_ARG(tArgList, INT32U);
if(==iKeyLen)
{
VA_END(tArgList);
return NULL;
}
if(ucKeyNum>=MAX_LIST_NODE_KEYS_NUM)
{
VA_END(tArgList);
return NULL;
}
aKeyGroup[ucKeyNum].pKeyValue=pKeyValue;
aKeyGroup[ucKeyNum].iKeyOffset=iKeyOffset;
aKeyGroup[ucKeyNum++].iKeyLen=iKeyLen;
}
VA_END(tArgList); pNode=Omci_List_First(pList);
while(NULL!=pNode)
{
pData=(INT8U*)pNode->pNodeData;
for(i=; i<ucKeyNum; i++)
{
if(!=memcmp(&pData[aKeyGroup[i].iKeyOffset], aKeyGroup[i].pKeyValue, aKeyGroup[i].iKeyLen))
break;
}
if(i>=ucKeyNum)
{
break;
}
pNode=pNode->pNext;
}
return pNode;
}
Omci_List_Query
使用示例如下:
OMCI_LIST_NODE* pGemPortNode = Omci_List_Query(g_pIgmpUpAction, &tIGMPVlanAction, , sizeof(OMCI_ADAPTER_MulticastPort_vlan_EX), LIST_END);
if(NULL != pGemPortNode)
{
Omci_List_Remove(g_pIgmpUpAction, pGemPortNode);
}
Omci_List_Query Usage
可见,该函数有效地隐藏了链表细节,但变参函数易用性和可读性比较差(注意原函数没有注释)。对比下面的查找函数:
/**********************************************************************
* 函数名称: OmciLocateListNode
* 功能描述: 查找链表首个与pData满足函数fCompareNode判定关系的结点
* 输入参数: T_OMCI_LIST* pList :链表指针
* VOID* pData :待比较数据指针
* CompareNodeFunc fCompareNode :比较回调函数指针
* 输出参数: NA
* 返 回 值: T_OMCI_LIST_NODE* 链表结点指针(未找到时返回NULL)
***********************************************************************/
T_OMCI_LIST_NODE* OmciLocateListNode(T_OMCI_LIST *pList, VOID *pData, CompareNodeFunc fCompareNode)
{
CHECK_TRIPLE_POINTER(pList, pData, fCompareNode, NULL);
CHECK_SINGLE_POINTER(pList->pHead, NULL);
CHECK_SINGLE_POINTER(pList->pHead->pNext, NULL); T_OMCI_LIST_NODE *pListNode = pList->pHead->pNext;
while(pListNode != pList->pHead)
{
if( == fCompareNode(pListNode->pNodeData, pData, pList->dwNodeDataSize))
return pListNode; pListNode = pListNode->pNext;
} return NULL;
}
两个函数由不同作者分别实现,互不知情。但对比之下,易用性和可读性可窥一斑。
1.3 编码缺陷
举例如下:
INT8U ONUMACDRV_get_omci(ONUMAC_OMCI* vpOut , INT8U* vpIn)
{
//static INT16U wTci;
INT8U * buffer ;
INT32U index, crc_result = , crc_trans = ;
char omcilog[] = {}; buffer = ( INT8U * ) OMCI_MALLOC ( OMCI_MESSAGE_LENGTH ) ;
if ( NULL == buffer )
{
OmciPrint_error("[%s]OMCI OMCI_MALLOC buffer is NULL!\n\r", FUNCTION_NAME);
return OMCI_FUNC_RETURN_FAILURE;
}
memmove(buffer,vpIn,OMCI_MESSAGE_LENGTH); /*升级过程中,若出现crc出错时,是需要应答OLT的,故需要继续取值*/
vpOut->omci_header.wtranscorrelationid = (buffer [ ]<< ) | (buffer [ ]);
vpOut->omci_header.ucar = (buffer [ ]& 0x40)>>;
vpOut->omci_header.ucak = (buffer [ ]& 0x20)>>;
vpOut->omci_header.ucmsgtype = buffer [ ]& 0x1f;
vpOut->omci_header.ucdevid = buffer [ ];
vpOut->omci_header.wmeclass = (buffer [ ]<< ) | (buffer [ ]);
vpOut->omci_header.wmeid = (buffer [ ]<< ) | (buffer [ ]); /*crc 校验*/
crc_result = OMCI_crc32(buffer,OMCI_CRC_SIZE);
crc_trans =( buffer [ ] << ) |(buffer [ ] << )|(buffer [ ] << )|(buffer [ ]);
if(crc_result != crc_trans)
{
OmciPrint_error("[%s]ZTE----OMCI CRC error!crc_trans = %ud,crc_result = %ud \n\r", FUNCTION_NAME, crc_trans,crc_result);
OMCI_FREE ( buffer ) ;
return OMCI_FUNC_RETURN_PARAMETER_ERROR;
} for(index = ;index < ; index++)
{
vpOut->auccontent[index] = buffer [index+];
} if((!=(dwPrintSwitch & 0x00000001))||(!=(dwPrintSwitch & OMCI_PRINT_SWITCH_OMCIFRAME)))
{
memset(&omcilog[],,);
if(vpOut->omci_header.ucmsgtype == OMCIMETYPE_MIB_UPLOAD_NEXT)
sprintf(&omcilog[strlen(omcilog)],"Recv MIB upload next Msg, TCI=%d, SequenceNum=%d",
vpOut->omci_header.wtranscorrelationid, (vpOut->auccontent[]<<) | vpOut->auccontent[]);
else
sprintf(&omcilog[strlen(omcilog)],"Recv omci Msg, MsgType=%d, MeClass=%d, MeId=0x%04x, TCI=%d",
vpOut->omci_header.ucmsgtype,vpOut->omci_header.wmeclass,vpOut->omci_header.wmeid, vpOut->omci_header.wtranscorrelationid);
for(index = ;index < OMCI_MESSAGE_LENGTH; index++)
{
if( == (index%))
sprintf(&omcilog[strlen(omcilog)],"\n\r");
if( == index)
sprintf(&omcilog[strlen(omcilog)],"[");
if( == index)
sprintf(&omcilog[strlen(omcilog)],"]");
if( == index)
sprintf(&omcilog[strlen(omcilog)]," ");
sprintf(&omcilog[strlen(omcilog)],"%02x ",buffer[index]);
}
sprintf(&omcilog[strlen(omcilog)],"\n\r");
if(!=(dwPrintSwitch & 0x00000001))
{
omci_printTime();
/* using this function just for it is compatible with old print switchs */
omcidebug_print("%s",omcilog);
}
else
{
OmciPrint_omciframe("%s", omcilog);
}
} OMCI_FREE ( buffer ) ;
return OMCI_FUNC_RETURN_SUCCESS;
}
ONUMACDRV_get_omci
ONUMACDRV_get_omci函数对接收到的OMCI帧进行解析和受控打印。此处不考虑单一职责的设计问题,仅分析其编码缺陷:
1)OMCI帧定长为32字节,没有必要申请动态内存;
2)omcilog[strlen(omcilog)]导致大量毫无必要的字符串长度计算。
上述两个问题极大地拖慢该函数执行速度,并直接导致某产品在打开OMCI帧打印时无法注册上线。ONUMACDRV_set_omci函数存在同样问题。
关于strlen的冗余计算,再举类似一例:
/***************************************************************
* Function: Omci_IPv4_Int_to_String
* Description: 将IP地址从INT32U型转换成点分十进制的字符串
* Input:
* Output:
* Returns: 点分十进制的字符串表示的IP地址
***************************************************************/
INT8U *Omci_IPv4_Int_to_String(INT32U dwInt)
{
INT8U ucByte,ucPos;
static INT8U azStr[];
memset(azStr,,); ucPos = strlen(azStr);
sprintf(&azStr[ucPos],"%d.",(INT8U)((dwInt>>)&0x000000FF)); ucPos = strlen(azStr);
sprintf(&azStr[ucPos],"%d.",(INT8U)((dwInt>>)&0x000000FF)); ucPos = strlen(azStr);
sprintf(&azStr[ucPos],"%d.",(INT8U)((dwInt>>)&0x000000FF)); ucPos = strlen(azStr);
sprintf(&azStr[ucPos],"%d",(INT8U)(dwInt&0x000000FF)); return azStr;
}
Omci_IPv4_Int_to_String
对比其另一实现:
/* IPv4 Format: 0x0a285b09 ----> "10.40.91.9" or "0a.28.5b.09" */
/* **************************Usage**************************** */
/* INT32S dwIp = 0x0a285b09; CHAR aIpAddr[32] = {0}; */
/* HexIpv4ToStrIp(dwIp, aIpAddr, sizeof(aIpAddr)); */
#define STR_IPV4_MAX_SIZE (INT8U)sizeof("255.255.255.255")
INT32S HexIpv4ToStrIp(INT32S dwIp, CHAR *pStrIp, INT16U wSize)
{
if((NULL == pStrIp) || (wSize < STR_IPV4_MAX_SIZE))
{
return -;
} sprintf(pStrIp, "%d.%d.%d.%d", (dwIp & 0xFF000000) >> , (dwIp & 0x00FF0000) >> ,
(dwIp & 0x0000FF00) >> , dwIp & 0x000000FF);
/* Use "%02x.%02x.%02x.%02x" to form "0a.28.5b.09" */ return ;
}
HexIpv4ToStrIp
1.4 重复造轮
重复造轮主要体现在对于相同或相似的功能,存在多种实现方式。
例如,通过Get-Next命令从表里逐条获取记录时,应执行相同的操作。代码里实现如下:
函数索引表里的函数实现(绿色)几乎各不相同,表明未做好足够的抽象。
另一处明显的重复出现在Sub_InsertPerformanceDb/Sub_InsertPerfInfoDb、Sub_DeletePerformanceDb/ Sub_DeletePerfInfoDb等,对于性能统计历史库和性能统计信息库定义了几乎相同的两套操作函数。同样,这里体现了抽象和提取能力的欠缺。
当然,上述重复性相比SNdbplat.c文件里各实体操作数据库的诸多函数,简直不值一提。
重复造轮还表现在OMCI函数状态返回值多处定义,违反了DRY(Don’t Repeat Yourself)原则。列举部分成功返回值如下:
OMCI_RET_OK
OMCI_RET_SUCCESS
R_OA_OK
AGENT_OPER_SUCCESS
OMCI_LIST_OPER_SUCCESS
OMCI_FUNC_RETURN_SUCCESS
…………
Return Code
其实,将函数返回状态定义为统一的枚举变量不是更好吗?
1.5 引用混乱
引用混乱主要体现在头文件嵌套或交叉引用,违反了编译防火墙的原则。举例如下:
#include "omci_common_function.h"
→#include "omci_me_layer2_vlan.h"
→#include "omcifunc.h"
→
以上仅示出引用链上的关键头文件,这些多重引用的头文件不仅拖慢编译和链接速度,而且常常引发莫名其妙的错误。例如,曾发现头文件A中将OP_SET定义为宏,头文件B中将OP_SET定义为枚举值;而某源文件所必须包含的头文件辗转包含了头文件A和B,于是……
对于独立功能的代码(如链表操作等),头文件嵌套引用的做法还会削弱其独立性(使用者可能不得不拷贝不必要的文件)。
当然,头文件嵌套引用可以“简化”编码,如使用者可能只需在.c源文件里#include “XXXCommon.h”。但这无异于饮鸩止渴,除了纵容使用者的懒惰和不求甚解外别无他用!
因此我们约定:在头文件内尽量不引用其他头文件,而在源文件内引用所需的头文件。
此外,不加区分地将以太网、组播、语音、升级等相关宏和数据结构揉为一体,造就了1570行的巨型头文件:OmciComm.h!
注意该头文件的名称后缀(Comm),您似乎已意识到这是个被普遍包含的大家伙?
1.6 命名随意
词不达意的变量名自不待说,先就函数命名的随意性举例若干:
void Sub_SNAddmulticastoperationsprofile(DB_ONUMAC_OMCI * in,SN_OPERATE_OUT * out)
{
WORD wNum, i = ;
BYTE ucResult = ;
BYTE * pRecord = DB_POINTER_NULL;
SN_MULCAST_OPERATIONS_PROFILE MulCastOp; SN_MULCAST_OPERATIONS_PROFILE* pMulCastOpRecord = DB_POINTER_NULL; MIB_OMCI_INDEX ont_data_index; OmciPrint_debug("[LHP]Enter Sub_SNAddmulticastoperationsprofile MeClass %u MeId %u \n\r",in->index.wMeClass,in->index.wMeID);
g_wMulticastOperationsProfileDbHandle = _GetDBHandle(MULTICASTOPERATIONSPROFILE_DB); //... ...
}
Sub_SNAddmulticastoperationsprofile
示例函数无论函数名还是局部变量名(毫无用处的全局DB句柄)长度都有点过分。然而更过分的示例来自下面的函数:
void Sub_SNQuerymulticastmulticastsubscriberconfiginfoForMultopProfilePT(INT16U Meid,SN_MULTOPPRO_MULTSUBS_POINT_QUERY_OUT * out,int MaxCount)
Sub_SNQuerymulticastmulticastsubscriberconfiginfoForMultopProfilePT
该函数名足足有67个字符!代码作者似乎“刻意”将multicast重复两遍,并在函数名末尾鬼使神差地使用了缩写:MultopProfile也许意指multicastoperationsprofile,但PT作何解呢?该函数在完成其既定功能的同时,还顺带考验读者的耐心和智商。
看过“大腹便便”型的函数名后,再来赏析“雾里看花”型的函数名:
INT32U OA_manage_configure_flows(void)
{
OA_manage_compare_flows();
OA_manage_delete_flows();
OA_manage_modify_flows();
OA_manage_add_flows(); return R_OA_OK;
}
OA_manage_configure_flows
据远古的传说讲,compare、delete、modify、add的操作对象似乎不是同一条(或所有条)flow……
1.7 接口晦涩
对外提供的接口应有详细的文档说明和注释,且文档应随接口代码发布(最好置于同一目录)。接口函数应有详尽的函数签名,相同层次的函数应有相同的行为模式。此外,接口在封装内部实现的同时,应对外提供足够的透明性。无法探知内部状态的接口,无论易用性或安全性都将大打折扣。
此处就R73内存数据库接口加以说明:
1)从上研移植来的R73内存数据库设计文档已不可知。代码中也未提供数据库视图、关键结构/参数说明。接口函数签名信息量太少,直接将函数名或参数名拆分作为注释。
/*--------------------------------------------------------------
function name: _CreateDB
para: dbName = db name
desc: create db
udate history:
XXX write code on 2002-03-04
--------------------------------------------------------------*/
WORD _CreateDB( BYTE *dbName )
_CreateDB
2)插入(_Insert)、删除(_Delete)、修改(_Update)接口从逻辑而言属于相同层次的函数,但其返回值模式并不相同,即:
- _Insert成功时返回0,否则返回非0正数;
- _Delete/_Update成功时返回非0正数,否则返回0或负数。
首先,成功时返回非0正数违反编程习惯,默认约定0应表示成功。其次,三个接口返回模式不同,会增加使用者出错的可能性。
3)修改(_Update)接口内含大量校验处理,且失败时均返回-1。加之接口未提供错误打印(这本是良好的设计风格),导致使用者在调用失败时很难定位原因,而不得不手工添加调试打印:
if ((dbHandle<)||(dbHandle>=mm_db_num)||(keyPtr==NULL)||(indexNo>INDEX_NUM-))
{
GeneralErrorHandler();
return -;
}
if(SafetyCheck(WRITE_OP,dbHandle)) return -;
curRecNum=GetRecNum(dbHandle);
if (curRecNum==)
{
FreeLock(WRITE_OP,dbHandle);
return -;
}
_Update
其实接口设计者应定义一套错误原因码,校验失败时通过返回值向使用者抛出该错误码。如果接口位于同一源文件,则最简单最偷懒的错误码就是行数(__LINE__)!
另一处晦涩的接口来自1.2节的Omci_List_Query函数,该函数参数长度可变,但没有任何函数签名和注释。使用它的唯一“安全”方式就是搜索已有的调用代码,照猫画虎。
1.8 警告丛生
GCC编译器所提示的模块内警告多达1361处。若再进行更为严苛的pclint检查,则可以预想警告的暴增。
这是前人馈赠给后人的一大“遗产”,其中暗含不计其数的陷阱……
二 小结
以上示出的主要为函数级问题,尚未涉及流程设计。事实上,糟糕的流程设计不仅降低了代码执行效率,而且增加了人工理解和维护的难度。迷失在巨型函数森林里的人们,除了焦虑和绝望别无一物。也许,会有少数人知晓某条林间蹊径,但无法驱散追随者心中的不安。
幸运的是,软件工程先哲们指出了一条路:重构。那么,亲们准备好了吗?
MDU某产品OMCI模块代码质量现状分析的更多相关文章
- sonar+Jenkins 构建代码质量自动化分析平台
1.Sonar 介绍 Sonar 是一个用于管理代码质量的开源工具,可以分析代码中的bug和漏洞以及Code Smells,支持20多种编程语言的检测,如java,c/c++,python,php等语 ...
- 分析师机构发布中国低代码平台现状分析报告,华为云AppCube为数字化转型加码
摘要:Forrester指出,中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码. 华为消息,知名研究与分析机构Forrester Resear ...
- Linux-3.0.8中基于S5PV210的GPIO模块代码追踪和分析
编写按键驱动时,想知道内核是如何管理GPIO的,所以开始追踪代码,中间走了一些弯路,现记录于此. 追踪代码之前,我猜测:第一,这部分代码应该在系统set up阶段执行:第二,GPIO的代码应该在mac ...
- Linux-3.0.8中基于S5PV210的IRQ模块代码追踪和分析
init/main.c: asmlinkage void start_kernel(void) { ...... early_irq_init(); init_IRQ(); ...... } earl ...
- 使用JSLint提高JS代码质量
随着富 Web 前端应用的出现,开发人员不得不重新审视并重视 JavaScript 语言的能力和使用,抛弃过去那种只靠“复制 / 粘贴”常用脚本完成简单前端任务的模式.JavaScript 语言本身是 ...
- ESLint 检查代码质量
利用 ESLint 检查代码质量 其实很早的时候就想尝试 ESLint 了,但是很多次都是玩了一下就觉得这东西巨复杂,一执行检查就是满屏的error,简直是不堪入目,遂放弃.直到某天终于下定决心深入看 ...
- (转)提高代码质量---one
1. 摘要 这是烂代码系列的第二篇,在文章中我会跟大家讨论一下如何尽可能高效和客观的评价代码的优劣. 在发布了关于烂代码的那些事(上)之后,发现这篇文章竟然意外的很受欢迎,很多人也描(tu)述(cao ...
- Linux下SonarQube代码质量平台的安装和使用方法
Sonar简介: Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量,可以从七个维度检测代码质量 通过插件形式,可以支持包括java,C#,C/C++,PL/SQL,Cobol,Java ...
- 前端代码质量保障之代码review
经验丰富的程序员和一般程序员之间的最大区别,不仅体现在解决问题的能力上, 还体现在日常代码的风格上.掌握一门技术可能需要几月,甚至几周就够了. 好的习惯风格养成却需数年. 团队成员之间需要合作,代码需 ...
随机推荐
- linux下时间同步的两种方法分享
方法1:与一个已知的时间服务器同步 复制代码 代码如下: ntpdate time.nist.gov 其中 time.nist.gov 是一个时间服务器. 删除本地时间并设置时区为上海 复制代码 代码 ...
- js 创建多行字符串
function heredoc(fn) { ,-).join('\n') + '\n' } var tmpl = heredoc(function(){/* !!! 5 html include h ...
- js判断字符串是否json格式
function isJSON(str) { if (typeof str == 'string') { try { var obj=JSON.parse(str); if(typeof obj == ...
- css3实现小箭头,各种图形
转:http://blog.csdn.net/tangtang5963/article/details/51490107 https://segmentfault.com/a/119000000278 ...
- iOS:自定义字体
转自: <iOS tips: Custom Fonts> Post by Steve Vlaminck My good friend google told me that using a ...
- Mac mysql 解决中文乱码
Mac mysql 解决中文乱码问题 出现"???"之类的无法识别的乱码 到/etc目录下自己建一个my.cnf文件(需要最高权限,使用sudo su),然后写入内容: [clie ...
- Python学习(五)——列表操作全透析
列表是以类的形式实现的. "创建"列表实际上是将一个类实例化. 因此,列表有多种方法能够操作. Python列表操作的函数和方法 列表操作包括下面函数: 1.cmp(list1, ...
- iOS模拟(糟糕的)网络环境
有时候为了模拟在糟糕的网络环境下app的表现,会故意拔网线(断wifi),苹果其实提供了专门的工具来精确地模拟你在几个预设的场景下的网络连接情况:Network Link Conditioner 点击 ...
- 如何用BarTender将日期变量和序列号变量放一起打印成条码?
刚接触BarTender 2016的小伙伴们可能对条码的数据源还不太搞的定,例如有时需要将日期变量和序列号变量放一起打印成条码,那如何简单达到目的呢?下面,小编教大家解决这一问题的三大步骤. 1.在B ...
- C++中使用ODBC API访问数据库例程
使用ODBC API访问数据库简单流程,供参考使用: ODBC API 123456789101112131415161718192021222324252627282930313233343536 ...