一、sharedBuffer&string如何牵手的
在之前看android camera framework代码时,发现代码中大量使用了string8、string16类。由于之前学习的是C#,java等高级面向对象语言唯独没有认真研究过C++。所以对C++中的string比较陌生。虽然他们的用法都是一样的,但是这里还是想认真研究一下底层实现原理。
string8和string16原理基本上是一样的,这里我们就分析常用的string8了,说到string8,我们就能想到和它有千丝万缕的sharedbuffer类。string8的内存就是通过sharebuffer类来管理的。每个string8对象对应一个sharebuffer的对象,sharedBuffer对象的下面就是我们的string8的内存空间。string8类中有一个mString的私有域就是指向sharedBuffer下面开始处的,如下图所示。
为了说明string8对象就是sharedBuffer下面的内存空间,我们来看看shredBuffer对象的的的alloc方法。可以看到在申请了sizeof(SharedBuffer)+size大小的空间,其中size就是我们字符串大小。
SharedBuffer* SharedBuffer::alloc(size_t size)
{
SharedBuffer* sb = static_cast<SharedBuffer *>(malloc(sizeof(SharedBuffer) + size));
if (sb) {
sb->mRefs = 1;
sb->mSize = size;
}
return sb;
}
上面的sharedBuffer类我们只看到确实是多申请了Buffer,没看到是不是string8真的指向sharedBuffer对象下面的空间呢。我们来看看String8常用构造函数,下面的调用了allocFromUTF8方法。我们继续来到allocFromUTF8 慌然看到了sharedBuffer::alloc()方法,而且申请的大小是len+1,大家想想为啥还要+1.考虑一会...................
时间到了,大家在学习C语言的时候,都要在最后加一个'\n'换行符,也就是下面的str[len]=0;这点我思考了10分钟。哎。我们在看看这行代码char* str = (char*)buf->data();针对sharedBuffer的data方法,我们在下面会看到返回的是this+1.由于this指针的类型是sharedBuffer类型,所以+1就意味这加了sizeof(sharedBuffer)大小,加上这么大的大小就指向了紧挨着sharedBuffer的空间了,就是上图所示那样。不知道大家明白了没有。
String8::String8(const char* o)
: mString(allocFromUTF8(o, strlen(o))) //调用<span style="font-family:SimSun;">下面的函数</span>
{
if (mString == NULL) {
mString = getEmptyString();
}
}
static char* allocFromUTF8(const char* in, size_t len)
{
if (len > 0) {
SharedBuffer* buf = SharedBuffer::alloc(len+1); //为什么<span style="font-family:SimSun;">多+1???</span>
ALOG_ASSERT(buf, "Unable to allocate shared buffer");
if (buf) {
char* str = (char*)buf->data(); //笔锋转到下面的data方法
memcpy(str, in, len);
str[len] = 0;
return str;
}
return NULL;
}
return getEmptyString();
}
//shardBuffer的data方法
const void* SharedBuffer::data() const {
return this + 1;
}
二、sharedBuffer类介绍
下图是sharedBuffer类的全部属性和部分方法。从名字上来看,该类视乎和共享buffer有关系,其实仔细看看代码,它就只是对底层标准malloc等c库函数的再一次封装。我们就来给大家介绍几个我自认为重要的方法吧。
SharedBuffer* SharedBuffer::editResize(size_t newSize) const
{
if (onlyOwner()) { //这里判断是不是只有owner在用这个sharedBuffer对象,具体代码请看附件吧
SharedBuffer* buf = const_cast<SharedBuffer*>(this);//拿到当前对象的指针
if (buf->mSize == newSize) return buf;
buf = (SharedBuffer*)realloc(buf, sizeof(SharedBuffer) + newSize); //重新申请一个sharedBuffer对象,realloc会申请制定大小的内存并拷贝
if (buf != NULL) {
buf->mSize = newSize;
return buf; //返回这个新建的sharedBuffer对象,看来这只能在sharedBuffer类中使用。
}
}
SharedBuffer* sb = alloc(newSize); //如果不只有一个拥有者,存在引用对象,那么就重新申请一个sharedBuffer对象,防止对源数据的影响。
if (sb) {
const size_t mySize = mSize;
memcpy(sb->data(), data(), newSize < mySize ? newSize : mySize); //拷贝数据
release(); //释放当前对象了。
}
return sb;//返回新创建的sharedBuffer对象
}
这个函数就是根据当前string对象,找到它的知己sharedBuffer对象,奇怪的地方就是代码中为什么-1,
SharedBuffer* SharedBuffer::bufferFromData(void* data) {
return data ? static_cast<SharedBuffer *>(data)-1 : 0; //为什么要-1,减1就指向了sharedBuffer对象地址了。
}
size_t SharedBuffer::sizeFromData(const void* data) {
return data ? bufferFromData(data)->mSize : 0; //这是给sring类调用,用于返回string对象内存大小
}
bool SharedBuffer::onlyOwner() const {
return (mRefs == 1); //引用计数器为1时,只有创建者自己在使用此对象。
}
三、string8类介绍
下图是string8全部的属性和部分方法,同样这里我也只贴出部分我自认为重要的方法,来分析一下,详细伙计们还是看源码吧。
下面这个方法一看大家都明白,就是为当前string对象设置数据了
status_t String8::setTo(const char* other)
{
const char *newString = allocFromUTF8(other, strlen(other)); //申请新的内存空间,其实生成了新的sharedBuffer对象
SharedBuffer::bufferFromData(mString)->release(); //释放旧的内存空间
mString = newString;
if (mString) return NO_ERROR;
mString = getEmptyString();
return NO_MEMORY;
}
void String8::setTo(const String8& other)
{
SharedBuffer::bufferFromData(other.mString)->acquire(); //被引用的string8对象,引用计数+1.
SharedBuffer::bufferFromData(mString)->release();
mString = other.mString;
}
status_t String8::append(const char* other) //在原有数据基础上,添加对应的数据,还是看源码吧^_^
{
return append(other, strlen(other));
}
此函数有点不太好理解,我在本地调试时,试了很多情况,结果也没变化。后来发现原来是引用计数+1,-1原子操作时存在问题。当一个对象存在多个引用对象时,调用lockBuffer()方法,会返回一个新的string对象,以保护源数据。
char* String8::lockBuffer(size_t size)
{
SharedBuffer* buf = SharedBuffer::bufferFromData(mString) //找到当前string对象的好基友sharedBuffer对象
->editResize(size+1);
if (buf) {
char* str = (char*)buf->data();
mString = str;
return str;
}
return NULL;
}
四、测试代码
下面只列出了main函数的实现内容,完整的代码大家可以下载 附件,这里就是演示一下常用的函数。int main(int argc, char **argv)
{
android::initialize_string8();
android::initialize_string16();
// start of your code
String8 str1("armwind");
String8 str2(" is a good man! ^_^");
String8 add8 = str1 + str2; //这个在类中有重载运算符的实现
cout<<"name:"<<add8.string()<<endl;
add8.toUpper();
cout<<"toUpper:"<<add8.string()<<endl;
String8 &str4 =add8; //下面3行都是在后来添加的
str4.setTo(str3); //add8的引用计数会+1
str4.setTo(str3);
cout<<"lockBuffer:"<<add8.lockBuffer(8)<<endl;//这里调用lockBuffer,会返回一个字符串,显示的只是我们想要访问的8字节。
String8 str3 = add8;
cout<<"lockBuffer_test:"<<str3.string()<<endl;
add8.setTo("hello");//重新设置成某个字符串
cout<<"name:"<<add8.string()<<endl;
add8.append(" is a good boy!"); //扩展
cout<<"name:"<<add8.string()<<endl;
add8.clear();
cout<<"clear:"<<add8.string()<<endl;
cout<<"format:"<<String8::format("hello %c","world").string()<<endl;
//end of your code
android::terminate_string16();
android::terminate_string8();
return kPassed;
}
打印结果:
lockBuffer:ARMWIND IS A GOOD MAN! ^_^
lockBuffer_test:ARMWIND IS A GOOD MAN! ^_^
name:hello
name:hello is a good boy!
clear:
format:hello p
分析:上面的代码只是部分main函数,完整的代码大家可以下载 附件,除了initialize_string()和terminate_string()函数,其它的函数都是在探究一下string8的特性。目前在调用format函数时,输出的结果并不是我们意向中的“hello world”,这里可能在移植到x86下,一些数据类型不匹配导致的吧。具体原因我就不找了,如果大家找到原因了,可以贴到评论上^_^,此外示例源码中android_atomic_add()函数也存在问题,即在执行+1,-1操作时,没有将修改后的值赋给引用计数器(大家下载下来自己动手改改吧,我就不上传了)。这样就会导致后面lockbuffer()时探索遇到了点问题。
开始和结尾的init代码(如下所示),本来是在static.cpp代码中调用的,这里由于不想在加一个源文件,就直接在这里调用了。
android::initialize_string8();
android::initialize_string16();
android::terminate_string16();
android::terminate_string8();
下面可以看到,在类的构造函数和析构函数中,调用了相应的初始化函数和终结函数。而在类声明的下方就定义了LibUtilsFirstStatics 对象 gFirstStatics 该对象其它文件中也没调用,也许就是为了调用init函数和终结函数吧。
class LibUtilsFirstStatics
{
public:
LibUtilsFirstStatics()
{
initialize_string8();
initialize_string16();
}
~LibUtilsFirstStatics()
{
terminate_string16();
terminate_string8();
}
};
static LibUtilsFirstStatics gFirstStatics;
五、调试过程中遇到的问题
1) 在调试过程,由于命名空间的使用问题,导致一些方法找不到,编译不过。解决办法就是添加对应的命名空间
2) 假如在类A中使用了类B,在没有包含类B时,编译时提示找不到类B相应的方法。解决办法就是包含对应的类,如直接包含class String8; 结构体也可以这样包含。
3) main函数不能包含在命名空间中,要不然编译会出问题的。