各位老铁们好,相信很多人对多端网站源码分享都不是特别的了解,因此呢,今天就来为大家分享下关于多端网站源码分享以及简单网站源码的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!
作者:lucasfan,腾讯IEG游戏客户端开发工程师
使用C++进行SDK开发的同学一定少不了遇到现网偶现的Crash问题,而崩溃堆栈有很多直接Crash在std::string源码中,std::string源码晦涩难懂增加了Debug的难度。在多次与std::string的斗争中,笔者阅读了多个版本的string实现源码,对重要的实现进行分析、并对比其实现,希望对各位同学有帮助。
本文将对比以下几个版本的string源码实现。
string版本场景特性libstdc++string(gnu4.9)腾讯内部AndroidSDK常用写时拷贝(COW)libc++string腾讯内部iOSSDK常用短字符串优化(SSO);右值拷贝构造tpstlstring腾讯自研string,SDK内部使用解决跨库问题;内存池
在Class实现中,最重要的就是Class的定义、内存结构、常用构造器、=操作符、析构方法,本文将对三种不同的string实现进行介绍。
一、libstdc++string
目前公司的AndroidSDK普遍采用了gnu4.9版本的C++库。根据项目经验,Android平台string的崩溃率是远远超过iOS的,因此也是本次介绍的重点。
1、定义
typedefbasic_string<char>string;\n\ntemplate<typename_CharT,typename_Traits=char_traits<_CharT>,\ntypename_Alloc=allocator<_CharT>>\nclassbasic_string;\n
可以看到string其实就是basic_string<char>,通过basic_string可以构造出不同字符类型的字符串类型。比如wstring就是basic_string<wchar_t>。
查看basic_string可以发现basic_string包括了三个模板参数,分别是:
_CharT字符类型;_Traits特性类,主要提供char特性相关的方法,比如求字符串长度;_Alloc内存分配器,主要用于字符串的内存分配。
_Traits和_Alloc`不是本文介绍的重点,有兴趣的同学可以自己查看源码学习。
2、内存结构
通过代码发现std::string中只包括一个成员变量_M_dataplus。
mutable_Alloc_hider_M_dataplus;\n\nstruct_Alloc_hider:_Alloc\n{\n_Alloc_hider(_CharT*__dat,const_Alloc&__a)_GLIBCXX_NOEXCEPT\n:_Alloc(__a),_M_p(__dat){}\n\n_CharT*_M_p;//Theactualdata.\n};\n
_Alloc_hider包括一个成员变量_M_p,存储了真实的字符串地址。因此在栈上分配一个string时,这个栈上的string只保存了一个地址。
struct_Rep_base\n{\n//字符串的真实长度\nsize_type_M_length;\n//字符串的容量\nsize_type_M_capacity;\n//引用计数\n_Atomic_word_M_refcount;\n};\n\nstruct_Rep:_Rep_base\n{\n/**/\n}\n
那么字符串的长度信息保存在哪里呢?
其实在构造时,string会在堆上申请一个内存空间,包括了一个_Rep类型的对象和一个字符串内存。_Rep就包括了字符串的长度等信息,具体可看其代码定义。
不过_M_p指向的并不是_Rep数据结构的起始地址,而是字符串的起始地址。由于_Rep数据结构的大小是已知的,因此可以通过字符串的起始地址减_Rep的大小,就可以获取_Rep对象的地址。
3、char*构造器
std::stringstr(&34;);\n
当用一个char*去构造std::string时,即调用了char*构造器。
template<typename_CharT,typename_Traits,typename_Alloc>\nbasic_string<_CharT,_Traits,_Alloc>::\nbasic_string(const_CharT*__s,const_Alloc&__a)\n:_M_dataplus(_S_construct(__s,__s?__s+traits_type::length(__s):\n__s+npos,__a),__a)\n{}\n
char*构造器的具体实现是空的,初始化是在初始化列表中。_S_construct方法返回的字符串地址和分配器__a构造了_M_dataplus。
_Alloc_hider(_CharT*__dat,const_Alloc&__a)_GLIBCXX_NOEXCEPT\n:_Alloc(__a),_M_p(__dat){}\n
_M_dataplus的类型是_Alloc_hider,其构造器只是简单的地址拷贝。最主要的就是将构造的地址拷贝到_Alloc_hider中。
template<typename_CharT,typename_Traits,typename_Alloc>\ntemplate<typename_InIterator>\n_CharT*\nbasic_string<_CharT,_Traits,_Alloc>::\n_S_construct(_InIterator__beg,_InIterator__end,const_Alloc&__a,\nforward_iterator_tag)\n{\nendif\n//NB:Notrequired,butconsideredbestpractice.\nif(__gnu_cxx::__is_null_pointer(__beg)&&__beg!=__end)\n__throw_logic_error(__N(&34;));\n//计算字符串长度\nconstsize_type__dnew=static_cast<size_type>(std::distance(__beg,__end));\n//Checkforout_of_rangeandlength_errorexceptions.\n//_S_create申请内存空间,返回的是_Rep数据结构地址\n_Rep*__r=_Rep::_S_create(__dnew,size_type(0),__a);\n__try//拷贝数据\n{_S_copy_chars(__r->_M_refdata(),__beg,__end);}\n__catch(…)\n{\n//如果发生异常,_M_destory销毁分配的字符串空间\n__r->_M_destroy(__a);\n__throw_exception_again;\n}\n//设置字符串长度,并将引用计数为0(0表示实际的引用个数为1)\n__r->_M_set_length_and_sharable(__dnew);\n//返回字符串地址\nreturn__r->_M_refdata();\n}\n
_S_construct进行了内存空间的申请和字符串的拷贝操作。
根据以上代码综合来看,char*构造器其实就是申请了一块内存并进行了字符串的拷贝操作。
4、拷贝构造
std::stringorginStr=&34;;\nstd::stringnewStr(orginStr);//拷贝构造\n
拷贝构造同样常见,也非常重要。
template<typename_CharT,typename_Traits,typename_Alloc>\nbasic_string<_CharT,_Traits,_Alloc>::\nbasic_string(constbasic_string&__str)\n:_M_dataplus(__str._M_rep()->_M_grab(_Alloc(__str.get_allocator()),\n__str.get_allocator()),\n__str.get_allocator())\n{}\n
与char*构造器不同的主要是构造字符串的方法,由_S_construct变为了__str._M_rep()->_M_grab。
_CharT*\n_M_grab(const_Alloc&__alloc1,const_Alloc&__alloc2)\n{\nreturn(!_M_is_leaked()&&__alloc1==__alloc2)\n?_M_refcopy():_M_clone(__alloc1);\n}\n
_M_grab实现了:如果字符串可共享,进行引用拷贝,否则进行深度拷贝。
正常情况下,字符串都是可共享的。只有个别情况下不可共享,比如这个字符串正在被写入时就不可被共享。
先看下引用拷贝的方法实现:
_CharT*\n_M_refcopy()throw()\n{\nendif\n__gnu_cxx::__atomic_add_dispatch(&this->_M_refcount,1);\nreturn_M_refdata();\n}\n
注意__builtin_expect只是用于编译器优化的方法,返回值仍然是第一个参数。
在引用拷贝的方法实现_M_refcopy中,对字符串的引用计数+1,然后直接返回源字符串的字符串地址。
方法返回后,用源字符串的地址构造新的字符串,也就是说新的std::string内部保存了源字符串同样的地址,只是引用计数增加了1。
再看一下发生直接拷贝时的代码实现。
template<typename_CharT,typename_Traits,typename_Alloc>\n_CharT*\nbasic_string<_CharT,_Traits,_Alloc>::_Rep::\n_M_clone(const_Alloc&__alloc,size_type__res)\n{\n//Requestedcapacityoftheclone.\nconstsize_type__requested_cap=this->_M_length+__res;\n_Rep*__r=_Rep::_S_create(__requested_cap,this->_M_capacity,\n__alloc);\nif(this->_M_length)\n_M_copy(__r->_M_refdata(),_M_refdata(),this->_M_length);\n\n__r->_M_set_length_and_sharable(this->_M_length);\nreturn__r->_M_refdata();\n}\n
_M_clone的方法也比较容易理解,就是进行内存分配和字符串拷贝,并设置字符串长度、引用计数。
5、=操作符
std::stringstr1;\nstd::stringstr2(&34;);\n\nstd1=str2;//使用operator=\n
=操作符的代码实现比较简单,都是调用重载了的assign方法。
basic_string&\noperator=(constbasic_string&__str)\n{returnthis->assign(__str);}\n\n\nbasic_string&\noperator=(const_CharT*__s)\n{returnthis->assign(__s);}\n
assign实现类似,以assign(constbasic_string&__str)举例。
template<typename_CharT,typename_Traits,typename_Alloc>\nbasic_string<_CharT,_Traits,_Alloc>&\nbasic_string<_CharT,_Traits,_Alloc>::\nassign(constbasic_string&__str)\n{\nif(_M_rep()!=__str._M_rep())\n{\n//XXXMT\nconstallocator_type__a=this->get_allocator();\n//调用_M_grab对源字符串进行拷贝\n_CharT*__tmp=__str._M_rep()->_M_grab(__a,__str.get_allocator());\n//对现有字符串的堆上内存进行析构处理\n_M_rep()->_M_dispose(__a);\n_M_data(__tmp);\n}\nreturn*this;\n}\n
assign方法内部主要是对源字符串进行拷贝,然后对现在字符串的内存进行了析构处理,并用新的字符串地址构造了当前字符串。
void\n_M_dispose(const_Alloc&__a)_GLIBCXX_NOEXCEPT\n{\nendif\n{\n//Berace-detector-friendly.Formoreinfoseebits/c++config.\n_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&this->_M_refcount);\n//__exchange_and_add_dispatch对_M_refcount进行减1,但会返回_M_refcount原来的值\nif(__gnu_cxx::__exchange_and_add_dispatch(&this->_M_refcount,\n-1)<=0)\n{\n_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&this->_M_refcount);\n//销毁当前内存空间\n_M_destroy(__a);\n}\n}\n}\n
_M_dispose方法用于析构字符串占用的内存空间。其会判断当前字符串的引用计数,如果当前的引用计数<=0,才会销毁当前的内容空间,否则只会将引用计数减1。
6、析构方法
string的析构方法就是调用_M_dispose,如果引用计数<=0,才会真正销毁堆上的空间。
~basic_string()_GLIBCXX_NOEXCEPT\n{_M_rep()->_M_dispose(this->get_allocator());}\n
7、COW特性
gnulibstdc++实现的std::string主要使用了写时拷贝(COW)特性,用于解决如下问题:
大多数的string拷贝都用于只读每次拷贝消耗性能
但是写时拷贝的特性导致存在一些问题,包括:
存在多线程风险。比如某个std::string通过COW进行拷贝后,一个堆上的字符串有可能会被多个线程同时访问,存在有多线程风险。可能增加内存拷贝情况。比如A和B共享同一段内存,在多线程环境下同时对A和B进行写操作,可能会有如下序列:A写操作,A拷贝内存,B写操作,B拷贝内存,A对引用计数减一,B对引用计数减一,加上初始的一次构造总共三次内存申请,如果使用全拷贝的string,只会发生两次内存申请。
二、libc++string
目前iOS平台使用的C++版本均已经切到了llvm实现的libc++。
1、定义
string的定义也比较简单,主要的实现仍然是在basic_string中。
template<class_CharT,//for<stdexcept>\nclass_Traits=char_traits<_CharT>,\nclass_Allocator=allocator<_CharT>>\nclass_LIBCPP_TEMPLATE_VISbasic_string;\ntypedefbasic_string<char,char_traits<char>,allocator<char>>string;\n
2、内存结构
libc++string的内存结构更巧妙一些,针对使用过程中更多的字符串都是短字符串,且字符串经常是入栈申请、出栈销毁的情况。libc++string将字符串的内存结构分为了长字符串模式和短字符串模式。
长字符串模式下,在栈上保存字符串容量、大小和堆上申请的字符串地址。短字符串模式下,直接将其数据存在栈中,而不去堆中动态申请空间,避免了申请堆空间所需的开销。
struct__long//24字节\n{\n//字符串容量\nsize_type__cap_;\n//字符串实际大小\nsize_type__size_;\n//字符串指针\npointer__data_;\n};\n\n//__min_cap=24-1(long类型的大小减一个字节的大小,1个字节用于存储短字符串的实际大小)\nenum{__min_cap=(sizeof(__long)-1)/sizeof(value_type)>2?\n(sizeof(__long)-1)/sizeof(value_type):2};\n\nstruct__short//24字节\n{\nunion\n{\nunsignedchar__size_;\nvalue_type__lx;\n};\nvalue_type__data_[__min_cap];\n};\n\nunion__ulx{__long__lx;__short__lxx;};\n\nenum{__n_words=sizeof(__ulx)/sizeof(size_type)};\n\nstruct__raw//24字节\n{\nsize_type__words[__n_words];\n};\n\n//最关键的联合体类型\nstruct__rep\n{\nunion\n{\n__long__l;\n__short__s;\n__raw__r;\n};\n};\n\n//唯一的成员变量\n__compressed_pair<__rep,allocator_type>__r_;\n
string唯一的成员变量就是__r_,最主要的是保存了一个__rep。
__rep是一个联合体类型,可以保存__long或__short,而__raw只是用于便捷的用数组的方式操作字符串。__long和__short分别代表了两种字符串模式。
可以发现,string巧妙的使用了联合体类型,来保存不同模式的字符串。
既然一个空间既可以表示长字符串又可以表示短字符串,那么如何判断这个字符串到底是长字符串还是短字符串呢?
libc++string是通过一个bit标志位来判断的。
长字符串__cap_最后一个字节的末位bit固定为1短字符串__size_的末位bit固定为0
由于引入了这个标志位:
长字符串的容量就必须为偶数(末位只作为标志位,真实容量=_cap-1)短字符串的长度保存时需要左移一位,而取出是需要右移一位,用于保存末位的0
3、char*构造器
template<class_CharT,class_Traits,class_Allocator>\ninline_LIBCPP_INLINE_VISIBILITY\nbasic_string<_CharT,_Traits,_Allocator>::basic_string(const_CharT*__s)\n{\n_LIBCPP_ASSERT(__s!=nullptr,&34;);\n__init(__s,traits_type::length(__s));\nendif\n}\n
libc++的char*构造器是主要调用的是__init方法。
template<class_CharT,class_Traits,class_Allocator>\nvoid\nbasic_string<_CharT,_Traits,_Allocator>::__init(constvalue_type*__s,size_type__sz)\n{\nif(__sz>max_size())\nthis->__throw_length_error();\npointer__p;\n//<=22字节的为短字符串\nif(__sz<__min_cap)\n{\n//设置短字符串长度\n__set_short_size(__sz);\n//获取短字符串首地址\n__p=__get_short_pointer();\n}\nelse//>=23的为长字符串\n{\n//__recommend获得推荐的容量\nsize_type__cap=__recommend(__sz);\n//分配空间\n__p=__alloc_traits::allocate(__alloc(),__cap+1);\n//设置__rep数据\n__set_long_pointer(__p);\n__set_long_cap(__cap+1);\n__set_long_size(__sz);\n}\n//拷贝数据\ntraits_type::copy(_VSTD::__to_raw_pointer(__p),__s,__sz);\n//末尾设置为\\0\ntraits_type::assign(__p[__sz],value_type());\n}\n
__init方法主要是针对长短字符串,分别实现了初始化方法。
短字符串,直接使用当前栈上的空间;长字符串,申请推荐的容量大小,进行初始化设置。
4、左值拷贝构造
在介绍拷贝构造之前,先回顾一下之前学习的C++知识:左值、右值、转移语义。
左值:非临时变量。如std::stringa,a为左值;右值:临时的对象,只在当前语句有效。如std::string()为右值;转移语义可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高C++应用程序的性能;拷贝语义&转移语义约等于拷贝&剪切。
C++中&用于表示左值引用,&&用于表示右值引用。
如果拷贝构造时,源字符串是一个左值,将调用左值拷贝构造函数。
template<class_CharT,class_Traits,class_Allocator>\nbasic_string<_CharT,_Traits,_Allocator>::basic_string(constbasic_string&__str)\n:__r_(__second_tag(),__alloc_traits::select_on_container_copy_construction(__str.__alloc()))\n{\nif(!__str.__is_long())\n//如果为短字符串,使用数组(__raw)的方式直接拷贝\n__r_.first().__r=__str.__r_.first().__r;\nelse\n//如果为长字符串,使用__init方法进行内存拷贝\n__init(_VSTD::__to_raw_pointer(__str.__get_long_pointer()),__str.__get_long_size());\nendif\n}\n
左值拷贝构造函数的源字符串如果为
短字符串,使用数组(__raw)的方式直接拷贝;长字符串,使用__init方法进行内存拷贝。
5、右值拷贝构造
libc++string实现时就很好的使用了转移语义。如果源字符串为右值,可以直接将源字符串的数据转移到新的字符串,而不用重新申请空间。其实就是将源string堆上申请的空间直接交给新的string管理,源string不再管理原来的内存。
template<class_CharT,class_Traits,class_Allocator>\ninline_LIBCPP_INLINE_VISIBILITY\nbasic_string<_CharT,_Traits,_Allocator>::basic_string(basic_string&&__str)\nelse\n_NOEXCEPT\nif_LIBCPP_DEBUG_LEVEL>=2\n//…\nif_LIBCPP_DEBUG_LEVEL>=2\n__get_db()->__erase_c(this);\n#endif\nif(__is_long())\n__alloc_traits::deallocate(__alloc(),__get_long_pointer(),__get_long_cap());\n}\n
三、TPSTLstring
Tpstl是腾讯自己开发一个简化版STL。主要是为了解决:
当以静态库形式提供基础组件服务时,原生的stl代码容易和目标app编译产生冲突,通过自实现stl代码,可以有效规避这种问题。std::string实现过于复杂,难定位问题。
1、定义
tpstlstring定义比较简单,就是basic_string<char>。
typedefbasic_string<char>string;\n
2、内存结构
内存结构包括了字符串地址和字符串的长度。
template<class_Tp>classbasic_string{\nprivate:\n//字符串地址\n_Tp*_M_buf;\n//字符串长度\nsize_t_M_len;\n}\n
3、char*构造器
basic_string(const_Tp*s)\n:_M_buf(0),_M_len(0)\n{\nassign_str(s);\n}\n
char*构造器中,会首先将_M_buf和_M_len初始化为空值。然后调用assign_str方法。
template<class_Tp>\nvoidbasic_string<_Tp>::assign_str(const_Tp*s)\n{\n//将原有_M_buf析构\n_M_deallocate(_M_buf);\n_M_buf=0;\n_M_len=0;\n\nif(s!=0)\n{\n//取字符串长度\nsize_tlen=strlen(s);\n//分配内存空间\n_M_buf=_M_allocate(len+1);\nif(_M_buf==0)\n{\n__TPSTL_ASSERT(0);\nreturn;\n}\n//字符串拷贝\nfor(size_ti=0;i<len;i++)\n{\n_M_buf[i]=s[i];\n}\n//末位置0\n_M_buf[len]=0;\n_M_len=len;\n}\n}\n
assign_str方法主要是析构原有字符串,并申请空间、进行字符串拷贝操作。
需要注意的是,tpstl并没有直接使用系统的malloc和free方法,而是使用了自己实现的_M_allocate和_M_deallocate方法。实际上tpstl进行内存申请和释放都是在其内存池上进行的。
_Tp*_M_allocate(size_t__n)\n{\n_Tp*ptr=(_Tp*)__TPSTL_NAMESPACE_EX::allocate_node(sizeof(_Tp)*__n);\nif(ptr==0)\n{\n__TPSTL_ASSERT(0);\nreturn0;\n}\n__TPSTL_LEAK_COUNT_INC(sizeof(_Tp)*__n);\n\nreturnptr;\n}\n\nvoid_M_deallocate(_Tp*__p)\n{\nif(__p==0)return;\n\n__TPSTL_LEAK_COUNT_DEC(sizeof(_Tp)*(_M_len+1));\n\n__TPSTL_NAMESPACE_EX::deallocate_node(__p,(_M_len+1));\n}\n
4、拷贝构造
tpstlstring的拷贝构造也只是使用了assign_str方法。并没有做特殊处理。
basic_string(constbasic_string<_Tp>&__x)\n:_M_buf(0),_M_len(0)\n{\nassign_str(__x.c_str());\n}\n
5、=操作符
tpstlstring的=操作符也是很简单,也只是使用了assign_str方法。也并没有做特殊处理。
basic_string<_Tp>&operator=(constbasic_string<_Tp>&__x)\n{\nif(&__x!=this)\n{\nassign_str(__x.c_str());\n}\nreturn*this;\n}\n
6、析构方法
string的析构方法调用了_M_deallocate方法,实际都是在内存池上进行的。
~basic_string()\n{\n_M_deallocate(_M_buf);\n}\n
7、内存池
TPSTL内部使用了内存池,其主要目的:
解决内存碎片问题。由于每次都malloc,产生了大量的内存碎片,通过使用内存池,每次分配一个较大的内存,可以避免内存碎片问题。减少malloc调用次数,降低性能消耗。每次申请内存时,均通过内存池分配,大大减少了malloc的次数。
内存池的实现原理是:
针对8、16、24、32…128字节的string分配内存池,大于128的字节直接malloc。针对不同大小的string,每次分配一块1KB的空间用于内存分配,分配内存时直接从内存池中取。内存申请和释放达到一定阈值后,可进行内存重整,回收不用的内存
内存池针对不同大小的字符串,分别分配了不同的内存池,比如一个13字节的字符串,会在16字节大小的内存池上进行分配。
在需要进行内存分配时,每次分配一块1KB的空间用于内存分配,如果是16字节大小的内存,每个内存块就可以存储1024/16个字符串(其实还有一个区域存储公共字段)。
当内存块中的内存全部被分配过了,就会再创建一个内存块,每个内存块之间通过指针串起来。
如果使用过程中,某个内存被回收,则会将下一个要被分配空间地址的指向这个内存。
当内存申请和释放达到一定阈值时,会进行内存的重整,释放掉内存全部被释放的内存块,节省内存空间。
四、结语
通过阅读主流移动端SDK相关的string源码,我们已经基本理解了其内部实现的原理。在出现Crash问题时,也就可以根据堆栈信息找到具体的排查方向。
后续我会再整理一些string源码崩溃的案例,分享解决问题的思路和方法。
更多干货尽在腾讯技术,官方QQ交流群已建立,交流讨论可加:711094866。
文章分享结束,多端网站源码分享和简单网站源码的答案你都知道了吗?欢迎再次光临本站哦!
